Merge pull request #8423 from overleaf/briangough-issue8388

increase lock and timeout for history resync to 6 minutes

GitOrigin-RevId: 2aea0cbc26c92ed0aad8f815ccd41a2abc0b752e
This commit is contained in:
Brian Gough 2022-06-17 09:03:31 +01:00 committed by Copybot
parent 57114c4503
commit 2970a09d19
9 changed files with 118 additions and 67 deletions

View file

@ -149,6 +149,7 @@ app.delete('/project', HttpController.deleteMultipleProjects)
app.post('/project/:project_id', HttpController.updateProject) app.post('/project/:project_id', HttpController.updateProject)
app.post( app.post(
'/project/:project_id/history/resync', '/project/:project_id/history/resync',
longerTimeout,
HttpController.resyncProjectHistory HttpController.resyncProjectHistory
) )
app.post('/project/:project_id/flush', HttpController.flushProject) app.post('/project/:project_id/flush', HttpController.flushProject)
@ -258,3 +259,8 @@ for (const signal of [
]) { ]) {
process.on(signal, shutdownCleanly(signal)) process.on(signal, shutdownCleanly(signal))
} }
function longerTimeout(req, res, next) {
res.setTimeout(6 * 60 * 1000)
next()
}

View file

@ -221,6 +221,7 @@ function resyncProjectHistory(
path: `/project/${projectId}/history/resync`, path: `/project/${projectId}/history/resync`,
json: { docs, files, projectHistoryId }, json: { docs, files, projectHistoryId },
method: 'POST', method: 'POST',
timeout: 6 * 60 * 1000, // allow 6 minutes for resync
}, },
projectId, projectId,
'resync-project-history', 'resync-project-history',
@ -299,6 +300,7 @@ function _makeRequest(options, projectId, metricsKey, callback) {
url: `${settings.apis.documentupdater.url}${options.path}`, url: `${settings.apis.documentupdater.url}${options.path}`,
json: options.json, json: options.json,
method: options.method || 'GET', method: options.method || 'GET',
timeout: options.timeout || 30 * 1000,
}, },
function (error, res, body) { function (error, res, body) {
timer.done() timer.done()

View file

@ -97,6 +97,8 @@ module.exports = HistoryController = {
}, },
resyncProjectHistory(req, res, next) { resyncProjectHistory(req, res, next) {
// increase timeout to 6 minutes
res.setTimeout(6 * 60 * 1000)
const projectId = req.params.Project_id const projectId = req.params.Project_id
ProjectEntityUpdateHandler.resyncProjectHistory(projectId, function (err) { ProjectEntityUpdateHandler.resyncProjectHistory(projectId, function (err) {
if (err instanceof Errors.ProjectHistoryDisabledError) { if (err instanceof Errors.ProjectHistoryDisabledError) {

View file

@ -99,6 +99,7 @@ async function resyncProject(projectId, options = {}) {
await request.post({ await request.post({
url: `${settings.apis.project_history.url}/project/${projectId}/resync`, url: `${settings.apis.project_history.url}/project/${projectId}/resync`,
json: body, json: body,
timeout: 6 * 60 * 1000,
}) })
} catch (err) { } catch (err) {
throw OError.tag(err, 'failed to resync project history', { projectId }) throw OError.tag(err, 'failed to resync project history', { projectId })

View file

@ -30,7 +30,7 @@ const VALID_ROOT_DOC_REGEXP = new RegExp(
'i' 'i'
) )
function wrapWithLock(methodWithoutLock) { function wrapWithLock(methodWithoutLock, lockManager = LockManager) {
// This lock is used to make sure that the project structure updates are made // This lock is used to make sure that the project structure updates are made
// sequentially. In particular the updates must be made in mongo and sent to // sequentially. In particular the updates must be made in mongo and sent to
// the doc-updater in the same order. // the doc-updater in the same order.
@ -39,7 +39,7 @@ function wrapWithLock(methodWithoutLock) {
const adjustedLength = Math.max(rest.length, 1) const adjustedLength = Math.max(rest.length, 1)
const args = rest.slice(0, adjustedLength - 1) const args = rest.slice(0, adjustedLength - 1)
const callback = rest[adjustedLength - 1] const callback = rest[adjustedLength - 1]
LockManager.runWithLock( lockManager.runWithLock(
LOCK_NAMESPACE, LOCK_NAMESPACE,
projectId, projectId,
cb => methodWithoutLock(projectId, ...args, cb), cb => methodWithoutLock(projectId, ...args, cb),
@ -56,7 +56,7 @@ function wrapWithLock(methodWithoutLock) {
const adjustedLength = Math.max(rest.length, 1) const adjustedLength = Math.max(rest.length, 1)
const args = rest.slice(0, adjustedLength - 1) const args = rest.slice(0, adjustedLength - 1)
const callback = rest[adjustedLength - 1] const callback = rest[adjustedLength - 1]
LockManager.runWithLock( lockManager.runWithLock(
LOCK_NAMESPACE, LOCK_NAMESPACE,
projectId, projectId,
cb => mainTask(projectId, ...args, cb), cb => mainTask(projectId, ...args, cb),
@ -1331,7 +1331,8 @@ const ProjectEntityUpdateHandler = {
// This doesn't directly update project structure but we need to take the lock // This doesn't directly update project structure but we need to take the lock
// to prevent anything else being queued before the resync update // to prevent anything else being queued before the resync update
resyncProjectHistory: wrapWithLock((projectId, callback) => resyncProjectHistory: wrapWithLock(
(projectId, callback) =>
ProjectGetter.getProject( ProjectGetter.getProject(
projectId, projectId,
{ rootFolder: true, overleaf: true }, { rootFolder: true, overleaf: true },
@ -1386,7 +1387,8 @@ const ProjectEntityUpdateHandler = {
} }
) )
} }
) ),
LockManager.withTimeout(6 * 60) // use an extended lock for the resync operations
), ),
_checkFiletree(projectId, projectHistoryId, entities, callback) { _checkFiletree(projectId, projectHistoryId, entities, callback) {

View file

@ -5,13 +5,34 @@ const { callbackify, promisify } = require('util')
const RedisWebLocker = require('@overleaf/redis-wrapper/RedisWebLocker') const RedisWebLocker = require('@overleaf/redis-wrapper/RedisWebLocker')
const LockManager = new RedisWebLocker({ // this method creates a lock manager with the provided timeout options
function createLockManager(options) {
return new RedisWebLocker({
rclient, rclient,
getKey(namespace, id) { getKey(namespace, id) {
return `lock:web:${namespace}:${id}` return `lock:web:${namespace}:${id}`
}, },
options: settings.lockManager, options,
}) })
}
// this is the default lock manager for web
const LockManager = createLockManager(settings.lockManager)
// this method creates a lock manager with a configurable timeout
// it shares the lock keys with the default lock manager
LockManager.withTimeout = function (timeout) {
const overrides = {
redisLockExpiry: timeout, // in seconds
slowExecutionThreshold: 0.5 * timeout * 1000, // in ms
}
const lockManagerSettingsWithTimeout = Object.assign(
{},
settings.lockManager,
overrides
)
return createLockManager(lockManagerSettingsWithTimeout)
}
// need to bind the promisified function when it is part of a class // need to bind the promisified function when it is part of a class
// see https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilpromisifyoriginal // see https://nodejs.org/dist/latest-v16.x/docs/api/util.html#utilpromisifyoriginal

View file

@ -350,6 +350,7 @@ describe('DocumentUpdaterHandler', function () {
user_id: this.user_id, user_id: this.user_id,
}, },
method: 'POST', method: 'POST',
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
}) })
@ -439,6 +440,7 @@ describe('DocumentUpdaterHandler', function () {
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`, url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`,
method: 'GET', method: 'GET',
json: true, json: true,
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
}) })
@ -658,6 +660,7 @@ describe('DocumentUpdaterHandler', function () {
change_ids: [this.change_id], change_ids: [this.change_id],
}, },
method: 'POST', method: 'POST',
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
}) })
@ -881,6 +884,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version, version: this.version,
projectHistoryId: this.projectHistoryId, projectHistoryId: this.projectHistoryId,
}, },
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
done() done()
@ -926,6 +930,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version, version: this.version,
projectHistoryId: this.projectHistoryId, projectHistoryId: this.projectHistoryId,
}, },
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
done() done()
@ -975,6 +980,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version, version: this.version,
projectHistoryId: this.projectHistoryId, projectHistoryId: this.projectHistoryId,
}, },
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
done() done()
@ -1018,6 +1024,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version, version: this.version,
projectHistoryId: this.projectHistoryId, projectHistoryId: this.projectHistoryId,
}, },
timeout: 30 * 1000,
}) })
.should.equal(true) .should.equal(true)
done() done()
@ -1080,6 +1087,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version, version: this.version,
projectHistoryId: this.projectHistoryId, projectHistoryId: this.projectHistoryId,
}, },
timeout: 30 * 1000,
}) })
done() done()
} }

