Persist Last Author id on Document Flush (#50)

Persist Last Author id on Document Flush
This commit is contained in:
Timothée Alby 2019-05-02 15:09:58 +01:00 committed by GitHub
commit 70c505830b
11 changed files with 115 additions and 36 deletions

View file

@ -102,14 +102,14 @@ module.exports = DocumentManager =
callback = (args...) -> callback = (args...) ->
timer.done() timer.done()
_callback(args...) _callback(args...)
RedisManager.getDoc project_id, doc_id, (error, lines, version, ranges) -> RedisManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname, projectHistoryId, unflushedTime, lastUpdatedAt, lastUpdatedBy) ->
return callback(error) if error? return callback(error) if error?
if !lines? or !version? if !lines? or !version?
logger.log project_id: project_id, doc_id: doc_id, "doc is not loaded so not flushing" logger.log project_id: project_id, doc_id: doc_id, "doc is not loaded so not flushing"
callback null # TODO: return a flag to bail out, as we go on to remove doc from memory? callback null # TODO: return a flag to bail out, as we go on to remove doc from memory?
else else
logger.log project_id: project_id, doc_id: doc_id, version: version, "flushing doc" logger.log project_id: project_id, doc_id: doc_id, version: version, "flushing doc"
PersistenceManager.setDoc project_id, doc_id, lines, version, ranges, (error) -> PersistenceManager.setDoc project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, (error) ->
return callback(error) if error? return callback(error) if error?
RedisManager.clearUnflushedTime doc_id, callback RedisManager.clearUnflushedTime doc_id, callback
@ -141,7 +141,7 @@ module.exports = DocumentManager =
return callback(new Errors.NotFoundError("document not found: #{doc_id}")) return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
RangesManager.acceptChanges change_ids, ranges, (error, new_ranges) -> RangesManager.acceptChanges change_ids, ranges, (error, new_ranges) ->
return callback(error) if error? return callback(error) if error?
RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, (error) -> RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, {}, (error) ->
return callback(error) if error? return callback(error) if error?
callback() callback()
@ -157,7 +157,7 @@ module.exports = DocumentManager =
return callback(new Errors.NotFoundError("document not found: #{doc_id}")) return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
RangesManager.deleteComment comment_id, ranges, (error, new_ranges) -> RangesManager.deleteComment comment_id, ranges, (error, new_ranges) ->
return callback(error) if error? return callback(error) if error?
RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, (error) -> RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, {}, (error) ->
return callback(error) if error? return callback(error) if error?
callback() callback()

View file

@ -50,7 +50,7 @@ module.exports = PersistenceManager =
else else
return callback(new Error("error accessing web API: #{url} #{res.statusCode}")) return callback(new Error("error accessing web API: #{url} #{res.statusCode}"))
setDoc: (project_id, doc_id, lines, version, ranges, _callback = (error) ->) -> setDoc: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy,_callback = (error) ->) ->
timer = new Metrics.Timer("persistenceManager.setDoc") timer = new Metrics.Timer("persistenceManager.setDoc")
callback = (args...) -> callback = (args...) ->
timer.done() timer.done()
@ -64,6 +64,8 @@ module.exports = PersistenceManager =
lines: lines lines: lines
ranges: ranges ranges: ranges
version: version version: version
lastUpdatedBy: lastUpdatedBy
lastUpdatedAt: lastUpdatedAt
auth: auth:
user: Settings.apis.web.user user: Settings.apis.web.user
pass: Settings.apis.web.pass pass: Settings.apis.web.pass

View file

