mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
57114c4503
commit
2970a09d19
9 changed files with 118 additions and 67 deletions
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue