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/history/resync',
longerTimeout,
HttpController.resyncProjectHistory
)
app.post('/project/:project_id/flush', HttpController.flushProject)
@ -258,3 +259,8 @@ for (const signal of [
]) {
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`,
json: { docs, files, projectHistoryId },
method: 'POST',
timeout: 6 * 60 * 1000, // allow 6 minutes for resync
},
projectId,
'resync-project-history',
@ -299,6 +300,7 @@ function _makeRequest(options, projectId, metricsKey, callback) {
url: `${settings.apis.documentupdater.url}${options.path}`,
json: options.json,
method: options.method || 'GET',
timeout: options.timeout || 30 * 1000,
},
function (error, res, body) {
timer.done()

View file

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

View file

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

View file

@ -30,7 +30,7 @@ const VALID_ROOT_DOC_REGEXP = new RegExp(
'i'
)
function wrapWithLock(methodWithoutLock) {
function wrapWithLock(methodWithoutLock, lockManager = LockManager) {
// 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
// the doc-updater in the same order.
@ -39,7 +39,7 @@ function wrapWithLock(methodWithoutLock) {
const adjustedLength = Math.max(rest.length, 1)
const args = rest.slice(0, adjustedLength - 1)
const callback = rest[adjustedLength - 1]
LockManager.runWithLock(
lockManager.runWithLock(
LOCK_NAMESPACE,
projectId,
cb => methodWithoutLock(projectId, ...args, cb),
@ -56,7 +56,7 @@ function wrapWithLock(methodWithoutLock) {
const adjustedLength = Math.max(rest.length, 1)
const args = rest.slice(0, adjustedLength - 1)
const callback = rest[adjustedLength - 1]
LockManager.runWithLock(
lockManager.runWithLock(
LOCK_NAMESPACE,
projectId,
cb => mainTask(projectId, ...args, cb),
@ -1331,62 +1331,64 @@ const ProjectEntityUpdateHandler = {
// This doesn't directly update project structure but we need to take the lock
// to prevent anything else being queued before the resync update
resyncProjectHistory: wrapWithLock((projectId, callback) =>
ProjectGetter.getProject(
projectId,
{ rootFolder: true, overleaf: true },
(error, project) => {
if (error != null) {
return callback(error)
}
const projectHistoryId =
project &&
project.overleaf &&
project.overleaf.history &&
project.overleaf.history.id
if (projectHistoryId == null) {
error = new Errors.ProjectHistoryDisabledError(
`project history not enabled for ${projectId}`
)
return callback(error)
}
let { docs, files, folders } =
ProjectEntityHandler.getAllEntitiesFromProject(project)
// _checkFileTree() must be passed the folders before docs and
// files
ProjectEntityUpdateHandler._checkFiletree(
projectId,
projectHistoryId,
[...folders, ...docs, ...files],
error => {
if (error) {
return callback(error)
}
docs = _.map(docs, doc => ({
doc: doc.doc._id,
path: doc.path,
}))
files = _.map(files, file => ({
file: file.file._id,
path: file.path,
url: FileStoreHandler._buildUrl(projectId, file.file._id),
_hash: file.file.hash,
}))
DocumentUpdaterHandler.resyncProjectHistory(
projectId,
projectHistoryId,
docs,
files,
callback
)
resyncProjectHistory: wrapWithLock(
(projectId, callback) =>
ProjectGetter.getProject(
projectId,
{ rootFolder: true, overleaf: true },
(error, project) => {
if (error != null) {
return callback(error)
}
)
}
)
const projectHistoryId =
project &&
project.overleaf &&
project.overleaf.history &&
project.overleaf.history.id
if (projectHistoryId == null) {
error = new Errors.ProjectHistoryDisabledError(
`project history not enabled for ${projectId}`
)
return callback(error)
}
let { docs, files, folders } =
ProjectEntityHandler.getAllEntitiesFromProject(project)
// _checkFileTree() must be passed the folders before docs and
// files
ProjectEntityUpdateHandler._checkFiletree(
projectId,
projectHistoryId,
[...folders, ...docs, ...files],
error => {
if (error) {
return callback(error)
}
docs = _.map(docs, doc => ({
doc: doc.doc._id,
path: doc.path,
}))
files = _.map(files, file => ({
file: file.file._id,
path: file.path,
url: FileStoreHandler._buildUrl(projectId, file.file._id),
_hash: file.file.hash,
}))
DocumentUpdaterHandler.resyncProjectHistory(
projectId,
projectHistoryId,
docs,
files,
callback
)
}
)
}
),
LockManager.withTimeout(6 * 60) // use an extended lock for the resync operations
),
_checkFiletree(projectId, projectHistoryId, entities, callback) {

View file

@ -5,13 +5,34 @@ const { callbackify, promisify } = require('util')
const RedisWebLocker = require('@overleaf/redis-wrapper/RedisWebLocker')
const LockManager = new RedisWebLocker({
rclient,
getKey(namespace, id) {
return `lock:web:${namespace}:${id}`
},
options: settings.lockManager,
})
// this method creates a lock manager with the provided timeout options
function createLockManager(options) {
return new RedisWebLocker({
rclient,
getKey(namespace, id) {
return `lock:web:${namespace}:${id}`
},
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
// 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,
},
method: 'POST',
timeout: 30 * 1000,
})
.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}`,
method: 'GET',
json: true,
timeout: 30 * 1000,
})
.should.equal(true)
})
@ -658,6 +660,7 @@ describe('DocumentUpdaterHandler', function () {
change_ids: [this.change_id],
},
method: 'POST',
timeout: 30 * 1000,
})
.should.equal(true)
})
@ -881,6 +884,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version,
projectHistoryId: this.projectHistoryId,
},
timeout: 30 * 1000,
})
.should.equal(true)
done()
@ -926,6 +930,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version,
projectHistoryId: this.projectHistoryId,
},
timeout: 30 * 1000,
})
.should.equal(true)
done()
@ -975,6 +980,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version,
projectHistoryId: this.projectHistoryId,
},
timeout: 30 * 1000,
})
.should.equal(true)
done()
@ -1018,6 +1024,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version,
projectHistoryId: this.projectHistoryId,
},
timeout: 30 * 1000,
})
.should.equal(true)
done()
@ -1080,6 +1087,7 @@ describe('DocumentUpdaterHandler', function () {
version: this.version,
projectHistoryId: this.projectHistoryId,
},
timeout: 30 * 1000,
})
done()
}

View file

@ -311,7 +311,7 @@ describe('HistoryController', function () {
beforeEach(function () {
this.project_id = 'mock-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.error = new Errors.ProjectHistoryDisabledError()
@ -335,7 +335,7 @@ describe('HistoryController', function () {
beforeEach(function () {
this.project_id = 'mock-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.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 () {
return this.ProjectEntityUpdateHandler.resyncProjectHistory
.calledWith(this.project_id)

View file

@ -88,6 +88,7 @@ describe('ProjectEntityUpdateHandler', function () {
runWithLock: sinon.spy((namespace, id, runner, callback) =>
runner(callback)
),
withTimeout: sinon.stub().returns(this.LockManager),
}
this.ProjectModel = {
updateOne: sinon.stub(),
@ -1954,6 +1955,10 @@ describe('ProjectEntityUpdateHandler', function () {
.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 () {
const docs = [
{