@ -90,6 +90,8 @@ module.exports = RedisManager =
multi.del keys.pathname(doc_id:doc_id) multi.del keys.pathname(doc_id:doc_id)
multi.del keys.projectHistoryId(doc_id:doc_id) multi.del keys.projectHistoryId(doc_id:doc_id)
multi.del keys.unflushedTime(doc_id:doc_id) multi.del keys.unflushedTime(doc_id:doc_id)
multi.del keys.lastUpdatedAt(doc_id: doc_id)
multi.del keys.lastUpdatedBy(doc_id: doc_id)
multi.exec (error) -> multi.exec (error) ->
return callback(error) if error? return callback(error) if error?
multi = rclient.multi() multi = rclient.multi()
@ -120,7 +122,9 @@ module.exports = RedisManager =
multi.get keys.pathname(doc_id:doc_id) multi.get keys.pathname(doc_id:doc_id)
multi.get keys.projectHistoryId(doc_id:doc_id) multi.get keys.projectHistoryId(doc_id:doc_id)
multi.get keys.unflushedTime(doc_id:doc_id) multi.get keys.unflushedTime(doc_id:doc_id)
multi.exec (error, [docLines, version, storedHash, doc_project_id, ranges, pathname, projectHistoryId, unflushedTime])-> multi.get keys.lastUpdatedAt(doc_id: doc_id)
multi.get keys.lastUpdatedBy(doc_id: doc_id)
multi.exec (error, [docLines, version, storedHash, doc_project_id, ranges, pathname, projectHistoryId, unflushedTime, lastUpdatedAt, lastUpdatedBy])->
timeSpan = timer.done() timeSpan = timer.done()
return callback(error) if error? return callback(error) if error?
# check if request took too long and bail out. only do this for # check if request took too long and bail out. only do this for
@ -152,14 +156,14 @@ module.exports = RedisManager =
# doc is not in redis, bail out # doc is not in redis, bail out
if !docLines? if !docLines?
return callback null, docLines, version, ranges, pathname, projectHistoryId, unflushedTime return callback null, docLines, version, ranges, pathname, projectHistoryId, unflushedTime, lastUpdatedAt, lastUpdatedBy
# doc should be in project set, check if missing (workaround for missing docs from putDoc) # doc should be in project set, check if missing (workaround for missing docs from putDoc)
rclient.sadd keys.docsInProject(project_id:project_id), doc_id, (error, result) -> rclient.sadd keys.docsInProject(project_id:project_id), doc_id, (error, result) ->
return callback(error) if error? return callback(error) if error?
if result isnt 0 # doc should already be in set if result isnt 0 # doc should already be in set
logger.error project_id: project_id, doc_id: doc_id, doc_project_id: doc_project_id, "doc missing from docsInProject set" logger.error project_id: project_id, doc_id: doc_id, doc_project_id: doc_project_id, "doc missing from docsInProject set"
callback null, docLines, version, ranges, pathname, projectHistoryId, unflushedTime callback null, docLines, version, ranges, pathname, projectHistoryId, unflushedTime, lastUpdatedAt, lastUpdatedBy
getDocVersion: (doc_id, callback = (error, version) ->) -> getDocVersion: (doc_id, callback = (error, version) ->) ->
rclient.get keys.docVersion(doc_id: doc_id), (error, version) -> rclient.get keys.docVersion(doc_id: doc_id), (error, version) ->
@ -209,7 +213,7 @@ module.exports = RedisManager =
DOC_OPS_TTL: 60 * minutes DOC_OPS_TTL: 60 * minutes
DOC_OPS_MAX_LENGTH: 100 DOC_OPS_MAX_LENGTH: 100
updateDocument : (project_id, doc_id, docLines, newVersion, appliedOps = [], ranges, callback = (error) ->)-> updateDocument : (project_id, doc_id, docLines, newVersion, appliedOps = [], ranges, updateMeta, callback = (error) ->)->
RedisManager.getDocVersion doc_id, (error, currentVersion) -> RedisManager.getDocVersion doc_id, (error, currentVersion) ->
return callback(error) if error? return callback(error) if error?
if currentVersion + appliedOps.length != newVersion if currentVersion + appliedOps.length != newVersion
@ -261,6 +265,11 @@ module.exports = RedisManager =
# hasn't been modified before (the content in mongo has been # hasn't been modified before (the content in mongo has been
# valid up to this point). Otherwise leave it alone ("NX" flag). # valid up to this point). Otherwise leave it alone ("NX" flag).
multi.set keys.unflushedTime(doc_id: doc_id), Date.now(), "NX" multi.set keys.unflushedTime(doc_id: doc_id), Date.now(), "NX"
multi.set keys.lastUpdatedAt(doc_id: doc_id), Date.now() # index 8
if updateMeta?.user_id
multi.set keys.lastUpdatedBy(doc_id: doc_id), updateMeta.user_id # index 9
else
multi.del keys.lastUpdatedBy(doc_id: doc_id) # index 9
multi.exec (error, result) -> multi.exec (error, result) ->
return callback(error) if error? return callback(error) if error?
# check the hash computed on the redis server # check the hash computed on the redis server

