Initialise full project history for old projects when project opened (#4687)

* Initialise full project history for old projects when project opened

This begins a second attempt at initialising the full project history in
the background for projects without a full project history id.

The original web-internal#4345 was reverted in web-internal#4353. This
commit reverts the revert, and adds an additional flush of the project
before initialising full project history.

GitOrigin-RevId: ac263dca8cf0d80186fee916a76e5572ec5649d4
This commit is contained in:
Thomas 2021-08-12 15:07:34 +01:00 committed by Copybot
parent 3ace29999b
commit 7517a818b2
6 changed files with 104 additions and 30 deletions

View file

@ -8,12 +8,14 @@ module.exports = {
initializeProject: callbackify(initializeProject), initializeProject: callbackify(initializeProject),
flushProject: callbackify(flushProject), flushProject: callbackify(flushProject),
resyncProject: callbackify(resyncProject), resyncProject: callbackify(resyncProject),
forceResyncProject: callbackify(forceResyncProject),
deleteProject: callbackify(deleteProject), deleteProject: callbackify(deleteProject),
injectUserDetails: callbackify(injectUserDetails), injectUserDetails: callbackify(injectUserDetails),
promises: { promises: {
initializeProject, initializeProject,
flushProject, flushProject,
resyncProject, resyncProject,
forceResyncProject,
deleteProject, deleteProject,
injectUserDetails, injectUserDetails,
}, },
@ -65,6 +67,18 @@ async function resyncProject(projectId) {
} }
} }
async function forceResyncProject(projectId) {
try {
await request.post({
url: `${settings.apis.project_history.url}/project/${projectId}/resync?force=true`,
})
} catch (err) {
throw OError.tag(err, 'failed to force resync project history', {
projectId,
})
}
}
async function deleteProject(projectId, historyId) { async function deleteProject(projectId, historyId) {
try { try {
const tasks = [ const tasks = [

View file

@ -9,6 +9,7 @@ const { ObjectId } = require('mongodb')
const ProjectDeleter = require('./ProjectDeleter') const ProjectDeleter = require('./ProjectDeleter')
const ProjectDuplicator = require('./ProjectDuplicator') const ProjectDuplicator = require('./ProjectDuplicator')
const ProjectCreationHandler = require('./ProjectCreationHandler') const ProjectCreationHandler = require('./ProjectCreationHandler')
const ProjectHistoryHandler = require('./ProjectHistoryHandler')
const EditorController = require('../Editor/EditorController') const EditorController = require('../Editor/EditorController')
const ProjectHelper = require('./ProjectHelper') const ProjectHelper = require('./ProjectHelper')
const metrics = require('@overleaf/metrics') const metrics = require('@overleaf/metrics')
@ -680,6 +681,24 @@ const ProjectController = {
activate(cb) { activate(cb) {
InactiveProjectManager.reactivateProjectIfRequired(projectId, cb) InactiveProjectManager.reactivateProjectIfRequired(projectId, cb)
}, },
ensureHistoryExists(cb) {
// enable full project history in background for older projects
if (!Settings.apis.project_history || !Features.hasFeature('saas')) {
return cb()
}
ProjectHistoryHandler.ensureHistoryExistsForProject(
projectId,
err => {
if (err) {
logger.error(
{ err, projectId },
'error ensuring history exists for project'
)
}
cb()
}
)
},
markAsOpened(cb) { markAsOpened(cb) {
// don't need to wait for this to complete // don't need to wait for this to complete
ProjectUpdateHandler.markAsOpened(projectId, () => {}) ProjectUpdateHandler.markAsOpened(projectId, () => {})

View file

@ -19,6 +19,7 @@ const logger = require('logger-sharelatex')
const settings = require('@overleaf/settings') const settings = require('@overleaf/settings')
const HistoryManager = require('../History/HistoryManager') const HistoryManager = require('../History/HistoryManager')
const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const { promisifyAll } = require('../../util/promises') const { promisifyAll } = require('../../util/promises')
const ProjectHistoryHandler = { const ProjectHistoryHandler = {
@ -134,31 +135,44 @@ const ProjectHistoryHandler = {
if (history_id != null) { if (history_id != null) {
return callback() return callback()
} // history already exists, success } // history already exists, success
return HistoryManager.initializeProject(function (err, history) { return HistoryManager.flushProject(project_id, function (err) {
if (err != null) { if (err != null) {
return callback(err) return callback(err)
} }
if (!(history != null ? history.overleaf_id : undefined)) { return HistoryManager.initializeProject(function (err, history) {
return callback(new Error('failed to initialize history id')) if (err != null) {
} return callback(err)
return ProjectHistoryHandler.setHistoryId(
project_id,
history.overleaf_id,
function (err) {
if (err != null) {
return callback(err)
}
return ProjectEntityUpdateHandler.resyncProjectHistory(
project_id,
function (err) {
if (err != null) {
return callback(err)
}
return HistoryManager.flushProject(project_id, callback)
}
)
} }
) if (!history || !history.overleaf_id) {
return callback(new Error('failed to initialize history id'))
}
return ProjectHistoryHandler.setHistoryId(
project_id,
history.overleaf_id,
function (err) {
if (err != null) {
return callback(err)
}
return DocumentUpdaterHandler.flushProjectToMongoAndDelete(
project_id,
function (err) {
if (err != null) {
return callback(err)
}
return HistoryManager.forceResyncProject(
project_id,
function (err) {
if (err != null) {
return callback(err)
}
return HistoryManager.flushProject(project_id, callback)
}
)
}
)
}
)
})
}) })
} }
) )

View file

@ -128,6 +128,10 @@ class MockProjectHistoryApi extends AbstractMockApi {
this.app.post('/project/:projectId/flush', (req, res) => { this.app.post('/project/:projectId/flush', (req, res) => {
res.sendStatus(200) res.sendStatus(200)
}) })
this.app.post('/project/:projectId/resync', (req, res) => {
res.sendStatus(204)
})
} }
} }

View file

@ -54,6 +54,9 @@ describe('ProjectController', function () {
.stub() .stub()
.callsArgWith(2, null, { _id: this.project_id }), .callsArgWith(2, null, { _id: this.project_id }),
} }
this.ProjectHistoryHandler = {
ensureHistoryExistsForProject: sinon.stub().callsArg(1),
}
this.SubscriptionLocator = { getUsersSubscription: sinon.stub() } this.SubscriptionLocator = { getUsersSubscription: sinon.stub() }
this.LimitationsManager = { hasPaidSubscription: sinon.stub() } this.LimitationsManager = { hasPaidSubscription: sinon.stub() }
this.TagsHandler = { getAllTags: sinon.stub() } this.TagsHandler = { getAllTags: sinon.stub() }
@ -140,6 +143,7 @@ describe('ProjectController', function () {
'./ProjectDeleter': this.ProjectDeleter, './ProjectDeleter': this.ProjectDeleter,
'./ProjectDuplicator': this.ProjectDuplicator, './ProjectDuplicator': this.ProjectDuplicator,
'./ProjectCreationHandler': this.ProjectCreationHandler, './ProjectCreationHandler': this.ProjectCreationHandler,
'./ProjectHistoryHandler': this.ProjectHistoryHandler,
'../Editor/EditorController': this.EditorController, '../Editor/EditorController': this.EditorController,
'../User/UserController': this.UserController, '../User/UserController': this.UserController,
'./ProjectHelper': this.ProjectHelper, './ProjectHelper': this.ProjectHelper,
@ -1024,6 +1028,18 @@ describe('ProjectController', function () {
this.ProjectController.loadEditor(this.req, this.res) this.ProjectController.loadEditor(this.req, this.res)
}) })
it('should ensureHistoryExistsForProject if saas and project_history enabled', function (done) {
this.Features.hasFeature.withArgs('saas').returns(true)
this.settings.apis.project_history = 'enabled'
this.res.render = (pageName, opts) => {
this.ProjectHistoryHandler.ensureHistoryExistsForProject
.calledWith(this.project_id)
.should.equal(true)
done()
}
this.ProjectController.loadEditor(this.req, this.res)
})
it('should mark project as opened', function (done) { it('should mark project as opened', function (done) {
this.res.render = (pageName, opts) => { this.res.render = (pageName, opts) => {
this.ProjectUpdateHandler.markAsOpened this.ProjectUpdateHandler.markAsOpened

View file

@ -51,6 +51,7 @@ describe('ProjectHistoryHandler', function () {
'./ProjectDetailsHandler': (this.ProjectDetailsHandler = {}), './ProjectDetailsHandler': (this.ProjectDetailsHandler = {}),
'../History/HistoryManager': (this.HistoryManager = {}), '../History/HistoryManager': (this.HistoryManager = {}),
'./ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}), './ProjectEntityUpdateHandler': (this.ProjectEntityUpdateHandler = {}),
'../DocumentUpdater/DocumentUpdaterHandler': (this.DocumentUpdaterHandler = {}),
}, },
})) }))
}) })
@ -62,9 +63,10 @@ describe('ProjectHistoryHandler', function () {
.stub() .stub()
.callsArgWith(0, null, { overleaf_id: this.newHistoryId }) .callsArgWith(0, null, { overleaf_id: this.newHistoryId })
this.HistoryManager.flushProject = sinon.stub().callsArg(1) this.HistoryManager.flushProject = sinon.stub().callsArg(1)
return (this.ProjectEntityUpdateHandler.resyncProjectHistory = sinon this.HistoryManager.forceResyncProject = sinon.stub().callsArg(1)
this.DocumentUpdaterHandler.flushProjectToMongoAndDelete = sinon
.stub() .stub()
.callsArg(1)) .callsArg(1)
}) })
describe('when the history does not already exist', function () { describe('when the history does not already exist', function () {
@ -101,14 +103,21 @@ describe('ProjectHistoryHandler', function () {
.should.equal(true) .should.equal(true)
}) })
it('should resync the project history', function () { it('should trigger a hard resync of the project history', function () {
return this.ProjectEntityUpdateHandler.resyncProjectHistory return this.HistoryManager.forceResyncProject
.calledWith(project_id) .calledWith(project_id)
.should.equal(true) .should.equal(true)
}) })
it('should flush the project history', function () { it('should flush the project history (twice)', function () {
this.HistoryManager.flushProject.calledTwice.should.equal(true)
return this.HistoryManager.flushProject return this.HistoryManager.flushProject
.alwaysCalledWith(project_id)
.should.equal(true)
})
it('should tell docupdater to flush and delete', function () {
return this.DocumentUpdaterHandler.flushProjectToMongoAndDelete
.calledWith(project_id) .calledWith(project_id)
.should.equal(true) .should.equal(true)
}) })
@ -146,10 +155,8 @@ describe('ProjectHistoryHandler', function () {
return this.ProjectModel.updateOne.called.should.equal(false) return this.ProjectModel.updateOne.called.should.equal(false)
}) })
it('should not resync the project history', function () { it('should not trigger a hard resync of the project history', function () {
return this.ProjectEntityUpdateHandler.resyncProjectHistory.called.should.equal( return this.HistoryManager.forceResyncProject.called.should.equal(false)
false
)
}) })
it('should not flush the project history', function () { it('should not flush the project history', function () {