mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 14:13:43 +01:00
Merge pull request #2 from sharelatex/hof-doc-pathname
Add metadata to updates for history
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ pipeline {
|
||||
stage('Acceptance Tests') {
|
||||
steps {
|
||||
sh 'docker pull sharelatex/acceptance-test-runner'
|
||||
sh 'docker run --rm -v $(pwd):/app sharelatex/acceptance-test-runner'
|
||||
sh 'docker run --rm -e SHARELATEX_ENABLE_PROJECT_HISTORY=true -v $(pwd):/app sharelatex/acceptance-test-runner'
|
||||
}
|
||||
}
|
||||
stage('Package') {
|
||||
|
||||
@@ -11,31 +11,31 @@ RangesManager = require "./RangesManager"
|
||||
MAX_UNFLUSHED_AGE = 300 * 1000 # 5 mins, document should be flushed to mongo this time after a change
|
||||
|
||||
module.exports = DocumentManager =
|
||||
getDoc: (project_id, doc_id, _callback = (error, lines, version, ranges, unflushedTime, alreadyLoaded) ->) ->
|
||||
getDoc: (project_id, doc_id, _callback = (error, lines, version, ranges, pathname, unflushedTime, alreadyLoaded) ->) ->
|
||||
timer = new Metrics.Timer("docManager.getDoc")
|
||||
callback = (args...) ->
|
||||
timer.done()
|
||||
_callback(args...)
|
||||
|
||||
RedisManager.getDoc project_id, doc_id, (error, lines, version, ranges, unflushedTime) ->
|
||||
RedisManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname, unflushedTime) ->
|
||||
return callback(error) if error?
|
||||
if !lines? or !version?
|
||||
logger.log {project_id, doc_id}, "doc not in redis so getting from persistence API"
|
||||
PersistenceManager.getDoc project_id, doc_id, (error, lines, version, ranges) ->
|
||||
PersistenceManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname) ->
|
||||
return callback(error) if error?
|
||||
logger.log {project_id, doc_id, lines, version}, "got doc from persistence API"
|
||||
RedisManager.putDocInMemory project_id, doc_id, lines, version, ranges, (error) ->
|
||||
RedisManager.putDocInMemory project_id, doc_id, lines, version, ranges, pathname, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, lines, version, ranges, null, false
|
||||
callback null, lines, version, ranges, pathname, null, false
|
||||
else
|
||||
callback null, lines, version, ranges, unflushedTime, true
|
||||
callback null, lines, version, ranges, pathname, unflushedTime, true
|
||||
|
||||
getDocAndRecentOps: (project_id, doc_id, fromVersion, _callback = (error, lines, version, recentOps, ranges) ->) ->
|
||||
timer = new Metrics.Timer("docManager.getDocAndRecentOps")
|
||||
callback = (args...) ->
|
||||
timer.done()
|
||||
_callback(args...)
|
||||
|
||||
|
||||
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges) ->
|
||||
return callback(error) if error?
|
||||
if fromVersion == -1
|
||||
@@ -55,9 +55,9 @@ module.exports = DocumentManager =
|
||||
return callback(new Error("No lines were provided to setDoc"))
|
||||
|
||||
UpdateManager = require "./UpdateManager"
|
||||
DocumentManager.getDoc project_id, doc_id, (error, oldLines, version, ranges, unflushedTime, alreadyLoaded) ->
|
||||
DocumentManager.getDoc project_id, doc_id, (error, oldLines, version, ranges, pathname, unflushedTime, alreadyLoaded) ->
|
||||
return callback(error) if error?
|
||||
|
||||
|
||||
if oldLines? and oldLines.length > 0 and oldLines[0].text?
|
||||
logger.log doc_id: doc_id, project_id: project_id, oldLines: oldLines, newLines: newLines, "document is JSON so not updating"
|
||||
return callback(null)
|
||||
@@ -115,12 +115,9 @@ module.exports = DocumentManager =
|
||||
|
||||
DocumentManager.flushDocIfLoaded project_id, doc_id, (error) ->
|
||||
return callback(error) if error?
|
||||
|
||||
# Flush in the background since it requires and http request
|
||||
# to track changes
|
||||
HistoryManager.flushDocChanges project_id, doc_id, (err) ->
|
||||
if err?
|
||||
logger.err {err, project_id, doc_id}, "error flushing to track changes"
|
||||
|
||||
# Flush in the background since it requires a http request
|
||||
HistoryManager.flushChangesAsync project_id, doc_id
|
||||
|
||||
RedisManager.removeDocFromMemory project_id, doc_id, (error) ->
|
||||
return callback(error) if error?
|
||||
@@ -138,10 +135,10 @@ module.exports = DocumentManager =
|
||||
return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
|
||||
RangesManager.acceptChanges change_ids, ranges, (error, new_ranges) ->
|
||||
return callback(error) if error?
|
||||
RedisManager.updateDocument doc_id, lines, version, [], new_ranges, (error) ->
|
||||
RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
|
||||
deleteComment: (project_id, doc_id, comment_id, _callback = (error) ->) ->
|
||||
timer = new Metrics.Timer("docManager.deleteComment")
|
||||
callback = (args...) ->
|
||||
@@ -154,12 +151,12 @@ module.exports = DocumentManager =
|
||||
return callback(new Errors.NotFoundError("document not found: #{doc_id}"))
|
||||
RangesManager.deleteComment comment_id, ranges, (error, new_ranges) ->
|
||||
return callback(error) if error?
|
||||
RedisManager.updateDocument doc_id, lines, version, [], new_ranges, (error) ->
|
||||
RedisManager.updateDocument project_id, doc_id, lines, version, [], new_ranges, (error) ->
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
|
||||
getDocAndFlushIfOld: (project_id, doc_id, callback = (error, doc) ->) ->
|
||||
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges, unflushedTime, alreadyLoaded) ->
|
||||
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname, unflushedTime, alreadyLoaded) ->
|
||||
return callback(error) if error?
|
||||
# if doc was already loaded see if it needs to be flushed
|
||||
if alreadyLoaded and unflushedTime? and (Date.now() - unflushedTime) > MAX_UNFLUSHED_AGE
|
||||
@@ -172,7 +169,7 @@ module.exports = DocumentManager =
|
||||
getDocWithLock: (project_id, doc_id, callback = (error, lines, version) ->) ->
|
||||
UpdateManager = require "./UpdateManager"
|
||||
UpdateManager.lockUpdatesAndDo DocumentManager.getDoc, project_id, doc_id, callback
|
||||
|
||||
|
||||
getDocAndRecentOpsWithLock: (project_id, doc_id, fromVersion, callback = (error, lines, version) ->) ->
|
||||
UpdateManager = require "./UpdateManager"
|
||||
UpdateManager.lockUpdatesAndDo DocumentManager.getDocAndRecentOps, project_id, doc_id, fromVersion, callback
|
||||
@@ -184,7 +181,7 @@ module.exports = DocumentManager =
|
||||
setDocWithLock: (project_id, doc_id, lines, source, user_id, undoing, callback = (error) ->) ->
|
||||
UpdateManager = require "./UpdateManager"
|
||||
UpdateManager.lockUpdatesAndDo DocumentManager.setDoc, project_id, doc_id, lines, source, user_id, undoing, callback
|
||||
|
||||
|
||||
flushDocIfLoadedWithLock: (project_id, doc_id, callback = (error) ->) ->
|
||||
UpdateManager = require "./UpdateManager"
|
||||
UpdateManager.lockUpdatesAndDo DocumentManager.flushDocIfLoaded, project_id, doc_id, callback
|
||||
|
||||
@@ -1,44 +1,72 @@
|
||||
settings = require "settings-sharelatex"
|
||||
Settings = require "settings-sharelatex"
|
||||
request = require "request"
|
||||
logger = require "logger-sharelatex"
|
||||
HistoryRedisManager = require "./HistoryRedisManager"
|
||||
|
||||
module.exports = HistoryManager =
|
||||
flushDocChanges: (project_id, doc_id, callback = (error) ->) ->
|
||||
if !settings.apis?.trackchanges?
|
||||
logger.warn doc_id: doc_id, "track changes API is not configured, so not flushing"
|
||||
return callback()
|
||||
flushChangesAsync: (project_id, doc_id) ->
|
||||
HistoryManager._flushDocChangesAsync project_id, doc_id
|
||||
if Settings.apis?.project_history?.enabled
|
||||
HistoryManager._flushProjectChangesAsync project_id
|
||||
|
||||
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/doc/#{doc_id}/flush"
|
||||
logger.log project_id: project_id, doc_id: doc_id, url: url, "flushing doc in track changes api"
|
||||
_flushDocChangesAsync: (project_id, doc_id) ->
|
||||
if !Settings.apis?.trackchanges?
|
||||
logger.warn { doc_id }, "track changes API is not configured, so not flushing"
|
||||
return
|
||||
|
||||
url = "#{Settings.apis.trackchanges.url}/project/#{project_id}/doc/#{doc_id}/flush"
|
||||
logger.log { project_id, doc_id, url }, "flushing doc in track changes api"
|
||||
request.post url, (error, res, body)->
|
||||
if error?
|
||||
return callback(error)
|
||||
else if res.statusCode >= 200 and res.statusCode < 300
|
||||
return callback(null)
|
||||
else
|
||||
error = new Error("track changes api returned a failure status code: #{res.statusCode}")
|
||||
return callback(error)
|
||||
logger.error(
|
||||
{ error, doc_id, project_id},
|
||||
"track changes doc to track changes api"
|
||||
)
|
||||
else if res.statusCode < 200 and res.statusCode >= 300
|
||||
logger.error(
|
||||
{ doc_id, project_id },
|
||||
"track changes api returned a failure status code: #{res.statusCode}"
|
||||
)
|
||||
|
||||
_flushProjectChangesAsync: (project_id) ->
|
||||
return if !Settings.apis?.project_history?
|
||||
|
||||
url = "#{Settings.apis.project_history.url}/project/#{project_id}/flush"
|
||||
logger.log { project_id, url }, "flushing doc in project history api"
|
||||
request.post url, (error, res, body)->
|
||||
if error?
|
||||
logger.error { error, project_id}, "project history doc to track changes api"
|
||||
else if res.statusCode < 200 and res.statusCode >= 300
|
||||
logger.error { project_id }, "project history api returned a failure status code: #{res.statusCode}"
|
||||
|
||||
FLUSH_EVERY_N_OPS: 100
|
||||
recordAndFlushHistoryOps: (project_id, doc_id, ops = [], length, callback = (error) ->) ->
|
||||
recordAndFlushHistoryOps: (project_id, doc_id, ops = [], doc_ops_length, project_ops_length, callback = (error) ->) ->
|
||||
if ops.length == 0
|
||||
return callback()
|
||||
HistoryRedisManager.recordDocHasHistoryOps project_id, doc_id, ops, (error) ->
|
||||
return callback(error) if error?
|
||||
return callback() if not length? # don't flush unless we know the length
|
||||
# We want to flush every 100 ops, i.e. 100, 200, 300, etc
|
||||
# Find out which 'block' (i.e. 0-99, 100-199) we were in before and after pushing these
|
||||
# ops. If we've changed, then we've gone over a multiple of 100 and should flush.
|
||||
# (Most of the time, we will only hit 100 and then flushing will put us back to 0)
|
||||
previousLength = length - ops.length
|
||||
prevBlock = Math.floor(previousLength / HistoryManager.FLUSH_EVERY_N_OPS)
|
||||
newBlock = Math.floor(length / HistoryManager.FLUSH_EVERY_N_OPS)
|
||||
if newBlock != prevBlock
|
||||
|
||||
if Settings.apis?.project_history?.enabled
|
||||
if HistoryManager._shouldFlushHistoryOps(project_ops_length, ops, HistoryManager.FLUSH_EVERY_N_OPS)
|
||||
# Do this in the background since it uses HTTP and so may be too
|
||||
# slow to wait for when processing a doc update.
|
||||
logger.log length: length, doc_id: doc_id, project_id: project_id, "flushing track changes api"
|
||||
HistoryManager.flushDocChanges project_id, doc_id, (error) ->
|
||||
if error?
|
||||
logger.error err: error, doc_id: doc_id, project_id: project_id, "error flushing doc to track changes api"
|
||||
logger.log { project_ops_length, project_id }, "flushing project history api"
|
||||
HistoryManager._flushProjectChangesAsync project_id
|
||||
|
||||
HistoryRedisManager.recordDocHasHistoryOps project_id, doc_id, ops, (error) ->
|
||||
return callback(error) if error?
|
||||
if HistoryManager._shouldFlushHistoryOps(doc_ops_length, ops, HistoryManager.FLUSH_EVERY_N_OPS)
|
||||
# Do this in the background since it uses HTTP and so may be too
|
||||
# slow to wait for when processing a doc update.
|
||||
logger.log { doc_ops_length, doc_id, project_id }, "flushing track changes api"
|
||||
HistoryManager._flushDocChangesAsync project_id, doc_id
|
||||
callback()
|
||||
|
||||
_shouldFlushHistoryOps: (length, ops, threshold) ->
|
||||
return false if !length # don't flush unless we know the length
|
||||
# We want to flush every 100 ops, i.e. 100, 200, 300, etc
|
||||
# Find out which 'block' (i.e. 0-99, 100-199) we were in before and after pushing these
|
||||
# ops. If we've changed, then we've gone over a multiple of 100 and should flush.
|
||||
# (Most of the time, we will only hit 100 and then flushing will put us back to 0)
|
||||
previousLength = length - ops.length
|
||||
prevBlock = Math.floor(previousLength / threshold)
|
||||
newBlock = Math.floor(length / threshold)
|
||||
return newBlock != prevBlock
|
||||
|
||||
@@ -13,7 +13,7 @@ request = (require("requestretry")).defaults({
|
||||
MAX_HTTP_REQUEST_LENGTH = 5000 # 5 seconds
|
||||
|
||||
module.exports = PersistenceManager =
|
||||
getDoc: (project_id, doc_id, _callback = (error, lines, version, ranges) ->) ->
|
||||
getDoc: (project_id, doc_id, _callback = (error, lines, version, ranges, pathname) ->) ->
|
||||
timer = new Metrics.Timer("persistenceManager.getDoc")
|
||||
callback = (args...) ->
|
||||
timer.done()
|
||||
@@ -42,7 +42,7 @@ module.exports = PersistenceManager =
|
||||
return callback(new Error("web API response had no doc lines"))
|
||||
if !body.version? or not body.version instanceof Number
|
||||
return callback(new Error("web API response had no valid doc version"))
|
||||
return callback null, body.lines, body.version, body.ranges
|
||||
return callback null, body.lines, body.version, body.ranges, body.pathname
|
||||
else if res.statusCode == 404
|
||||
return callback(new Errors.NotFoundError("doc not not found: #{url}"))
|
||||
else
|
||||
|
||||
@@ -31,11 +31,12 @@ MAX_RANGES_SIZE = 3 * MEGABYTES
|
||||
|
||||
keys = Settings.redis.documentupdater.key_schema
|
||||
historyKeys = Settings.redis.history.key_schema
|
||||
projectHistoryKeys = Settings.redis.project_history.key_schema
|
||||
|
||||
module.exports = RedisManager =
|
||||
rclient: rclient
|
||||
|
||||
putDocInMemory : (project_id, doc_id, docLines, version, ranges, _callback)->
|
||||
putDocInMemory : (project_id, doc_id, docLines, version, ranges, pathname, _callback)->
|
||||
timer = new metrics.Timer("redis.put-doc")
|
||||
callback = (error) ->
|
||||
timer.done()
|
||||
@@ -60,6 +61,7 @@ module.exports = RedisManager =
|
||||
multi.set keys.ranges(doc_id:doc_id), ranges
|
||||
else
|
||||
multi.del keys.ranges(doc_id:doc_id)
|
||||
multi.set keys.pathname(doc_id:doc_id), pathname
|
||||
multi.exec (error, result) ->
|
||||
return callback(error) if error?
|
||||
# check the hash computed on the redis server
|
||||
@@ -85,6 +87,7 @@ module.exports = RedisManager =
|
||||
multi.del keys.docVersion(doc_id:doc_id)
|
||||
multi.del keys.docHash(doc_id:doc_id)
|
||||
multi.del keys.ranges(doc_id:doc_id)
|
||||
multi.del keys.pathname(doc_id:doc_id)
|
||||
multi.del keys.unflushedTime(doc_id:doc_id)
|
||||
multi.exec (error) ->
|
||||
return callback(error) if error?
|
||||
@@ -105,7 +108,7 @@ module.exports = RedisManager =
|
||||
clearProjectState: (project_id, callback = (error) ->) ->
|
||||
rclient.del keys.projectState(project_id:project_id), callback
|
||||
|
||||
getDoc : (project_id, doc_id, callback = (error, lines, version, ranges, unflushedTime) ->)->
|
||||
getDoc : (project_id, doc_id, callback = (error, lines, version, ranges, pathname, unflushedTime) ->)->
|
||||
timer = new metrics.Timer("redis.get-doc")
|
||||
multi = rclient.multi()
|
||||
multi.get keys.docLines(doc_id:doc_id)
|
||||
@@ -113,8 +116,9 @@ module.exports = RedisManager =
|
||||
multi.get keys.docHash(doc_id:doc_id)
|
||||
multi.get keys.projectKey(doc_id:doc_id)
|
||||
multi.get keys.ranges(doc_id:doc_id)
|
||||
multi.get keys.pathname(doc_id:doc_id)
|
||||
multi.get keys.unflushedTime(doc_id:doc_id)
|
||||
multi.exec (error, [docLines, version, storedHash, doc_project_id, ranges, unflushedTime])->
|
||||
multi.exec (error, [docLines, version, storedHash, doc_project_id, ranges, pathname, unflushedTime])->
|
||||
timeSpan = timer.done()
|
||||
return callback(error) if error?
|
||||
# check if request took too long and bail out. only do this for
|
||||
@@ -143,14 +147,14 @@ module.exports = RedisManager =
|
||||
|
||||
# doc is not in redis, bail out
|
||||
if !docLines?
|
||||
return callback null, docLines, version, ranges
|
||||
return callback null, docLines, version, ranges, pathname, unflushedTime
|
||||
|
||||
# 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) ->
|
||||
return callback(error) if error?
|
||||
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"
|
||||
callback null, docLines, version, ranges, unflushedTime
|
||||
callback null, docLines, version, ranges, pathname, unflushedTime
|
||||
|
||||
getDocVersion: (doc_id, callback = (error, version) ->) ->
|
||||
rclient.get keys.docVersion(doc_id: doc_id), (error, version) ->
|
||||
@@ -200,7 +204,7 @@ module.exports = RedisManager =
|
||||
|
||||
DOC_OPS_TTL: 60 * minutes
|
||||
DOC_OPS_MAX_LENGTH: 100
|
||||
updateDocument : (doc_id, docLines, newVersion, appliedOps = [], ranges, callback = (error) ->)->
|
||||
updateDocument : (project_id, doc_id, docLines, newVersion, appliedOps = [], ranges, callback = (error) ->)->
|
||||
RedisManager.getDocVersion doc_id, (error, currentVersion) ->
|
||||
return callback(error) if error?
|
||||
if currentVersion + appliedOps.length != newVersion
|
||||
@@ -258,9 +262,16 @@ module.exports = RedisManager =
|
||||
writeHash = result?[0]
|
||||
if logHashWriteErrors and writeHash? and writeHash isnt newHash
|
||||
logger.error doc_id: doc_id, writeHash: writeHash, origHash: newHash, docLines:newDocLines, "hash mismatch on updateDocument"
|
||||
# return length of uncompressedHistoryOps queue (index 7)
|
||||
uncompressedHistoryOpsLength = result?[7]
|
||||
return callback(null, uncompressedHistoryOpsLength)
|
||||
|
||||
# length of uncompressedHistoryOps queue (index 7)
|
||||
docUpdateCount = result[7]
|
||||
|
||||
if jsonOps.length > 0 && Settings.apis?.project_history?.enabled
|
||||
rclient.rpush projectHistoryKeys.projectHistoryOps({project_id}), jsonOps..., (error, projectUpdateCount) ->
|
||||
callback null, docUpdateCount, projectUpdateCount
|
||||
else
|
||||
callback null, docUpdateCount
|
||||
|
||||
|
||||
clearUnflushedTime: (doc_id, callback = (error) ->) ->
|
||||
rclient.del keys.unflushedTime(doc_id:doc_id), callback
|
||||
@@ -276,7 +287,7 @@ module.exports = RedisManager =
|
||||
# Most doc will have empty ranges so don't fill redis with lots of '{}' keys
|
||||
jsonRanges = null
|
||||
return callback null, jsonRanges
|
||||
|
||||
|
||||
_deserializeRanges: (ranges) ->
|
||||
if !ranges? or ranges == ""
|
||||
return {}
|
||||
|
||||
@@ -4,6 +4,7 @@ RealTimeRedisManager = require "./RealTimeRedisManager"
|
||||
ShareJsUpdateManager = require "./ShareJsUpdateManager"
|
||||
HistoryManager = require "./HistoryManager"
|
||||
Settings = require('settings-sharelatex')
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
logger = require('logger-sharelatex')
|
||||
Metrics = require "./Metrics"
|
||||
@@ -69,7 +70,7 @@ module.exports = UpdateManager =
|
||||
profile = new Profiler("applyUpdate", {project_id, doc_id})
|
||||
UpdateManager._sanitizeUpdate update
|
||||
profile.log("sanitizeUpdate")
|
||||
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges) ->
|
||||
DocumentManager.getDoc project_id, doc_id, (error, lines, version, ranges, pathname) ->
|
||||
profile.log("getDoc")
|
||||
return callback(error) if error?
|
||||
if !lines? or !version?
|
||||
@@ -78,15 +79,15 @@ module.exports = UpdateManager =
|
||||
profile.log("sharejs.applyUpdate")
|
||||
return callback(error) if error?
|
||||
RangesManager.applyUpdate project_id, doc_id, ranges, appliedOps, updatedDocLines, (error, new_ranges) ->
|
||||
UpdateManager._addProjectHistoryMetadataToOps(appliedOps, pathname, updatedDocLines)
|
||||
profile.log("RangesManager.applyUpdate")
|
||||
return callback(error) if error?
|
||||
RedisManager.updateDocument doc_id, updatedDocLines, version, appliedOps, new_ranges, (error, historyOpsLength) ->
|
||||
RedisManager.updateDocument project_id, doc_id, updatedDocLines, version, appliedOps, new_ranges, (error, doc_ops_length, project_ops_length) ->
|
||||
profile.log("RedisManager.updateDocument")
|
||||
return callback(error) if error?
|
||||
HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, historyOpsLength, (error) ->
|
||||
HistoryManager.recordAndFlushHistoryOps project_id, doc_id, appliedOps, doc_ops_length, project_ops_length, (error) ->
|
||||
profile.log("recordAndFlushHistoryOps")
|
||||
return callback(error) if error?
|
||||
callback()
|
||||
callback(error)
|
||||
|
||||
lockUpdatesAndDo: (method, project_id, doc_id, args..., callback) ->
|
||||
profile = new Profiler("lockUpdatesAndDo", {project_id, doc_id})
|
||||
@@ -109,15 +110,15 @@ module.exports = UpdateManager =
|
||||
_handleErrorInsideLock: (doc_id, lockValue, original_error, callback = (error) ->) ->
|
||||
LockManager.releaseLock doc_id, lockValue, (lock_error) ->
|
||||
callback(original_error)
|
||||
|
||||
|
||||
_sanitizeUpdate: (update) ->
|
||||
# In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
|
||||
#
|
||||
#
|
||||
# From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
|
||||
# "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
|
||||
# for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
|
||||
# and one Low Surrogate. A single surrogate code point will never be assigned a character.""
|
||||
#
|
||||
#
|
||||
# The main offender seems to be \uD835 as a stand alone character, which would be the first
|
||||
# 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
|
||||
# Something must be going on client side that is screwing up the encoding and splitting the
|
||||
@@ -128,3 +129,12 @@ module.exports = UpdateManager =
|
||||
op.i = op.i.replace(/[\uD800-\uDFFF]/g, "\uFFFD")
|
||||
return update
|
||||
|
||||
_addProjectHistoryMetadataToOps: (ops, pathname, lines) ->
|
||||
doc_length = _.reduce lines,
|
||||
(chars, line) -> chars + line.length,
|
||||
0
|
||||
doc_length += lines.length - 1 # count newline characters
|
||||
ops.forEach (op) ->
|
||||
op.meta ||= {}
|
||||
op.meta.pathname = pathname
|
||||
op.meta.doc_length = doc_length
|
||||
|
||||
@@ -14,6 +14,9 @@ module.exports =
|
||||
pass: "password"
|
||||
trackchanges:
|
||||
url: "http://localhost:3015"
|
||||
project_history:
|
||||
enabled: process.env.SHARELATEX_ENABLE_PROJECT_HISTORY == 'true'
|
||||
url: "http://localhost:3054"
|
||||
|
||||
redis:
|
||||
realtime:
|
||||
@@ -41,6 +44,7 @@ module.exports =
|
||||
projectKey: ({doc_id}) -> "ProjectId:#{doc_id}"
|
||||
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
||||
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
||||
pathname: ({doc_id}) -> "Pathname:#{doc_id}"
|
||||
projectState: ({project_id}) -> "ProjectState:#{project_id}"
|
||||
unflushedTime: ({doc_id}) -> "UnflushedTime:#{doc_id}"
|
||||
# cluster: [{
|
||||
@@ -64,6 +68,10 @@ module.exports =
|
||||
key_schema:
|
||||
uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
|
||||
docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
|
||||
|
||||
project_history:
|
||||
key_schema:
|
||||
projectHistoryOps: ({project_id}) -> "ProjectHistory:Ops:#{project_id}"
|
||||
# cluster: [{
|
||||
# port: "7000"
|
||||
# host: "localhost"
|
||||
|
||||
@@ -8,6 +8,7 @@ rclient_history = require("redis-sharelatex").createClient(Settings.redis.histor
|
||||
rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
|
||||
Keys = Settings.redis.documentupdater.key_schema
|
||||
HistoryKeys = Settings.redis.history.key_schema
|
||||
ProjectHistoryKeys = Settings.redis.project_history.key_schema
|
||||
|
||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
@@ -58,6 +59,11 @@ describe "Applying updates to a doc", ->
|
||||
result.should.equal 1
|
||||
done()
|
||||
|
||||
it "should push the applied updates to the project history changes api", (done) ->
|
||||
rclient_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
|
||||
throw error if error?
|
||||
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
||||
done()
|
||||
|
||||
describe "when the document is loaded", ->
|
||||
before (done) ->
|
||||
@@ -89,6 +95,12 @@ describe "Applying updates to a doc", ->
|
||||
result.should.equal 1
|
||||
done()
|
||||
|
||||
it "should push the applied updates to the project history changes api", (done) ->
|
||||
rclient_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
|
||||
JSON.parse(updates[0]).op.should.deep.equal @update.op
|
||||
done()
|
||||
|
||||
|
||||
describe "when the document has been deleted", ->
|
||||
describe "when the ops come in a single linear order", ->
|
||||
before (done) ->
|
||||
@@ -136,7 +148,7 @@ describe "Applying updates to a doc", ->
|
||||
rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
|
||||
result.should.equal 1
|
||||
done()
|
||||
|
||||
|
||||
it "should store the doc ops in the correct order", (done) ->
|
||||
rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
|
||||
updates = (JSON.parse(u) for u in updates)
|
||||
@@ -181,7 +193,7 @@ describe "Applying updates to a doc", ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
@broken_update = { doc_id: @doc_id, v: @version, op: [d: "not the correct content", p: 0 ] }
|
||||
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
||||
|
||||
|
||||
DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
|
||||
|
||||
DocUpdaterClient.sendUpdate @project_id, @doc_id, @broken_update, (error) ->
|
||||
@@ -192,14 +204,14 @@ describe "Applying updates to a doc", ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
||||
doc.lines.should.deep.equal @lines
|
||||
done()
|
||||
|
||||
|
||||
it "should send a message with an error", ->
|
||||
@messageCallback.called.should.equal true
|
||||
[channel, message] = @messageCallback.args[0]
|
||||
channel.should.equal "applied-ops"
|
||||
JSON.parse(message).should.deep.equal {
|
||||
project_id: @project_id,
|
||||
doc_id: @doc_id,
|
||||
doc_id: @doc_id,
|
||||
error:'Delete component \'not the correct content\' does not match deleted text \'one\ntwo\nthree\''
|
||||
}
|
||||
|
||||
@@ -240,7 +252,7 @@ describe "Applying updates to a doc", ->
|
||||
lines: @lines
|
||||
}
|
||||
|
||||
update =
|
||||
update =
|
||||
doc: @doc_id
|
||||
op: @update.op
|
||||
v: 0
|
||||
@@ -252,12 +264,12 @@ describe "Applying updates to a doc", ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
||||
doc.lines.should.deep.equal @result
|
||||
done()
|
||||
|
||||
|
||||
describe "when the sending duplicate ops", ->
|
||||
before (done) ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
|
||||
|
||||
|
||||
DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
|
||||
|
||||
# One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
|
||||
@@ -292,7 +304,7 @@ describe "Applying updates to a doc", ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
||||
doc.lines.should.deep.equal @result
|
||||
done()
|
||||
|
||||
|
||||
it "should return a message about duplicate ops", ->
|
||||
@messageCallback.calledTwice.should.equal true
|
||||
@messageCallback.args[0][0].should.equal "applied-ops"
|
||||
|
||||
@@ -3,6 +3,7 @@ chai = require("chai")
|
||||
chai.should()
|
||||
|
||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
DocUpdaterClient = require "./helpers/DocUpdaterClient"
|
||||
|
||||
@@ -18,11 +19,13 @@ describe "Deleting a document", ->
|
||||
}]
|
||||
v: @version
|
||||
@result = ["one", "one and a half", "two", "three"]
|
||||
|
||||
|
||||
sinon.spy MockTrackChangesApi, "flushDoc"
|
||||
|
||||
sinon.spy MockProjectHistoryApi, "flushProject"
|
||||
|
||||
after ->
|
||||
MockTrackChangesApi.flushDoc.restore()
|
||||
MockProjectHistoryApi.flushProject.restore()
|
||||
|
||||
describe "when the updated doc exists in the doc updater", ->
|
||||
before (done) ->
|
||||
@@ -60,10 +63,13 @@ describe "Deleting a document", ->
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
done()
|
||||
|
||||
|
||||
it "should flush track changes", ->
|
||||
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
|
||||
|
||||
it "should flush project history", ->
|
||||
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
|
||||
|
||||
describe "when the doc is not in the doc updater", ->
|
||||
before (done) ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
@@ -93,9 +99,9 @@ describe "Deleting a document", ->
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
done()
|
||||
|
||||
|
||||
it "should flush track changes", ->
|
||||
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
|
||||
|
||||
|
||||
|
||||
it "should flush project history", ->
|
||||
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
|
||||
|
||||
@@ -4,6 +4,7 @@ chai.should()
|
||||
async = require "async"
|
||||
|
||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
DocUpdaterClient = require "./helpers/DocUpdaterClient"
|
||||
|
||||
@@ -38,11 +39,13 @@ describe "Deleting a project", ->
|
||||
lines: doc.lines
|
||||
version: doc.update.v
|
||||
}
|
||||
|
||||
|
||||
sinon.spy MockTrackChangesApi, "flushDoc"
|
||||
|
||||
sinon.spy MockProjectHistoryApi, "flushProject"
|
||||
|
||||
after ->
|
||||
MockTrackChangesApi.flushDoc.restore()
|
||||
MockProjectHistoryApi.flushProject.restore()
|
||||
|
||||
describe "with documents which have been updated", ->
|
||||
before (done) ->
|
||||
@@ -84,9 +87,10 @@ describe "Deleting a project", ->
|
||||
), () ->
|
||||
MockWebApi.getDocument.restore()
|
||||
done()
|
||||
|
||||
|
||||
it "should flush each doc in track changes", ->
|
||||
for doc in @docs
|
||||
MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal true
|
||||
|
||||
|
||||
it "should flush each doc in project history", ->
|
||||
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
|
||||
|
||||
@@ -7,6 +7,7 @@ rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupd
|
||||
Keys = Settings.redis.documentupdater.key_schema
|
||||
|
||||
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
|
||||
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
|
||||
MockWebApi = require "./helpers/MockWebApi"
|
||||
DocUpdaterClient = require "./helpers/DocUpdaterClient"
|
||||
|
||||
@@ -25,14 +26,16 @@ describe "Setting a document", ->
|
||||
@newLines = ["these", "are", "the", "new", "lines"]
|
||||
@source = "dropbox"
|
||||
@user_id = "user-id-123"
|
||||
|
||||
|
||||
sinon.spy MockTrackChangesApi, "flushDoc"
|
||||
sinon.spy MockProjectHistoryApi, "flushProject"
|
||||
sinon.spy MockWebApi, "setDocument"
|
||||
|
||||
|
||||
after ->
|
||||
MockWebApi.setDocument.restore()
|
||||
MockTrackChangesApi.flushDoc.restore()
|
||||
|
||||
MockProjectHistoryApi.flushProject.restore()
|
||||
MockWebApi.setDocument.restore()
|
||||
|
||||
describe "when the updated doc exists in the doc updater", ->
|
||||
before (done) ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
@@ -64,13 +67,13 @@ describe "Setting a document", ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
|
||||
doc.version.should.equal @version + 2
|
||||
done()
|
||||
|
||||
|
||||
it "should leave the document in redis", (done) ->
|
||||
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
|
||||
throw error if error?
|
||||
expect(JSON.parse(lines)).to.deep.equal @newLines
|
||||
done()
|
||||
|
||||
|
||||
describe "when the updated doc does not exist in the doc updater", ->
|
||||
before (done) ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
@@ -78,7 +81,7 @@ describe "Setting a document", ->
|
||||
DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
|
||||
@statusCode = res.statusCode
|
||||
setTimeout done, 200
|
||||
|
||||
|
||||
it "should return a 204 status code", ->
|
||||
@statusCode.should.equal 204
|
||||
|
||||
@@ -86,16 +89,19 @@ describe "Setting a document", ->
|
||||
MockWebApi.setDocument
|
||||
.calledWith(@project_id, @doc_id, @newLines)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should flush track changes", ->
|
||||
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
|
||||
|
||||
|
||||
it "should flush project history", ->
|
||||
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
|
||||
|
||||
it "should remove the document from redis", (done) ->
|
||||
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
|
||||
throw error if error?
|
||||
expect(lines).to.not.exist
|
||||
done()
|
||||
|
||||
|
||||
describe "with track changes", ->
|
||||
before ->
|
||||
@lines = ["one", "one and a half", "two", "three"]
|
||||
@@ -123,14 +129,14 @@ describe "Setting a document", ->
|
||||
DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, true, (error, res, body) =>
|
||||
@statusCode = res.statusCode
|
||||
setTimeout done, 200
|
||||
|
||||
|
||||
it "should undo the tracked changes", (done) ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
|
||||
throw error if error?
|
||||
ranges = data.ranges
|
||||
expect(ranges.changes).to.be.undefined
|
||||
done()
|
||||
|
||||
|
||||
describe "without the undo flag", ->
|
||||
before (done) ->
|
||||
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
|
||||
@@ -143,7 +149,7 @@ describe "Setting a document", ->
|
||||
DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, false, (error, res, body) =>
|
||||
@statusCode = res.statusCode
|
||||
setTimeout done, 200
|
||||
|
||||
|
||||
it "should not undo the tracked changes", (done) ->
|
||||
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
|
||||
throw error if error?
|
||||
@@ -151,4 +157,4 @@ describe "Setting a document", ->
|
||||
expect(ranges.changes.length).to.equal 1
|
||||
done()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
express = require("express")
|
||||
app = express()
|
||||
|
||||
module.exports = MockProjectHistoryApi =
|
||||
flushProject: (doc_id, callback = (error) ->) ->
|
||||
callback()
|
||||
|
||||
run: () ->
|
||||
app.post "/project/:project_id/flush", (req, res, next) =>
|
||||
@flushProject req.params.project_id, (error) ->
|
||||
if error?
|
||||
res.send 500
|
||||
else
|
||||
res.send 204
|
||||
|
||||
app.listen 3054, (error) ->
|
||||
throw error if error?
|
||||
|
||||
MockProjectHistoryApi.run()
|
||||
@@ -9,6 +9,7 @@ module.exports = MockWebApi =
|
||||
insertDoc: (project_id, doc_id, doc) ->
|
||||
doc.version ?= 0
|
||||
doc.lines ?= []
|
||||
doc.pathname = '/a/b/c.tex'
|
||||
@docs["#{project_id}:#{doc_id}"] = doc
|
||||
|
||||
setDocument: (project_id, doc_id, lines, version, ranges, callback = (error) ->) ->
|
||||
@@ -16,6 +17,7 @@ module.exports = MockWebApi =
|
||||
doc.lines = lines
|
||||
doc.version = version
|
||||
doc.ranges = ranges
|
||||
doc.pathname = '/a/b/c.tex'
|
||||
callback null
|
||||
|
||||
getDocument: (project_id, doc_id, callback = (error, doc) ->) ->
|
||||
|
||||
+43
-42
@@ -27,6 +27,7 @@ describe "DocumentManager", ->
|
||||
@lines = ["one", "two", "three"]
|
||||
@version = 42
|
||||
@ranges = { comments: "mock", entries: "mock" }
|
||||
@pathname = '/a/b/c.tex'
|
||||
@unflushedTime = Date.now()
|
||||
|
||||
describe "flushAndDeleteDoc", ->
|
||||
@@ -34,9 +35,9 @@ describe "DocumentManager", ->
|
||||
beforeEach ->
|
||||
@RedisManager.removeDocFromMemory = sinon.stub().callsArg(2)
|
||||
@DocumentManager.flushDocIfLoaded = sinon.stub().callsArgWith(2)
|
||||
@HistoryManager.flushDocChanges = sinon.stub().callsArg(2)
|
||||
@HistoryManager.flushChangesAsync = sinon.stub()
|
||||
@DocumentManager.flushAndDeleteDoc @project_id, @doc_id, @callback
|
||||
|
||||
|
||||
it "should flush the doc", ->
|
||||
@DocumentManager.flushDocIfLoaded
|
||||
.calledWith(@project_id, @doc_id)
|
||||
@@ -52,12 +53,12 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
|
||||
it "should flush to the history api", ->
|
||||
@HistoryManager.flushDocChanges
|
||||
@HistoryManager.flushChangesAsync
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "flushDocIfLoaded", ->
|
||||
describe "when the doc is in Redis", ->
|
||||
beforeEach ->
|
||||
@@ -75,7 +76,7 @@ describe "DocumentManager", ->
|
||||
@PersistenceManager.setDoc
|
||||
.calledWith(@project_id, @doc_id, @lines, @version, @ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback without error", ->
|
||||
@callback.calledWith(null).should.equal true
|
||||
|
||||
@@ -103,7 +104,7 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
|
||||
describe "getDocAndRecentOps", ->
|
||||
describe "with a previous version specified", ->
|
||||
beforeEach ->
|
||||
@@ -146,28 +147,28 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
|
||||
describe "getDoc", ->
|
||||
describe "when the doc exists in Redis", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @unflushedTime)
|
||||
@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, @unflushedTime)
|
||||
@DocumentManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
it "should get the doc from Redis", ->
|
||||
@RedisManager.getDoc
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback with the doc info", ->
|
||||
@callback.calledWith(null, @lines, @version, @ranges, @unflushedTime, true).should.equal true
|
||||
@callback.calledWith(null, @lines, @version, @ranges, @pathname, @unflushedTime, true).should.equal true
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
describe "when the doc does not exist in Redis", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null)
|
||||
@PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges)
|
||||
@RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null, null, null, null)
|
||||
@PersistenceManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname)
|
||||
@RedisManager.putDocInMemory = sinon.stub().yields()
|
||||
@DocumentManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
@@ -183,22 +184,22 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should set the doc in Redis", ->
|
||||
@RedisManager.putDocInMemory
|
||||
.calledWith(@project_id, @doc_id, @lines, @version, @ranges)
|
||||
.calledWith(@project_id, @doc_id, @lines, @version, @ranges, @pathname)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback with the doc info", ->
|
||||
@callback.calledWith(null, @lines, @version, @ranges, null, false).should.equal true
|
||||
@callback.calledWith(null, @lines, @version, @ranges, @pathname, null, false).should.equal true
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
|
||||
describe "setDoc", ->
|
||||
describe "with plain tex lines", ->
|
||||
beforeEach ->
|
||||
@beforeLines = ["before", "lines"]
|
||||
@afterLines = ["after", "lines"]
|
||||
@ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @ranges, @unflushedTime, true)
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @ranges, @pathname, @unflushedTime, true)
|
||||
@DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
|
||||
@UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null)
|
||||
@DocumentManager.flushDocIfLoaded = sinon.stub().callsArg(2)
|
||||
@@ -240,16 +241,16 @@ describe "DocumentManager", ->
|
||||
@DocumentManager.flushDocIfLoaded
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.calledWith(null).should.equal true
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
|
||||
|
||||
describe "when not already loaded", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, null, false)
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @beforeLines, @version, @pathname, null, false)
|
||||
@DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, false, @callback
|
||||
|
||||
it "should flush and delete the doc from the doc updater", ->
|
||||
@@ -263,21 +264,21 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should return the callback with an error", ->
|
||||
@callback.calledWith(new Error("No lines were passed to setDoc"))
|
||||
|
||||
|
||||
it "should not try to get the doc lines", ->
|
||||
@DocumentManager.getDoc.called.should.equal false
|
||||
|
||||
|
||||
describe "with the undoing flag", ->
|
||||
beforeEach ->
|
||||
# Copy ops so we don't interfere with other tests
|
||||
@ops = [{ i: "foo", p: 4 }, { d: "bar", p: 42 }]
|
||||
@DiffCodec.diffAsShareJsOp = sinon.stub().callsArgWith(2, null, @ops)
|
||||
@DocumentManager.setDoc @project_id, @doc_id, @afterLines, @source, @user_id, true, @callback
|
||||
|
||||
|
||||
it "should set the undo flag on each op", ->
|
||||
for op in @ops
|
||||
op.u.should.equal true
|
||||
|
||||
|
||||
describe "acceptChanges", ->
|
||||
beforeEach ->
|
||||
@change_id = "mock-change-id"
|
||||
@@ -289,33 +290,33 @@ describe "DocumentManager", ->
|
||||
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
|
||||
@RangesManager.acceptChanges = sinon.stub().yields(null, @updated_ranges)
|
||||
@RedisManager.updateDocument = sinon.stub().yields()
|
||||
|
||||
|
||||
describe "successfully with a single change", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.acceptChanges @project_id, @doc_id, [ @change_id ], @callback
|
||||
|
||||
|
||||
it "should get the document's current ranges", ->
|
||||
@DocumentManager.getDoc
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should apply the accept change to the ranges", ->
|
||||
@RangesManager.acceptChanges
|
||||
.calledWith([ @change_id ], @ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should save the updated ranges", ->
|
||||
@RedisManager.updateDocument
|
||||
.calledWith(@doc_id, @lines, @version, [], @updated_ranges)
|
||||
.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "successfully with multiple changes", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.acceptChanges @project_id, @doc_id, @change_ids, @callback
|
||||
|
||||
|
||||
it "should apply the accept change to the ranges", ->
|
||||
@RangesManager.acceptChanges
|
||||
.calledWith(@change_ids, @ranges)
|
||||
@@ -328,11 +329,11 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should not save anything", ->
|
||||
@RedisManager.updateDocument.called.should.equal false
|
||||
|
||||
|
||||
it "should call the callback with a not found error", ->
|
||||
error = new Errors.NotFoundError("document not found: #{@doc_id}")
|
||||
@callback.calledWith(error).should.equal true
|
||||
|
||||
|
||||
describe "deleteComment", ->
|
||||
beforeEach ->
|
||||
@comment_id = "mock-comment-id"
|
||||
@@ -343,26 +344,26 @@ describe "DocumentManager", ->
|
||||
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
|
||||
@RangesManager.deleteComment = sinon.stub().yields(null, @updated_ranges)
|
||||
@RedisManager.updateDocument = sinon.stub().yields()
|
||||
|
||||
|
||||
describe "successfully", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.deleteComment @project_id, @doc_id, @comment_id, @callback
|
||||
|
||||
|
||||
it "should get the document's current ranges", ->
|
||||
@DocumentManager.getDoc
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should delete the comment from the ranges", ->
|
||||
@RangesManager.deleteComment
|
||||
.calledWith(@comment_id, @ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should save the updated ranges", ->
|
||||
@RedisManager.updateDocument
|
||||
.calledWith(@doc_id, @lines, @version, [], @updated_ranges)
|
||||
.calledWith(@project_id, @doc_id, @lines, @version, [], @updated_ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
@@ -373,7 +374,7 @@ describe "DocumentManager", ->
|
||||
|
||||
it "should not save anything", ->
|
||||
@RedisManager.updateDocument.called.should.equal false
|
||||
|
||||
|
||||
it "should call the callback with a not found error", ->
|
||||
error = new Errors.NotFoundError("document not found: #{@doc_id}")
|
||||
@callback.calledWith(error).should.equal true
|
||||
@@ -389,7 +390,7 @@ describe "DocumentManager", ->
|
||||
describe "when the doc is in Redis", ->
|
||||
describe "and has changes to be flushed", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, Date.now() - 1e9, true)
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, Date.now() - 1e9, true)
|
||||
@DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
|
||||
|
||||
it "should get the doc", ->
|
||||
@@ -407,7 +408,7 @@ describe "DocumentManager", ->
|
||||
|
||||
describe "and has only changes that don't need to be flushed", ->
|
||||
beforeEach ->
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, Date.now() - 100, true)
|
||||
@DocumentManager.getDoc = sinon.stub().callsArgWith(2, null, @lines, @version, @ranges, @pathname, Date.now() - 100, true)
|
||||
@DocumentManager.getDocAndFlushIfOld @project_id, @doc_id, @callback
|
||||
|
||||
it "should get the doc", ->
|
||||
|
||||
+135
-70
@@ -7,106 +7,171 @@ describe "HistoryManager", ->
|
||||
beforeEach ->
|
||||
@HistoryManager = SandboxedModule.require modulePath, requires:
|
||||
"request": @request = {}
|
||||
"settings-sharelatex": @Settings = {}
|
||||
"settings-sharelatex": @Settings = {
|
||||
apis:
|
||||
project_history:
|
||||
enabled: true
|
||||
url: "http://project_history.example.com"
|
||||
trackchanges:
|
||||
url: "http://trackchanges.example.com"
|
||||
}
|
||||
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
|
||||
"./HistoryRedisManager": @HistoryRedisManager = {}
|
||||
@project_id = "mock-project-id"
|
||||
@doc_id = "mock-doc-id"
|
||||
@callback = sinon.stub()
|
||||
|
||||
describe "flushDocChanges", ->
|
||||
describe "flushChangesAsync", ->
|
||||
beforeEach ->
|
||||
@Settings.apis =
|
||||
trackchanges: url: "http://trackchanges.example.com"
|
||||
@HistoryManager._flushDocChangesAsync = sinon.stub()
|
||||
@HistoryManager._flushProjectChangesAsync = sinon.stub()
|
||||
|
||||
describe "successfully", ->
|
||||
beforeEach ->
|
||||
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
|
||||
@HistoryManager.flushDocChanges @project_id, @doc_id, @callback
|
||||
@HistoryManager.flushChangesAsync(@project_id, @doc_id)
|
||||
|
||||
it "should send a request to the track changes api", ->
|
||||
@request.post
|
||||
.calledWith("#{@Settings.apis.trackchanges.url}/project/#{@project_id}/doc/#{@doc_id}/flush")
|
||||
.should.equal true
|
||||
it "flushes doc changes", ->
|
||||
@HistoryManager._flushDocChangesAsync
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should return the callback", ->
|
||||
@callback.calledWith(null).should.equal true
|
||||
it "flushes project changes", ->
|
||||
@HistoryManager._flushProjectChangesAsync
|
||||
.calledWith(@project_id)
|
||||
.should.equal true
|
||||
|
||||
describe "when the track changes api returns an error", ->
|
||||
beforeEach ->
|
||||
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 500)
|
||||
@HistoryManager.flushDocChanges @project_id, @doc_id, @callback
|
||||
describe "_flushDocChangesAsync", ->
|
||||
beforeEach ->
|
||||
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
|
||||
|
||||
it "should return the callback with an error", ->
|
||||
@callback.calledWith(new Error("track changes api return non-success code: 500")).should.equal true
|
||||
@HistoryManager._flushDocChangesAsync @project_id, @doc_id
|
||||
|
||||
it "should send a request to the track changes api", ->
|
||||
@request.post
|
||||
.calledWith("#{@Settings.apis.trackchanges.url}/project/#{@project_id}/doc/#{@doc_id}/flush")
|
||||
.should.equal true
|
||||
|
||||
describe "_flushProjectChangesAsync", ->
|
||||
beforeEach ->
|
||||
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
|
||||
|
||||
@HistoryManager._flushProjectChangesAsync @project_id
|
||||
|
||||
it "should send a request to the project history api", ->
|
||||
@request.post
|
||||
.calledWith("#{@Settings.apis.project_history.url}/project/#{@project_id}/flush")
|
||||
.should.equal true
|
||||
|
||||
describe "recordAndFlushHistoryOps", ->
|
||||
beforeEach ->
|
||||
@ops = ["mock-ops"]
|
||||
@HistoryManager.flushDocChanges = sinon.stub().callsArg(2)
|
||||
@ops = [ 'mock-ops' ]
|
||||
@project_ops_length = 10
|
||||
@doc_ops_length = 5
|
||||
|
||||
describe "pushing the op", ->
|
||||
@HistoryManager._flushProjectChangesAsync = sinon.stub()
|
||||
@HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3)
|
||||
@HistoryManager._flushDocChangesAsync = sinon.stub()
|
||||
|
||||
describe "with no ops", ->
|
||||
beforeEach ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArgWith(3, null)
|
||||
@HistoryManager.recordAndFlushHistoryOps @project_id, @doc_id, @ops, 1, @callback
|
||||
@HistoryManager.recordAndFlushHistoryOps(
|
||||
@project_id, @doc_id, [], @doc_ops_length, @project_ops_length, @callback
|
||||
)
|
||||
|
||||
it "should push the ops into redis", ->
|
||||
it "should not flush project changes", ->
|
||||
@HistoryManager._flushProjectChangesAsync.called.should.equal false
|
||||
|
||||
it "should not record doc has history ops", ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps.called.should.equal false
|
||||
|
||||
it "should not flush doc changes", ->
|
||||
@HistoryManager._flushDocChangesAsync.called.should.equal false
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "with enough ops to flush project changes", ->
|
||||
beforeEach ->
|
||||
@HistoryManager._shouldFlushHistoryOps = sinon.stub()
|
||||
@HistoryManager._shouldFlushHistoryOps.withArgs(@project_ops_length).returns(true)
|
||||
@HistoryManager._shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(false)
|
||||
|
||||
@HistoryManager.recordAndFlushHistoryOps(
|
||||
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
|
||||
)
|
||||
|
||||
it "should flush project changes", ->
|
||||
@HistoryManager._flushProjectChangesAsync
|
||||
.calledWith(@project_id)
|
||||
.should.equal true
|
||||
|
||||
it "should record doc has history ops", ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps
|
||||
.calledWith(@project_id, @doc_id, @ops)
|
||||
|
||||
it "should not flush doc changes", ->
|
||||
@HistoryManager._flushDocChangesAsync.called.should.equal false
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
describe "with enough ops to flush doc changes", ->
|
||||
beforeEach ->
|
||||
@HistoryManager._shouldFlushHistoryOps = sinon.stub()
|
||||
@HistoryManager._shouldFlushHistoryOps.withArgs(@project_ops_length).returns(false)
|
||||
@HistoryManager._shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(true)
|
||||
|
||||
@HistoryManager.recordAndFlushHistoryOps(
|
||||
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
|
||||
)
|
||||
|
||||
it "should not flush project changes", ->
|
||||
@HistoryManager._flushProjectChangesAsync.called.should.equal false
|
||||
|
||||
it "should record doc has history ops", ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps
|
||||
.calledWith(@project_id, @doc_id, @ops)
|
||||
|
||||
it "should flush doc changes", ->
|
||||
@HistoryManager._flushDocChangesAsync
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
it "should not try to flush the op", ->
|
||||
@HistoryManager.flushDocChanges.called.should.equal false
|
||||
|
||||
describe "when we hit a multiple of FLUSH_EVERY_N_OPS ops", ->
|
||||
describe "when recording doc has history ops errors", ->
|
||||
beforeEach ->
|
||||
@error = new Error("error")
|
||||
@HistoryRedisManager.recordDocHasHistoryOps =
|
||||
sinon.stub().callsArgWith(3, null)
|
||||
@HistoryManager.recordAndFlushHistoryOps @project_id, @doc_id, @ops, 2 * @HistoryManager.FLUSH_EVERY_N_OPS,@callback
|
||||
sinon.stub().callsArgWith(3, @error)
|
||||
|
||||
it "should tell the track changes api to flush", ->
|
||||
@HistoryManager.flushDocChanges
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
@HistoryManager.recordAndFlushHistoryOps(
|
||||
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
|
||||
)
|
||||
|
||||
describe "when we go over a multiple of FLUSH_EVERY_N_OPS ops", ->
|
||||
beforeEach ->
|
||||
@ops = ["op1", "op2", "op3"]
|
||||
@HistoryRedisManager.recordDocHasHistoryOps =
|
||||
sinon.stub().callsArgWith(3, null)
|
||||
@HistoryManager.recordAndFlushHistoryOps @project_id, @doc_id, @ops, 2 * @HistoryManager.FLUSH_EVERY_N_OPS + 1, @callback
|
||||
it "should not flush doc changes", ->
|
||||
@HistoryManager._flushDocChangesAsync.called.should.equal false
|
||||
|
||||
it "should tell the track changes api to flush", ->
|
||||
@HistoryManager.flushDocChanges
|
||||
.calledWith(@project_id, @doc_id)
|
||||
.should.equal true
|
||||
it "should call the callback with the error", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
describe "when HistoryManager errors", ->
|
||||
beforeEach ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps =
|
||||
sinon.stub().callsArgWith(3, null)
|
||||
@HistoryManager.flushDocChanges = sinon.stub().callsArgWith(2, @error = new Error("oops"))
|
||||
@HistoryManager.recordAndFlushHistoryOps @project_id, @doc_id, @ops, 2 * @HistoryManager.FLUSH_EVERY_N_OPS, @callback
|
||||
describe "_shouldFlushHistoryOps", ->
|
||||
it "should return false if the number of ops is not known", ->
|
||||
@HistoryManager._shouldFlushHistoryOps(null, ['a', 'b', 'c'], 1).should.equal false
|
||||
|
||||
it "should log out the error", ->
|
||||
@logger.error
|
||||
.calledWith(
|
||||
err: @error
|
||||
doc_id: @doc_id
|
||||
project_id: @project_id
|
||||
"error flushing doc to track changes api"
|
||||
)
|
||||
.should.equal true
|
||||
|
||||
describe "with no ops", ->
|
||||
beforeEach ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArgWith(3, null)
|
||||
@HistoryManager.recordAndFlushHistoryOps @project_id, @doc_id, [], 1, @callback
|
||||
|
||||
it "should not call HistoryRedisManager.recordDocHasHistoryOps", ->
|
||||
@HistoryRedisManager.recordDocHasHistoryOps.called.should.equal false
|
||||
|
||||
it "should return false if the updates didn't take us past the threshold", ->
|
||||
# Currently there are 14 ops
|
||||
# Previously we were on 11 ops
|
||||
# We didn't pass over a multiple of 5
|
||||
@HistoryManager._shouldFlushHistoryOps(14, ['a', 'b', 'c'], 5).should.equal false
|
||||
|
||||
it "should return true if the updates took to the threshold", ->
|
||||
# Currently there are 15 ops
|
||||
# Previously we were on 12 ops
|
||||
# We've reached a new multiple of 5
|
||||
@HistoryManager._shouldFlushHistoryOps(15, ['a', 'b', 'c'], 5).should.equal true
|
||||
|
||||
it "should return true if the updates took past the threshold", ->
|
||||
# Currently there are 19 ops
|
||||
# Previously we were on 16 ops
|
||||
# We didn't pass over a multiple of 5
|
||||
@HistoryManager._shouldFlushHistoryOps(17, ['a', 'b', 'c'], 5).should.equal true
|
||||
|
||||
+8
-6
@@ -22,6 +22,7 @@ describe "PersistenceManager", ->
|
||||
@version = 42
|
||||
@callback = sinon.stub()
|
||||
@ranges = { comments: "mock", entries: "mock" }
|
||||
@pathname = '/a/b/c.tex'
|
||||
@Settings.apis =
|
||||
web:
|
||||
url: @url = "www.example.com"
|
||||
@@ -29,13 +30,14 @@ describe "PersistenceManager", ->
|
||||
pass: @pass = "password"
|
||||
|
||||
describe "getDoc", ->
|
||||
|
||||
|
||||
describe "with a successful response from the web api", ->
|
||||
beforeEach ->
|
||||
@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify({
|
||||
lines: @lines,
|
||||
version: @version,
|
||||
ranges: @ranges
|
||||
pathname: @pathname,
|
||||
}))
|
||||
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
|
||||
|
||||
@@ -56,7 +58,7 @@ describe "PersistenceManager", ->
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback with the doc lines, version and ranges", ->
|
||||
@callback.calledWith(null, @lines, @version, @ranges).should.equal true
|
||||
@callback.calledWith(null, @lines, @version, @ranges, @pathname).should.equal true
|
||||
|
||||
it "should time the execution", ->
|
||||
@Metrics.Timer::done.called.should.equal true
|
||||
@@ -76,7 +78,7 @@ describe "PersistenceManager", ->
|
||||
beforeEach ->
|
||||
@request.callsArgWith(1, null, {statusCode: 404}, "")
|
||||
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
|
||||
|
||||
|
||||
it "should return a NotFoundError", ->
|
||||
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
|
||||
|
||||
@@ -87,7 +89,7 @@ describe "PersistenceManager", ->
|
||||
beforeEach ->
|
||||
@request.callsArgWith(1, null, {statusCode: 500}, "")
|
||||
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
|
||||
|
||||
|
||||
it "should return an error", ->
|
||||
@callback.calledWith(new Error("web api error")).should.equal true
|
||||
|
||||
@@ -155,7 +157,7 @@ describe "PersistenceManager", ->
|
||||
beforeEach ->
|
||||
@request.callsArgWith(1, null, {statusCode: 404}, "")
|
||||
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback)
|
||||
|
||||
|
||||
it "should return a NotFoundError", ->
|
||||
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
|
||||
|
||||
@@ -166,7 +168,7 @@ describe "PersistenceManager", ->
|
||||
beforeEach ->
|
||||
@request.callsArgWith(1, null, {statusCode: 500}, "")
|
||||
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @callback)
|
||||
|
||||
|
||||
it "should return an error", ->
|
||||
@callback.calledWith(new Error("web api error")).should.equal true
|
||||
|
||||
|
||||
+204
-143
@@ -9,16 +9,16 @@ tk = require "timekeeper"
|
||||
|
||||
describe "RedisManager", ->
|
||||
beforeEach ->
|
||||
@rclient =
|
||||
auth: () ->
|
||||
exec: sinon.stub()
|
||||
@rclient.multi = () => @rclient
|
||||
@multi = exec: sinon.stub()
|
||||
@rclient = multi: () => @multi
|
||||
tk.freeze(new Date())
|
||||
@RedisManager = SandboxedModule.require modulePath,
|
||||
requires:
|
||||
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
|
||||
"settings-sharelatex": {
|
||||
"settings-sharelatex": @settings = {
|
||||
documentupdater: {logHashErrors: {write:true, read:true}}
|
||||
apis:
|
||||
project_history: {enabled: true}
|
||||
redis:
|
||||
documentupdater:
|
||||
key_schema:
|
||||
@@ -31,12 +31,16 @@ describe "RedisManager", ->
|
||||
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
|
||||
docsInProject: ({project_id}) -> "DocsIn:#{project_id}"
|
||||
ranges: ({doc_id}) -> "Ranges:#{doc_id}"
|
||||
pathname: ({doc_id}) -> "Pathname:#{doc_id}"
|
||||
projectState: ({project_id}) -> "ProjectState:#{project_id}"
|
||||
unflushedTime: ({doc_id}) -> "UnflushedTime:#{doc_id}"
|
||||
history:
|
||||
key_schema:
|
||||
uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
|
||||
docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
|
||||
project_history:
|
||||
key_schema:
|
||||
projectHistoryOps: ({project_id}) -> "ProjectHistory:Ops:#{project_id}"
|
||||
}
|
||||
"redis-sharelatex":
|
||||
createClient: () => @rclient
|
||||
@@ -54,7 +58,7 @@ describe "RedisManager", ->
|
||||
|
||||
afterEach ->
|
||||
tk.reset()
|
||||
|
||||
|
||||
@doc_id = "doc-id-123"
|
||||
@project_id = "project-id-123"
|
||||
@callback = sinon.stub()
|
||||
@@ -68,8 +72,9 @@ describe "RedisManager", ->
|
||||
@ranges = { comments: "mock", entries: "mock" }
|
||||
@json_ranges = JSON.stringify @ranges
|
||||
@unflushed_time = 12345
|
||||
@rclient.get = sinon.stub()
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @hash, @project_id, @json_ranges, @unflushed_time])
|
||||
@pathname = '/a/b/c.tex'
|
||||
@multi.get = sinon.stub()
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @hash, @project_id, @json_ranges, @pathname, @unflushed_time])
|
||||
@rclient.sadd = sinon.stub().yields(null, 0)
|
||||
|
||||
describe "successfully", ->
|
||||
@@ -77,30 +82,35 @@ describe "RedisManager", ->
|
||||
@RedisManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
it "should get the lines from redis", ->
|
||||
@rclient.get
|
||||
@multi.get
|
||||
.calledWith("doclines:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should get the version from", ->
|
||||
@rclient.get
|
||||
@multi.get
|
||||
.calledWith("DocVersion:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it 'should get the hash', ->
|
||||
@rclient.get
|
||||
@multi.get
|
||||
.calledWith("DocHash:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should get the ranges", ->
|
||||
@rclient.get
|
||||
@multi.get
|
||||
.calledWith("Ranges:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should get the unflushed time", ->
|
||||
@rclient.get
|
||||
@multi.get
|
||||
.calledWith("UnflushedTime:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should get the pathname", ->
|
||||
@multi.get
|
||||
.calledWith("Pathname:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should check if the document is in the DocsIn set", ->
|
||||
@rclient.sadd
|
||||
.calledWith("DocsIn:#{@project_id}")
|
||||
@@ -108,7 +118,7 @@ describe "RedisManager", ->
|
||||
|
||||
it 'should return the document', ->
|
||||
@callback
|
||||
.calledWithExactly(null, @lines, @version, @ranges, @unflushed_time)
|
||||
.calledWithExactly(null, @lines, @version, @ranges, @pathname, @unflushed_time)
|
||||
.should.equal true
|
||||
|
||||
it 'should not log any errors', ->
|
||||
@@ -117,7 +127,7 @@ describe "RedisManager", ->
|
||||
|
||||
describe "when the document is not present", ->
|
||||
beforeEach ->
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null])
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null])
|
||||
@rclient.sadd = sinon.stub().yields()
|
||||
@RedisManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
@@ -128,7 +138,7 @@ describe "RedisManager", ->
|
||||
|
||||
it 'should return an empty result', ->
|
||||
@callback
|
||||
.calledWithExactly(null, null, 0, {})
|
||||
.calledWithExactly(null, null, 0, {}, null, null)
|
||||
.should.equal true
|
||||
|
||||
it 'should not log any errors', ->
|
||||
@@ -146,13 +156,13 @@ describe "RedisManager", ->
|
||||
|
||||
it 'should return the document', ->
|
||||
@callback
|
||||
.calledWithExactly(null, @lines, @version, @ranges, @unflushed_time)
|
||||
.calledWithExactly(null, @lines, @version, @ranges, @pathname, @unflushed_time)
|
||||
.should.equal true
|
||||
|
||||
describe "with a corrupted document", ->
|
||||
beforeEach ->
|
||||
@badHash = "INVALID-HASH-VALUE"
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges])
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges])
|
||||
@RedisManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
it 'should log a hash error', ->
|
||||
@@ -167,11 +177,11 @@ describe "RedisManager", ->
|
||||
|
||||
describe "with a slow request to redis", ->
|
||||
beforeEach ->
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges, @unflushed_time])
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @badHash, @project_id, @json_ranges, @pathname, @unflushed_time])
|
||||
@clock = sinon.useFakeTimers();
|
||||
@rclient.exec = (cb) =>
|
||||
@multi.exec = (cb) =>
|
||||
@clock.tick(6000);
|
||||
cb(null, [@jsonlines, @version, @another_project_id, @json_ranges, @unflushed_time])
|
||||
cb(null, [@jsonlines, @version, @another_project_id, @json_ranges, @pathname, @unflushed_time])
|
||||
|
||||
@RedisManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
@@ -186,7 +196,7 @@ describe "RedisManager", ->
|
||||
describe "getDoc with an invalid project id", ->
|
||||
beforeEach ->
|
||||
@another_project_id = "project-id-456"
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @another_project_id, @json_ranges, @unflushed_time])
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [@jsonlines, @version, @hash, @another_project_id, @json_ranges, @pathname, @unflushed_time])
|
||||
@RedisManager.getDoc @project_id, @doc_id, @callback
|
||||
|
||||
it 'should return an error', ->
|
||||
@@ -271,7 +281,7 @@ describe "RedisManager", ->
|
||||
@rclient.get = sinon.stub().callsArgWith(1, null, @version.toString())
|
||||
@rclient.lrange = sinon.stub().callsArgWith(3, null, @jsonOps)
|
||||
@RedisManager.getPreviousDocOps(@doc_id, @start, @end, @callback)
|
||||
|
||||
|
||||
it "should return an error", ->
|
||||
@callback.calledWith(new Errors.OpRangeNotAvailableError("doc ops range is not loaded in redis")).should.equal true
|
||||
|
||||
@@ -309,128 +319,168 @@ describe "RedisManager", ->
|
||||
|
||||
describe "updateDocument", ->
|
||||
beforeEach ->
|
||||
@rclient.set = sinon.stub()
|
||||
@rclient.rpush = sinon.stub()
|
||||
@rclient.expire = sinon.stub()
|
||||
@rclient.ltrim = sinon.stub()
|
||||
@rclient.del = sinon.stub()
|
||||
@rclient.eval = sinon.stub()
|
||||
@RedisManager.getDocVersion = sinon.stub()
|
||||
|
||||
@lines = ["one", "two", "three", "これは"]
|
||||
@ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }]
|
||||
@version = 42
|
||||
@hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex')
|
||||
@ranges = { comments: "mock", entries: "mock" }
|
||||
@doc_update_list_length = sinon.stub()
|
||||
@project_update_list_length = sinon.stub()
|
||||
|
||||
@rclient.exec = sinon.stub().callsArg(0, null, [@hash])
|
||||
@RedisManager.getDocVersion = sinon.stub()
|
||||
@multi.set = sinon.stub()
|
||||
@multi.rpush = sinon.stub()
|
||||
@multi.expire = sinon.stub()
|
||||
@multi.ltrim = sinon.stub()
|
||||
@multi.del = sinon.stub()
|
||||
@multi.eval = sinon.stub()
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null,
|
||||
[@hash, null, null, null, null, null, null, @doc_update_list_length]
|
||||
)
|
||||
@rclient.rpush = sinon.stub().callsArgWith(@ops.length + 1, null, @project_update_list_length)
|
||||
|
||||
describe "with a consistent version", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it "should get the current doc version to check for consistency", ->
|
||||
@RedisManager.getDocVersion
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should set the doclines", ->
|
||||
@rclient.eval
|
||||
.calledWith(sinon.match(/redis.call/), 1, "doclines:#{@doc_id}", JSON.stringify(@lines))
|
||||
.should.equal true
|
||||
|
||||
it "should set the version", ->
|
||||
@rclient.set
|
||||
.calledWith("DocVersion:#{@doc_id}", @version)
|
||||
.should.equal true
|
||||
|
||||
it "should set the hash", ->
|
||||
@rclient.set
|
||||
.calledWith("DocHash:#{@doc_id}", @hash)
|
||||
.should.equal true
|
||||
|
||||
it "should set the ranges", ->
|
||||
@rclient.set
|
||||
.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
|
||||
.should.equal true
|
||||
describe "with project history enabled", ->
|
||||
beforeEach ->
|
||||
@settings.apis.project_history.enabled = true
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it "should set the unflushed time", ->
|
||||
@rclient.set
|
||||
.calledWith("UnflushedTime:#{@doc_id}", Date.now(), "NX")
|
||||
.should.equal true
|
||||
it "should get the current doc version to check for consistency", ->
|
||||
@RedisManager.getDocVersion
|
||||
.calledWith(@doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should push the doc op into the doc ops list", ->
|
||||
@rclient.rpush
|
||||
.calledWith("DocOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
|
||||
.should.equal true
|
||||
it "should set the doclines", ->
|
||||
@multi.eval
|
||||
.calledWith(sinon.match(/redis.call/), 1, "doclines:#{@doc_id}", JSON.stringify(@lines))
|
||||
.should.equal true
|
||||
|
||||
it "should renew the expiry ttl on the doc ops array", ->
|
||||
@rclient.expire
|
||||
.calledWith("DocOps:#{@doc_id}", @RedisManager.DOC_OPS_TTL)
|
||||
.should.equal true
|
||||
it "should set the version", ->
|
||||
@multi.set
|
||||
.calledWith("DocVersion:#{@doc_id}", @version)
|
||||
.should.equal true
|
||||
|
||||
it "should truncate the list to 100 members", ->
|
||||
@rclient.ltrim
|
||||
.calledWith("DocOps:#{@doc_id}", -@RedisManager.DOC_OPS_MAX_LENGTH, -1)
|
||||
.should.equal true
|
||||
it "should set the hash", ->
|
||||
@multi.set
|
||||
.calledWith("DocHash:#{@doc_id}", @hash)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
it "should set the ranges", ->
|
||||
@multi.set
|
||||
.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
|
||||
.should.equal true
|
||||
|
||||
it "should set the unflushed time", ->
|
||||
@multi.set
|
||||
.calledWith("UnflushedTime:#{@doc_id}", Date.now(), "NX")
|
||||
.should.equal true
|
||||
|
||||
it "should push the doc op into the doc ops list", ->
|
||||
@multi.rpush
|
||||
.calledWith("DocOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
|
||||
.should.equal true
|
||||
|
||||
it "should renew the expiry ttl on the doc ops array", ->
|
||||
@multi.expire
|
||||
.calledWith("DocOps:#{@doc_id}", @RedisManager.DOC_OPS_TTL)
|
||||
.should.equal true
|
||||
|
||||
it "should truncate the list to 100 members", ->
|
||||
@multi.ltrim
|
||||
.calledWith("DocOps:#{@doc_id}", -@RedisManager.DOC_OPS_MAX_LENGTH, -1)
|
||||
.should.equal true
|
||||
|
||||
it "should push the updates into the history ops list", ->
|
||||
@multi.rpush
|
||||
.calledWith("UncompressedHistoryOps:#{@doc_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
|
||||
.should.equal true
|
||||
|
||||
it "should push the updates into the project history ops list", ->
|
||||
@rclient.rpush
|
||||
.calledWith("ProjectHistory:Ops:#{@project_id}", JSON.stringify(@ops[0]), JSON.stringify(@ops[1]))
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback
|
||||
.calledWith(null, @doc_update_list_length, @project_update_list_length)
|
||||
.should.equal true
|
||||
|
||||
it 'should not log any errors', ->
|
||||
@logger.error.calledWith()
|
||||
.should.equal false
|
||||
|
||||
describe "with project history disabled", ->
|
||||
beforeEach ->
|
||||
@rclient.rpush = sinon.stub()
|
||||
@settings.apis.project_history.enabled = false
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it "should not push the updates into the project history ops list", ->
|
||||
@rclient.rpush.called.should.equal false
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback
|
||||
.calledWith(null, @doc_update_list_length)
|
||||
.should.equal true
|
||||
|
||||
it 'should not log any errors', ->
|
||||
@logger.error.calledWith()
|
||||
.should.equal false
|
||||
|
||||
describe "with an inconsistent version", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length - 1)
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it "should not call multi.exec", ->
|
||||
@rclient.exec.called.should.equal false
|
||||
|
||||
@multi.exec.called.should.equal false
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback
|
||||
.calledWith(new Error("Version mismatch. '#{@doc_id}' is corrupted."))
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "with no updates", ->
|
||||
beforeEach ->
|
||||
@rclient.rpush = sinon.stub().callsArgWith(1, null, @project_update_list_length)
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version)
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, [], @ranges, @callback
|
||||
|
||||
it "should not do an rpush", ->
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, [], @ranges, @callback
|
||||
|
||||
it "should not try to enqueue doc updates", ->
|
||||
@multi.rpush
|
||||
.called
|
||||
.should.equal false
|
||||
|
||||
it "should not try to enqueue project updates", ->
|
||||
@rclient.rpush
|
||||
.called
|
||||
.should.equal false
|
||||
|
||||
|
||||
it "should still set the doclines", ->
|
||||
@rclient.eval
|
||||
@multi.eval
|
||||
.calledWith(sinon.match(/redis.call/), 1, "doclines:#{@doc_id}", JSON.stringify(@lines))
|
||||
.should.equal true
|
||||
|
||||
|
||||
describe "with empty ranges", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, {}, @callback
|
||||
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, {}, @callback
|
||||
|
||||
it "should not set the ranges", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
|
||||
.should.equal false
|
||||
|
||||
|
||||
it "should delete the ranges key", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("Ranges:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
describe "with a corrupted write", ->
|
||||
beforeEach ->
|
||||
@badHash = "INVALID-HASH-VALUE"
|
||||
@rclient.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.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it 'should log a hash error', ->
|
||||
@logger.error.calledWith()
|
||||
@@ -444,22 +494,22 @@ describe "RedisManager", ->
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
|
||||
@_stringify = JSON.stringify
|
||||
@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
afterEach ->
|
||||
@JSON.stringify = @_stringify
|
||||
|
||||
|
||||
it "should log an error", ->
|
||||
@logger.error.called.should.equal true
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
||||
|
||||
|
||||
describe "with ranges that are too big", ->
|
||||
beforeEach ->
|
||||
@RedisManager.getDocVersion.withArgs(@doc_id).yields(null, @version - @ops.length)
|
||||
@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
|
||||
@RedisManager.updateDocument @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
@RedisManager.updateDocument @project_id, @doc_id, @lines, @version, @ops, @ranges, @callback
|
||||
|
||||
it 'should log an error', ->
|
||||
@logger.error.called.should.equal true
|
||||
@@ -469,45 +519,51 @@ describe "RedisManager", ->
|
||||
|
||||
describe "putDocInMemory", ->
|
||||
beforeEach ->
|
||||
@rclient.set = sinon.stub()
|
||||
@multi.set = sinon.stub()
|
||||
@rclient.sadd = sinon.stub().yields()
|
||||
@rclient.del = sinon.stub()
|
||||
@rclient.eval = sinon.stub()
|
||||
@multi.del = sinon.stub()
|
||||
@multi.eval = sinon.stub()
|
||||
@lines = ["one", "two", "three", "これは"]
|
||||
@version = 42
|
||||
@hash = crypto.createHash('sha1').update(JSON.stringify(@lines),'utf8').digest('hex')
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, [@hash])
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, [@hash])
|
||||
@ranges = { comments: "mock", entries: "mock" }
|
||||
|
||||
@pathname = '/a/b/c.tex'
|
||||
|
||||
describe "with non-empty ranges", ->
|
||||
beforeEach (done) ->
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, done
|
||||
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, done
|
||||
|
||||
it "should set the lines", ->
|
||||
@rclient.eval
|
||||
@multi.eval
|
||||
.calledWith(sinon.match(/redis.call/), 1, "doclines:#{@doc_id}", JSON.stringify(@lines))
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should set the version", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("DocVersion:#{@doc_id}", @version)
|
||||
.should.equal true
|
||||
|
||||
it "should set the hash", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("DocHash:#{@doc_id}", @hash)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should set the ranges", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should set the project_id for the doc", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("ProjectId:#{@doc_id}", @project_id)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should set the pathname for the doc", ->
|
||||
@multi.set
|
||||
.calledWith("Pathname:#{@doc_id}", @pathname)
|
||||
.should.equal true
|
||||
|
||||
it "should add the doc_id to the project set", ->
|
||||
@rclient.sadd
|
||||
.calledWith("DocsIn:#{@project_id}", @doc_id)
|
||||
@@ -516,25 +572,25 @@ describe "RedisManager", ->
|
||||
it 'should not log any errors', ->
|
||||
@logger.error.calledWith()
|
||||
.should.equal false
|
||||
|
||||
|
||||
describe "with empty ranges", ->
|
||||
beforeEach (done) ->
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, {}, done
|
||||
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, {}, @pathname, done
|
||||
|
||||
it "should delete the ranges key", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("Ranges:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should not set the ranges", ->
|
||||
@rclient.set
|
||||
@multi.set
|
||||
.calledWith("Ranges:#{@doc_id}", JSON.stringify(@ranges))
|
||||
.should.equal false
|
||||
|
||||
describe "with a corrupted write", ->
|
||||
beforeEach (done) ->
|
||||
@rclient.exec = sinon.stub().callsArgWith(0, null, ["INVALID-HASH-VALUE"])
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, done
|
||||
@multi.exec = sinon.stub().callsArgWith(0, null, ["INVALID-HASH-VALUE"])
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, done
|
||||
|
||||
it 'should log a hash error', ->
|
||||
@logger.error.calledWith()
|
||||
@@ -544,21 +600,21 @@ describe "RedisManager", ->
|
||||
beforeEach ->
|
||||
@_stringify = JSON.stringify
|
||||
@JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]'
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @callback
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, @callback
|
||||
|
||||
afterEach ->
|
||||
@JSON.stringify = @_stringify
|
||||
|
||||
|
||||
it "should log an error", ->
|
||||
@logger.error.called.should.equal true
|
||||
|
||||
it "should call the callback with an error", ->
|
||||
@callback.calledWith(new Error("null bytes found in doc lines")).should.equal true
|
||||
|
||||
|
||||
describe "with ranges that are too big", ->
|
||||
beforeEach ->
|
||||
@RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"))
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @callback
|
||||
@RedisManager.putDocInMemory @project_id, @doc_id, @lines, @version, @ranges, @pathname, @callback
|
||||
|
||||
it 'should log an error', ->
|
||||
@logger.error.called.should.equal true
|
||||
@@ -568,41 +624,46 @@ describe "RedisManager", ->
|
||||
|
||||
describe "removeDocFromMemory", ->
|
||||
beforeEach (done) ->
|
||||
@rclient.del = sinon.stub()
|
||||
@rclient.srem = sinon.stub()
|
||||
@rclient.exec.yields()
|
||||
@multi.del = sinon.stub()
|
||||
@multi.srem = sinon.stub()
|
||||
@multi.exec.yields()
|
||||
@RedisManager.removeDocFromMemory @project_id, @doc_id, done
|
||||
|
||||
|
||||
it "should delete the lines", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("doclines:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should delete the version", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("DocVersion:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should delete the hash", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("DocHash:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
it "should delete the unflushed time", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("UnflushedTime:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should delete the project_id for the doc", ->
|
||||
@rclient.del
|
||||
@multi.del
|
||||
.calledWith("ProjectId:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should remove the doc_id from the project set", ->
|
||||
@rclient.srem
|
||||
@multi.srem
|
||||
.calledWith("DocsIn:#{@project_id}", @doc_id)
|
||||
.should.equal true
|
||||
|
||||
it "should delete the pathname for the doc", ->
|
||||
@multi.del
|
||||
.calledWith("Pathname:#{@doc_id}")
|
||||
.should.equal true
|
||||
|
||||
describe "clearProjectState", ->
|
||||
beforeEach (done) ->
|
||||
@rclient.del = sinon.stub().callsArg(1)
|
||||
|
||||
+54
-23
@@ -19,7 +19,7 @@ describe "UpdateManager", ->
|
||||
"./Metrics": @Metrics =
|
||||
Timer: class Timer
|
||||
done: sinon.stub()
|
||||
"settings-sharelatex": Settings = {}
|
||||
"settings-sharelatex": @Settings = {}
|
||||
"./DocumentManager": @DocumentManager = {}
|
||||
"./RangesManager": @RangesManager = {}
|
||||
"./Profiler": class Profiler
|
||||
@@ -60,14 +60,14 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should process the outstanding updates", ->
|
||||
@UpdateManager.processOutstandingUpdates.calledWith(@project_id, @doc_id).should.equal true
|
||||
|
||||
|
||||
it "should do everything with the lock acquired", ->
|
||||
@UpdateManager.processOutstandingUpdates.calledAfter(@LockManager.tryLock).should.equal true
|
||||
@UpdateManager.processOutstandingUpdates.calledBefore(@LockManager.releaseLock).should.equal true
|
||||
|
||||
it "should continue processing new updates that may have come in", ->
|
||||
@UpdateManager.continueProcessingUpdatesWithLock.calledWith(@project_id, @doc_id).should.equal true
|
||||
|
||||
|
||||
it "should return the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
@@ -78,7 +78,7 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should free the lock", ->
|
||||
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
|
||||
|
||||
|
||||
it "should return the error in the callback", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
@@ -93,7 +93,7 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should not process the updates", ->
|
||||
@UpdateManager.processOutstandingUpdates.called.should.equal false
|
||||
|
||||
|
||||
describe "continueProcessingUpdatesWithLock", ->
|
||||
describe "when there are outstanding updates", ->
|
||||
beforeEach ->
|
||||
@@ -137,7 +137,7 @@ describe "UpdateManager", ->
|
||||
@UpdateManager.applyUpdate
|
||||
.calledWith(@project_id, @doc_id, update)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
@@ -154,7 +154,7 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should call the callback", ->
|
||||
@callback.called.should.equal true
|
||||
|
||||
|
||||
describe "applyUpdate", ->
|
||||
beforeEach ->
|
||||
@update = {op: [{p: 42, i: "foo"}]}
|
||||
@@ -163,23 +163,27 @@ describe "UpdateManager", ->
|
||||
@lines = ["original", "lines"]
|
||||
@ranges = { entries: "mock", comments: "mock" }
|
||||
@updated_ranges = { entries: "updated", comments: "updated" }
|
||||
@appliedOps = ["mock-applied-ops"]
|
||||
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges)
|
||||
@appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]
|
||||
@doc_ops_length = sinon.stub()
|
||||
@project_ops_length = sinon.stub()
|
||||
@pathname = '/a/b/c.tex'
|
||||
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges, @pathname)
|
||||
@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges)
|
||||
@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, @updatedDocLines, @version, @appliedOps)
|
||||
@RedisManager.updateDocument = sinon.stub().yields()
|
||||
@RedisManager.updateDocument = sinon.stub().yields(null, @doc_ops_length, @project_ops_length)
|
||||
@RealTimeRedisManager.sendData = sinon.stub()
|
||||
@HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(4)
|
||||
|
||||
@UpdateManager._addProjectHistoryMetadataToOps = sinon.stub()
|
||||
@HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5)
|
||||
|
||||
describe "normally", ->
|
||||
beforeEach ->
|
||||
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
|
||||
|
||||
|
||||
it "should apply the updates via ShareJS", ->
|
||||
@ShareJsUpdateManager.applyUpdate
|
||||
.calledWith(@project_id, @doc_id, @update, @lines, @version)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "should update the ranges", ->
|
||||
@RangesManager.applyUpdate
|
||||
.calledWith(@project_id, @doc_id, @ranges, @appliedOps, @updatedDocLines)
|
||||
@@ -187,12 +191,17 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should save the document", ->
|
||||
@RedisManager.updateDocument
|
||||
.calledWith(@doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges)
|
||||
.calledWith(@project_id, @doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges)
|
||||
.should.equal true
|
||||
|
||||
|
||||
it "shoould add metadata to the ops" , ->
|
||||
@UpdateManager._addProjectHistoryMetadataToOps
|
||||
.calledWith(@appliedOps, @pathname, @updatedDocLines)
|
||||
.should.equal true
|
||||
|
||||
it "should push the applied ops into the history queue", ->
|
||||
@HistoryManager.recordAndFlushHistoryOps
|
||||
.calledWith(@project_id, @doc_id, @appliedOps)
|
||||
.calledWith(@project_id, @doc_id, @appliedOps, @doc_ops_length, @project_ops_length)
|
||||
.should.equal true
|
||||
|
||||
it "should call the callback", ->
|
||||
@@ -207,16 +216,16 @@ describe "UpdateManager", ->
|
||||
@ShareJsUpdateManager.applyUpdate
|
||||
.calledWith(@project_id, @doc_id, @update)
|
||||
.should.equal true
|
||||
|
||||
|
||||
# \uFFFD is 'replacement character'
|
||||
@update.op[0].i.should.equal "\uFFFD\uFFFD"
|
||||
|
||||
|
||||
describe "with an error", ->
|
||||
beforeEach ->
|
||||
@error = new Error("something went wrong")
|
||||
@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(@error)
|
||||
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
|
||||
|
||||
|
||||
it "should call RealTimeRedisManager.sendData with the error", ->
|
||||
@RealTimeRedisManager.sendData
|
||||
.calledWith({
|
||||
@@ -228,7 +237,29 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should call the callback with the error", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
|
||||
describe "_addProjectHistoryMetadataToOps", ->
|
||||
it "should add pathname and doc_length metadata to the ops", ->
|
||||
lines = [
|
||||
'some'
|
||||
'test'
|
||||
'data'
|
||||
]
|
||||
appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]
|
||||
@UpdateManager._addProjectHistoryMetadataToOps(appliedOps, @pathname, lines)
|
||||
appliedOps.should.deep.equal [{
|
||||
v: 42
|
||||
op: "mock-op-42"
|
||||
meta:
|
||||
pathname: @pathname
|
||||
doc_length: 14
|
||||
}, {
|
||||
v: 45
|
||||
op: "mock-op-45"
|
||||
meta:
|
||||
pathname: @pathname
|
||||
doc_length: 14
|
||||
}]
|
||||
|
||||
describe "lockUpdatesAndDo", ->
|
||||
beforeEach ->
|
||||
@@ -283,7 +314,7 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should free the lock", ->
|
||||
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
|
||||
|
||||
|
||||
it "should return the error in the callback", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
@@ -295,7 +326,7 @@ describe "UpdateManager", ->
|
||||
|
||||
it "should free the lock", ->
|
||||
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
|
||||
|
||||
|
||||
it "should return the error in the callback", ->
|
||||
@callback.calledWith(@error).should.equal true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user