View file

@ -85,7 +85,7 @@ module.exports = UpdateManager =
UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, projectHistoryId, lines) UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, projectHistoryId, lines)
profile.log("RangesManager.applyUpdate") profile.log("RangesManager.applyUpdate")
return callback(error) if error? return callback(error) if error?
RedisManager.updateDocument project_id, doc_id, updatedDocLines, version, appliedOps, new_ranges, (error, doc_ops_length, project_ops_length) -> RedisManager.updateDocument project_id, doc_id, updatedDocLines, version, appliedOps, new_ranges, update.meta, (error, doc_ops_length, project_ops_length) ->
profile.log("RedisManager.updateDocument") profile.log("RedisManager.updateDocument")
return callback(error) if error? return callback(error) if error?
HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) -> HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) ->

View file

@ -79,6 +79,8 @@ module.exports =
projectHistoryId: ({doc_id}) -> "ProjectHistoryId:{#{doc_id}}" projectHistoryId: ({doc_id}) -> "ProjectHistoryId:{#{doc_id}}"
projectState: ({project_id}) -> "ProjectState:{#{project_id}}" projectState: ({project_id}) -> "ProjectState:{#{project_id}}"
pendingUpdates: ({doc_id}) -> "PendingUpdates:{#{doc_id}}" pendingUpdates: ({doc_id}) -> "PendingUpdates:{#{doc_id}}"
lastUpdatedBy: ({doc_id}) -> "lastUpdatedBy:{#{doc_id}}"
lastUpdatedAt: ({doc_id}) -> "lastUpdatedAt:{#{doc_id}}"
redisOptions: redisOptions:
keepAlive: 100 keepAlive: 100

View file

