Browse Source

Merge pull request #1040 from sharelatex/bg-start-history-in-v2-for-old-projects

allow creating v2 history for existing project without v2 history

GitOrigin-RevId: 2ccbe55f5c203c37d37be00306de5e265555f909
Brian Gough 7 years ago
parent
commit
707a258d5b

+ 11 - 0
services/web/app/coffee/Features/History/HistoryManager.coffee

@@ -27,6 +27,17 @@ module.exports = HistoryManager =
 				error = new Error("project-history returned a non-success status code: #{res.statusCode}")
 				callback error
 
+	flushProject: (project_id, callback = (error) ->) ->
+		request.post {
+			url: "#{settings.apis.project_history.url}/project/#{project_id}/flush"
+		}, (error, res, body)->
+			return callback(error) if error?
+			if res.statusCode >= 200 and res.statusCode < 300
+				callback()
+			else
+				error = new Error("project-history returned a non-success status code: #{res.statusCode}")
+				callback error
+
 	injectUserDetails: (data, callback = (error, data_with_users) ->) ->
 		# data can be either:
 		# {

+ 42 - 0
services/web/app/coffee/Features/Project/ProjectHistoryHandler.coffee

@@ -0,0 +1,42 @@
+Project = require('../../models/Project').Project
+ProjectDetailsHandler = require "./ProjectDetailsHandler"
+logger = require('logger-sharelatex')
+settings = require("settings-sharelatex")
+HistoryManager = require "../History/HistoryManager"
+ProjectEntityUpdateHandler = require "./ProjectEntityUpdateHandler"
+
+module.exports = ProjectHistoryHandler =
+
+	setHistoryId: (project_id, history_id, callback = (err) ->) ->
+		# reject invalid history ids
+		return callback(new Error("invalid history id")) if !history_id or typeof(history_id) isnt 'number'
+		# use $exists:false to prevent overwriting any existing history id, atomically
+		Project.update {_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":history_id}, (err, result)->
+			return callback(err) if err?
+			return callback(new Error("history exists")) if result?.n == 0
+			callback()
+
+	getHistoryId: (project_id, callback = (err, result) ->) ->
+		ProjectDetailsHandler.getDetails project_id, (err, project) ->
+			return callback(err) if err? # n.b. getDetails returns an error if the project doesn't exist
+			return callback(new Error("history exists")) if project.overleaf?.history?.id?
+			callback(null, project?.overleaf?.history?.id)
+
+	ensureHistoryExistsForProject: (project_id, callback = (err) ->) ->
+		# We can only set a history id for a project that doesn't have one. The
+		# history id is cached in the project history service, and changing an
+		# existing value corrupts the history, leaving it in an irrecoverable
+		# state. Setting a history id when one wasn't present before is ok,
+		# because undefined history ids aren't cached.
+		ProjectHistoryHandler.getHistoryId project_id, (err, history_id) ->
+			return callback(err) if err?
+			return callback() if history_id? # history already exists, success
+			HistoryManager.initializeProject (err, history) ->
+				return callback(err) if err?
+				return callback(new Error("failed to initialize history id")) if !history?.overleaf_id
+				ProjectHistoryHandler.setHistoryId project_id, history.overleaf_id, (err) ->
+					return callback(err) if err?
+					ProjectEntityUpdateHandler.resyncProjectHistory project_id, (err) ->
+						return callback(err) if err?
+						logger.log {project_id: project_id, history_id: history.overleaf_id}, "started syncing project with new history id"
+						HistoryManager.flushProject project_id, callback

+ 105 - 0
services/web/test/unit/coffee/Project/ProjectHistoryHandlerTests.coffee

@@ -0,0 +1,105 @@
+chai = require('chai')
+assert = require('chai').assert
+should = chai.should()
+expect = chai.expect
+sinon = require 'sinon'
+modulePath = "../../../../app/js/Features/Project/ProjectHistoryHandler"
+SandboxedModule = require('sandboxed-module')
+ObjectId = require("mongoose").Types.ObjectId
+
+describe 'ProjectHistoryHandler', ->
+	project_id = '4eecb1c1bffa66588e0000a1'
+	userId = 1234
+
+	beforeEach ->
+		@ProjectModel = class Project
+			constructor:(options)->
+				@._id = project_id
+				@name = "project_name_here"
+				@rev = 0
+			rootFolder:[@rootFolder]
+		@project = new @ProjectModel()
+
+		@callback = sinon.stub()
+
+		@ProjectHistoryHandler = SandboxedModule.require modulePath, requires:
+			'logger-sharelatex': @logger = {log:sinon.stub(), error: sinon.stub(), err:->}
+			'settings-sharelatex': @Settings = {}
+			'../../models/Project': Project:@ProjectModel
+			'./ProjectDetailsHandler': @ProjectDetailsHandler = {}
+			'../History/HistoryManager': @HistoryManager = {}
+			'./ProjectEntityUpdateHandler': @ProjectEntityUpdateHandler = {}
+
+	describe "starting history for an existing project", ->
+		beforeEach ->
+			@newHistoryId = 123456789
+			@HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, {overleaf_id: @newHistoryId})
+			@HistoryManager.flushProject = sinon.stub().callsArg(1)
+			@ProjectEntityUpdateHandler.resyncProjectHistory = sinon.stub().callsArg(1)
+
+		describe "when the history does not already exist", ->
+			beforeEach ->
+				@ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project)
+				@ProjectModel.update = sinon.stub().callsArgWith(2,null,{n:1})
+				@ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback
+
+			it "should get any existing history id for the project", ->
+				@ProjectDetailsHandler.getDetails
+					.calledWith(project_id)
+					.should.equal true
+
+			it "should initialize a new history in the v1 history service", ->
+				@HistoryManager.initializeProject
+					.called.should.equal.true
+			
+			it "should set the new history id on the project", ->
+				@ProjectModel.update
+					.calledWith({_id: project_id, "overleaf.history.id": {$exists:false}}, {"overleaf.history.id":@newHistoryId})
+					.should.equal true
+
+			it "should resync the project history", ->
+				@ProjectEntityUpdateHandler.resyncProjectHistory
+					.calledWith(project_id)
+					.should.equal true
+
+			it "should flush the project history", ->
+				@HistoryManager.flushProject
+					.calledWith(project_id)
+					.should.equal true
+			
+			it "should call the callback without an error", ->
+				@callback.called.should.equal true
+
+		describe "when the history already exists", ->
+			beforeEach ->
+				@project.overleaf = {history: {id: 1234}}
+				@ProjectDetailsHandler.getDetails = sinon.stub().withArgs(project_id).callsArgWith(1, null, @project)
+				@ProjectModel.update = sinon.stub()
+				@ProjectHistoryHandler.ensureHistoryExistsForProject project_id, @callback
+
+			it "should get any existing history id for the project", ->
+				@ProjectDetailsHandler.getDetails
+					.calledWith(project_id)
+					.should.equal true
+
+			it "should not initialize a new history in the v1 history service", ->
+				@HistoryManager.initializeProject
+					.called.should.equal false
+			
+			it "should not set the new history id on the project", ->
+				@ProjectModel.update
+					.called
+					.should.equal false
+
+			it "should not resync the project history", ->
+				@ProjectEntityUpdateHandler.resyncProjectHistory
+					.called
+					.should.equal false
+
+			it "should not flush the project history", ->
+				@HistoryManager.flushProject
+					.called
+					.should.equal false
+			
+			it "should call the callback", ->
+				@callback.calledWith().should.equal true