View file

@ -311,7 +311,7 @@ describe('HistoryController', function () {
beforeEach(function () { beforeEach(function () {
this.project_id = 'mock-project-id' this.project_id = 'mock-project-id'
this.req = { params: { Project_id: this.project_id } } this.req = { params: { Project_id: this.project_id } }
this.res = { sendStatus: sinon.stub() } this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
this.next = sinon.stub() this.next = sinon.stub()
this.error = new Errors.ProjectHistoryDisabledError() this.error = new Errors.ProjectHistoryDisabledError()
@ -335,7 +335,7 @@ describe('HistoryController', function () {
beforeEach(function () { beforeEach(function () {
this.project_id = 'mock-project-id' this.project_id = 'mock-project-id'
this.req = { params: { Project_id: this.project_id } } this.req = { params: { Project_id: this.project_id } }
this.res = { sendStatus: sinon.stub() } this.res = { setTimeout: sinon.stub(), sendStatus: sinon.stub() }
this.next = sinon.stub() this.next = sinon.stub()
this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon
@ -349,6 +349,10 @@ describe('HistoryController', function () {
) )
}) })
it('sets an extended response timeout', function () {
this.res.setTimeout.should.have.been.calledWith(6 * 60 * 1000)
})
it('resyncs the project', function () { it('resyncs the project', function () {
return this.ProjectEntityUpdateHandler.resyncProjectHistory return this.ProjectEntityUpdateHandler.resyncProjectHistory
.calledWith(this.project_id) .calledWith(this.project_id)

View file

@ -88,6 +88,7 @@ describe('ProjectEntityUpdateHandler', function () {
runWithLock: sinon.spy((namespace, id, runner, callback) => runWithLock: sinon.spy((namespace, id, runner, callback) =>
runner(callback) runner(callback)
), ),
withTimeout: sinon.stub().returns(this.LockManager),
} }
this.ProjectModel = { this.ProjectModel = {
updateOne: sinon.stub(), updateOne: sinon.stub(),
@ -1954,6 +1955,10 @@ describe('ProjectEntityUpdateHandler', function () {
.should.equal(true) .should.equal(true)
}) })
it('uses an extended timeout', function () {
this.LockManager.withTimeout.calledWith(6 * 60).should.equal(true)
})
it('tells the doc updater to sync the project', function () { it('tells the doc updater to sync the project', function () {
const docs = [ const docs = [
{ {