@ -14,6 +14,7 @@ describe "Flushing a doc to Mongo", ->
@version = 42 @version = 42
@update = @update =
doc: @doc_id doc: @doc_id
meta: { user_id: 'last-author-fake-id' }
op: [{ op: [{
i: "one and a half\n" i: "one and a half\n"
p: 4 p: 4
@ -42,6 +43,13 @@ describe "Flushing a doc to Mongo", ->
.calledWith(@project_id, @doc_id, @result, @version + 1) .calledWith(@project_id, @doc_id, @result, @version + 1)
.should.equal true .should.equal true
it "should flush the last update author and time to the web api", ->
lastUpdatedAt = MockWebApi.setDocument.lastCall.args[5]
parseInt(lastUpdatedAt).should.be.closeTo((new Date()).getTime(), 30000)
lastUpdatedBy = MockWebApi.setDocument.lastCall.args[6]
lastUpdatedBy.should.equal 'last-author-fake-id'
describe "when the doc does not exist in the doc updater", -> describe "when the doc does not exist in the doc updater", ->
before (done) -> before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()] [@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
@ -65,7 +73,7 @@ describe "Flushing a doc to Mongo", ->
version: @version version: @version
} }
t = 30000 t = 30000
sinon.stub MockWebApi, "setDocument", (project_id, doc_id, lines, version, ranges, callback = (error) ->) -> sinon.stub MockWebApi, "setDocument", (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
setTimeout callback, t setTimeout callback, t
t = 0 t = 0
DocUpdaterClient.preloadDoc @project_id, @doc_id, done DocUpdaterClient.preloadDoc @project_id, @doc_id, done

View file

@ -12,12 +12,14 @@ module.exports = MockWebApi =
doc.pathname = '/a/b/c.tex' doc.pathname = '/a/b/c.tex'
@docs["#{project_id}:#{doc_id}"] = doc @docs["#{project_id}:#{doc_id}"] = doc
setDocument: (project_id, doc_id, lines, version, ranges, callback = (error) ->) -> setDocument: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
doc = @docs["#{project_id}:#{doc_id}"] ||= {} doc = @docs["#{project_id}:#{doc_id}"] ||= {}
doc.lines = lines doc.lines = lines
doc.version = version doc.version = version
doc.ranges = ranges doc.ranges = ranges
doc.pathname = '/a/b/c.tex' doc.pathname = '/a/b/c.tex'
doc.lastUpdatedAt = lastUpdatedAt
doc.lastUpdatedBy = lastUpdatedBy
callback null callback null
getDocument: (project_id, doc_id, callback = (error, doc) ->) -> getDocument: (project_id, doc_id, callback = (error, doc) ->) ->
@ -34,7 +36,7 @@ module.exports = MockWebApi =
res.send 404 res.send 404
app.post "/project/:project_id/doc/:doc_id", express.bodyParser(), (req, res, next) => app.post "/project/:project_id/doc/:doc_id", express.bodyParser(), (req, res, next) =>
MockWebApi.setDocument req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, (error) -> MockWebApi.setDocument req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, req.body.lastUpdatedAt, req.body.lastUpdatedBy, (error) ->
if error? if error?
res.send 500 res.send 500
else else

View file

@ -35,6 +35,8 @@ describe "DocumentManager", ->
@ranges = { comments: "mock", entries: "mock" } @ranges = { comments: "mock", entries: "mock" }
@pathname = '/a/b/c.tex' @pathname = '/a/b/c.tex'
@unflushedTime = Date.now() @unflushedTime = Date.now()
@lastUpdatedAt = Date.now()
@lastUpdatedBy = 'last-author-id'
afterEach -> afterEach ->
tk.reset() tk.reset()
@ -70,7 +72,7 @@ describe "DocumentManager", ->
describe "flushDocIfLoaded", -> describe "flushDocIfLoaded", ->
describe "when the doc is in Redis", -> describe "when the doc is in Redis", ->
beforeEach -> beforeEach ->
@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges) @RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushedTime, @lastUpdatedAt, @lastUpdatedBy)
@RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null) @RedisManager.clearUnflushedTime = sinon.stub().callsArgWith(1, null)
@PersistenceManager.setDoc = sinon.stub().yields() @PersistenceManager.setDoc = sinon.stub().yields()
@DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback @DocumentManager.flushDocIfLoaded @project_id, @doc_id, @callback
@ -82,7 +84,7 @@ describe "DocumentManager", ->
it "should write the doc lines to the persistence layer", -> it "should write the doc lines to the persistence layer", ->
@PersistenceManager.setDoc @PersistenceManager.setDoc
.calledWith(@project_id, @doc_id, @lines, @version, @ranges) .calledWith(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy)
.should.equal true .should.equal true
it "should call the callback without error", -> it "should call the callback without error", ->
@ -324,7 +326,7 @@ describe "DocumentManager", ->
it "should save the updated ranges", -> it "should save the updated ranges", ->
@RedisManager.updateDocument @RedisManager.updateDocument
.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges) .calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges, {})
.should.equal true .should.equal true
it "should call the callback", -> it "should call the callback", ->
@ -378,7 +380,7 @@ describe "DocumentManager", ->
it "should save the updated ranges", -> it "should save the updated ranges", ->
@RedisManager.updateDocument @RedisManager.updateDocument
.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges) .calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges, {})
.should.equal true .should.equal true
it "should call the callback", -> it "should call the callback", ->

View file

@ -24,6 +24,8 @@ describe "PersistenceManager", ->
@callback = sinon.stub() @callback = sinon.stub()
@ranges = { comments: "mock", entries: "mock" } @ranges = { comments: "mock", entries: "mock" }
@pathname = '/a/b/c.tex' @pathname = '/a/b/c.tex'
@lastUpdatedAt = Date.now()
@lastUpdatedBy = 'last-author-id'
@Settings.apis = @Settings.apis =
web: web:
url: @url = "www.example.com" url: @url = "www.example.com"
@ -133,7 +135,7 @@ describe "PersistenceManager", ->
describe "with a successful response from the web api", -> describe "with a successful response from the web api", ->
beforeEach -> beforeEach ->
@request.callsArgWith(1, null, {statusCode: 200}) @request.callsArgWith(1, null, {statusCode: 200})
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback) @PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
it "should call the web api", -> it "should call the web api", ->
@request @request
@ -143,6 +145,8 @@ describe "PersistenceManager", ->
lines: @lines lines: @lines
version: @version version: @version
ranges: @ranges ranges: @ranges
lastUpdatedAt: @lastUpdatedAt
lastUpdatedBy: @lastUpdatedBy
method: "POST" method: "POST"
auth: auth:
user: @user user: @user
@ -162,7 +166,7 @@ describe "PersistenceManager", ->
describe "when request returns an error", -> describe "when request returns an error", ->
beforeEach -> beforeEach ->
@request.callsArgWith(1, @error = new Error("oops"), null, null) @request.callsArgWith(1, @error = new Error("oops"), null, null)
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback) @PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
it "should return the error", -> it "should return the error", ->
@callback.calledWith(@error).should.equal true @callback.calledWith(@error).should.equal true
@ -173,7 +177,7 @@ describe "PersistenceManager", ->
describe "when the request returns 404", -> describe "when the request returns 404", ->
beforeEach -> beforeEach ->
@request.callsArgWith(1, null, {statusCode: 404}, "") @request.callsArgWith(1, null, {statusCode: 404}, "")
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback) @PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
it "should return a NotFoundError", -> it "should return a NotFoundError", ->
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true @callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
@ -184,7 +188,7 @@ describe "PersistenceManager", ->
describe "when the request returns an error status code", -> describe "when the request returns an error status code", ->
beforeEach -> beforeEach ->
@request.callsArgWith(1, null, {statusCode: 500}, "") @request.callsArgWith(1, null, {statusCode: 500}, "")
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback) @PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
it "should return an error", -> it "should return an error", ->
@callback.calledWith(new Error("web api error")).should.equal true @callback.calledWith(new Error("web api error")).should.equal true

View file

@ -36,6 +36,8 @@ describe "RedisManager", ->
projectHistoryId: ({doc_id}) -> "ProjectHistoryId:#{doc_id}" projectHistoryId: ({doc_id}) -> "ProjectHistoryId:#{doc_id}"
projectState: ({project_id}) -> "ProjectState:#{project_id}" projectState: ({project_id}) -> "ProjectState:#{project_id}"
unflushedTime: ({doc_id}) -> "UnflushedTime:#{doc_id}" unflushedTime: ({doc_id}) -> "UnflushedTime:#{doc_id}"
lastUpdatedBy: ({doc_id}) -> "lastUpdatedBy:#{doc_id}"
lastUpdatedAt: ({doc_id}) -> "lastUpdatedAt:#{doc_id}"
history: history:
key_schema: key_schema:
uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}" uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
@ -116,6 +118,16 @@ describe "RedisManager", ->
.calledWith("ProjectHistoryId:#{@doc_id}") .calledWith("ProjectHistoryId:#{@doc_id}")
.should.equal true .should.equal true
it "should get lastUpdatedAt", ->
@multi.get
.calledWith("lastUpdatedAt:#{@doc_id}")
.should.equal true
it "should get lastUpdatedBy", ->
@multi.get
.calledWith("lastUpdatedBy:#{@doc_id}")
.should.equal true
it "should check if the document is in the DocsIn set", -> it "should check if the document is in the DocsIn set", ->
@rclient.sadd @rclient.sadd
.calledWith("DocsIn:#{@project_id}") .calledWith("DocsIn:#{@project_id}")
@ -123,7 +135,7 @@ describe "RedisManager", ->
it 'should return the document', -> it 'should return the document', ->
@callback @callback
.calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time) .calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time, @lastUpdatedAt, @lastUpdatedBy)
.should.equal true .should.equal true
it 'should not log any errors', -> it 'should not log any errors', ->
@ -132,7 +144,7 @@ describe "RedisManager", ->
describe "when the document is not present", -> describe "when the document is not present", ->
beforeEach -> beforeEach ->
@multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null]) @multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null, null, null])
@rclient.sadd = sinon.stub().yields() @rclient.sadd = sinon.stub().yields()
@RedisManager.getDoc @project_id, @doc_id, @callback @RedisManager.getDoc @project_id, @doc_id, @callback
@ -143,7 +155,7 @@ describe "RedisManager", ->
it 'should return an empty result', -> it 'should return an empty result', ->
@callback @callback
.calledWithExactly(null, null, 0, {}, null, null, null) .calledWithExactly(null, null, 0, {}, null, null, null, null, null)
.should.equal true .should.equal true
it 'should not log any errors', -> it 'should not log any errors', ->
@ -161,7 +173,7 @@ describe "RedisManager", ->
it 'should return the document', -> it 'should return the document', ->
@callback @callback
.calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time) .calledWithExactly(null, @lines, @version, @ranges, @pathname, @projectHistoryId, @unflushed_time, @lastUpdatedAt, @lastUpdatedBy)
.should.equal true .should.equal true
describe "with a corrupted document", -> describe "with a corrupted document", ->
@ -329,6 +341,7 @@ describe "RedisManager", ->
@version = 42 @version = 42
@hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex') @hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex')
@ranges = { comments: "mock", entries: "mock" } @ranges = { comments: "mock", entries: "mock" }
@updateMeta = { user_id: 'last-author-fake-id' }
@doc_update_list_length = sinon.stub() @doc_update_list_length = sinon.stub()
@project_update_list_length = sinon.stub() @project_update_list_length = sinon.stub()
@ -340,7 +353,7 @@ describe "RedisManager", ->
@multi.del = sinon.stub() @multi.del = sinon.stub()
@multi.eval = sinon.stub() @multi.eval = sinon.stub()
@multi.exec = sinon.stub().callsArgWith(0, null, @multi.exec = sinon.stub().callsArgWith(0, null,
[@hash, null, null, null, null, null, null, @doc_update_list_length] [@hash, null, null, null, null, null, null, @doc_update_list_length, null, null]
) )
@ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith( @ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith(
@ops.length + 1, null, @project_update_list_length @ops.length + 1, null, @project_update_list_length
@ -353,7 +366,7 @@ describe "RedisManager", ->
describe "with project history enabled", -> describe "with project history enabled", ->
beforeEach -> beforeEach ->
@settings.apis.project_history.enabled = true @settings.apis.project_history.enabled = true
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
it "should get the current doc version to check for consistency", -> it "should get the current doc version to check for consistency", ->
@RedisManager.getDocVersion @RedisManager.getDocVersion
@ -385,6 +398,16 @@ describe "RedisManager", ->
.calledWith("UnflushedTime:#{@doc_id}", Date.now(), "NX") .calledWith("UnflushedTime:#{@doc_id}", Date.now(), "NX")
.should.equal true .should.equal true
it "should set the last updated time", ->
@multi.set
.calledWith("lastUpdatedAt:#{@doc_id}", Date.now())
.should.equal true
it "should set the last updater", ->
@multi.set
.calledWith("lastUpdatedBy:#{@doc_id}", 'last-author-fake-id')
.should.equal true
it "should push the doc op into the doc ops list", -> it "should push the doc op into the doc ops list", ->
@multi.rpush @multi.rpush
.calledWith("DocOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1])) .calledWith("DocOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
@ -423,7 +446,7 @@ describe "RedisManager", ->
beforeEach -> beforeEach ->
@rclient.rpush = sinon.stub() @rclient.rpush = sinon.stub()
@settings.apis.project_history.enabled = false @settings.apis.project_history.enabled = false
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
it "should not push the updates into the project history ops list", -> it "should not push the updates into the project history ops list", ->
@rclient.rpush.called.should.equal false @rclient.rpush.called.should.equal false
@ -436,7 +459,7 @@ describe "RedisManager", ->
describe "with an inconsistent version", -> describe "with an inconsistent version", ->
beforeEach -> beforeEach ->
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length - 1) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length - 1)
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
it "should not call multi.exec", -> it "should not call multi.exec", ->
@multi.exec.called.should.equal false @multi.exec.called.should.equal false
@ -450,7 +473,7 @@ describe "RedisManager", ->
beforeEach -> beforeEach ->
@rclient.rpush = sinon.stub().callsArgWith(1, null, @project_update_list_length) @rclient.rpush = sinon.stub().callsArgWith(1, null, @project_update_list_length)
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version)
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, [], @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, [], @ranges, @updateMeta, @callback
it "should not try to enqueue doc updates", -> it "should not try to enqueue doc updates", ->
@multi.rpush @multi.rpush
@ -470,7 +493,7 @@ describe "RedisManager", ->
describe "with empty ranges", -> describe "with empty ranges", ->
beforeEach -> beforeEach ->
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, {}, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, {}, @updateMeta, @callback
it "should not set the ranges", -> it "should not set the ranges", ->
@multi.set @multi.set
@ -487,7 +510,7 @@ describe "RedisManager", ->
@badHash = "INVALID-HASH-VALUE" @badHash = "INVALID-HASH-VALUE"
@multi.exec = sinon.stub().callsArgWith(0, null, [@badHash]) @multi.exec = sinon.stub().callsArgWith(0, null, [@badHash])
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
it 'should log a hash error', -> it 'should log a hash error', ->
@logger.error.calledWith() @logger.error.calledWith()
@ -501,7 +524,7 @@ describe "RedisManager", ->
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
@_stringify = JSON.stringify @_stringify = JSON.stringify
@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]' @JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
afterEach -> afterEach ->
@JSON.stringify = @_stringify @JSON.stringify = @_stringify
@ -516,7 +539,7 @@ describe "RedisManager", ->
beforeEach -> beforeEach ->
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length) @RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large")) @RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback @RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @updateMeta, @callback
it 'should log an error', -> it 'should log an error', ->
@logger.error.called.should.equal true @logger.error.called.should.equal true
@ -524,6 +547,21 @@ describe "RedisManager", ->
it "should call the callback with the error", -> it "should call the callback with the error", ->
@callback.calledWith(new Error("ranges are too large")).should.equal true @callback.calledWith(new Error("ranges are too large")).should.equal true
describe "without user id from meta", ->
beforeEach ->
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, {}, @callback
it "should set the last updater to null", ->
@multi.del
.calledWith("lastUpdatedBy:#{@doc_id}")
.should.equal true
it "should still set the last updated time", ->
@multi.set
.calledWith("lastUpdatedAt:#{@doc_id}", Date.now())
.should.equal true
describe "putDocInMemory", -> describe "putDocInMemory", ->
beforeEach -> beforeEach ->
@multi.set = sinon.stub() @multi.set = sinon.stub()
@ -681,6 +719,17 @@ describe "RedisManager", ->
.calledWith("ProjectHistoryId:#{@doc_id}") .calledWith("ProjectHistoryId:#{@doc_id}")
.should.equal true .should.equal true
it "should delete lastUpdatedAt", ->
@multi.del
.calledWith("lastUpdatedAt:#{@doc_id}")
.should.equal true
it "should delete lastUpdatedBy", ->
@multi.del
.calledWith("lastUpdatedBy:#{@doc_id}")
.should.equal true
describe "clearProjectState", -> describe "clearProjectState", ->
beforeEach (done) -> beforeEach (done) ->
@rclient.del = sinon.stub().callsArg(1) @rclient.del = sinon.stub().callsArg(1)

View file

@ -159,7 +159,8 @@ describe "UpdateManager", ->
describe "applyUpdate", -> describe "applyUpdate", ->
beforeEach -> beforeEach ->
@update = {op: [{p: 42, i: "foo"}]} @updateMeta = { user_id: 'last-author-fake-id' }
@update = {op: [{p: 42, i: "foo"}], meta: @updateMeta}
@updatedDocLines = ["updated", "lines"] @updatedDocLines = ["updated", "lines"]
@version = 34 @version = 34
@lines = ["original", "lines"] @lines = ["original", "lines"]
@ -193,7 +194,7 @@ describe "UpdateManager", ->
it "should save the document", -> it "should save the document", ->
@RedisManager.updateDocument @RedisManager.updateDocument
.calledWith(@project_id, @doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges) .calledWith(@project_id, @doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges, @updateMeta)
.should.equal true .should.equal true
it "should add metadata to the ops" , -> it "should add metadata to the ops" , ->