UpdatesManager.coffee 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. MongoManager = require "./MongoManager"
  2. PackManager = require "./PackManager"
  3. RedisManager = require "./RedisManager"
  4. UpdateCompressor = require "./UpdateCompressor"
  5. LockManager = require "./LockManager"
  6. WebApiManager = require "./WebApiManager"
  7. UpdateTrimmer = require "./UpdateTrimmer"
  8. logger = require "logger-sharelatex"
  9. async = require "async"
  10. DocArchiveManager = require "./DocArchiveManager"
  11. _ = require "underscore"
  12. Settings = require "settings-sharelatex"
  13. module.exports = UpdatesManager =
  14. compressAndSaveRawUpdates: (project_id, doc_id, rawUpdates, temporary, callback = (error) ->) ->
  15. length = rawUpdates.length
  16. if length == 0
  17. return callback()
  18. MongoManager.peekLastCompressedUpdate doc_id, (error, lastCompressedUpdate, lastVersion) ->
  19. # lastCompressedUpdate is the most recent update in Mongo, and
  20. # lastVersion is its sharejs version number.
  21. #
  22. # The peekLastCompressedUpdate method may pass the update back
  23. # as 'null' (for example if the previous compressed update has
  24. # been archived). In this case it can still pass back the
  25. # lastVersion from the update to allow us to check consistency.
  26. return callback(error) if error?
  27. # Ensure that raw updates start where lastVersion left off
  28. if lastVersion?
  29. rawUpdates = rawUpdates.slice(0)
  30. while rawUpdates[0]? and rawUpdates[0].v <= lastVersion
  31. rawUpdates.shift()
  32. if rawUpdates[0]? and rawUpdates[0].v != lastVersion + 1
  33. ts = lastCompressedUpdate?.meta?.end_ts
  34. last_timestamp = if ts? then new Date(ts) else 'unknown time'
  35. error = new Error("Tried to apply raw op at version #{rawUpdates[0].v} to last compressed update with version #{lastVersion} from #{last_timestamp}")
  36. logger.error err: error, doc_id: doc_id, project_id: project_id, prev_end_ts: ts, "inconsistent doc versions"
  37. if Settings.trackchanges?.continueOnError and rawUpdates[0].v > lastVersion + 1
  38. # we have lost some ops - continue to write into the database, we can't recover at this point
  39. lastCompressedUpdate = null
  40. else
  41. return callback error
  42. if rawUpdates.length == 0
  43. return callback()
  44. if temporary and (not lastCompressedUpdate? or lastCompressedUpdate.pack?) # handle pack append as a special case
  45. UpdatesManager._updatePack project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback
  46. else #use the existing op code
  47. lastCompressedUpdate = null if lastCompressedUpdate?.pack? # can't handle packs with op code
  48. UpdatesManager._updateOp project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback
  49. _updatePack: (project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback) ->
  50. compressedUpdates = UpdateCompressor.compressRawUpdates null, rawUpdates
  51. PackManager.insertCompressedUpdates project_id, doc_id, lastCompressedUpdate, compressedUpdates, temporary, (error, result) ->
  52. return callback(error) if error?
  53. logger.log {project_id, doc_id, orig_v: lastCompressedUpdate?.v, new_v: result.v}, "inserted updates into pack" if result?
  54. callback()
  55. _updateOp: (project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback) ->
  56. compressedUpdates = UpdateCompressor.compressRawUpdates lastCompressedUpdate, rawUpdates
  57. if not lastCompressedUpdate?
  58. # no existing update, insert everything
  59. updateToModify = null
  60. updatesToInsert = compressedUpdates
  61. else
  62. # there are existing updates, see what happens when we
  63. # compress them together with the new ones
  64. [firstUpdate, additionalUpdates...] = compressedUpdates
  65. if firstUpdate.v == lastCompressedUpdate.v and _.isEqual(firstUpdate, lastCompressedUpdate)
  66. # first update version hasn't changed, skip it and insert remaining updates
  67. # this is an optimisation, we could update the existing op with itself
  68. updateToModify = null
  69. updatesToInsert = additionalUpdates
  70. else
  71. # first update version did changed, modify it and insert remaining updates
  72. updateToModify = firstUpdate
  73. updatesToInsert = additionalUpdates
  74. MongoManager.modifyCompressedUpdate lastCompressedUpdate, updateToModify, (error, result) ->
  75. return callback(error) if error?
  76. logger.log {project_id, doc_id, orig_v: lastCompressedUpdate.v, new_v: result.v}, "applied update in-place" if result?
  77. MongoManager.insertCompressedUpdates project_id, doc_id, updatesToInsert, temporary,(error) ->
  78. return callback(error) if error?
  79. logger.log project_id: project_id, doc_id: doc_id, rawUpdatesLength: rawUpdates.length, compressedUpdatesLength: compressedUpdates.length, "compressed doc updates"
  80. callback()
  81. REDIS_READ_BATCH_SIZE: 100
  82. processUncompressedUpdates: (project_id, doc_id, callback = (error) ->) ->
  83. UpdateTrimmer.shouldTrimUpdates project_id, (error, temporary) ->
  84. return callback(error) if error?
  85. MongoManager.backportProjectId project_id, doc_id, (error) ->
  86. return callback(error) if error?
  87. # get the updates as strings from redis (so we can delete them after they are applied)
  88. RedisManager.getOldestDocUpdates doc_id, UpdatesManager.REDIS_READ_BATCH_SIZE, (error, docUpdates) ->
  89. return callback(error) if error?
  90. length = docUpdates.length
  91. # parse the redis strings into ShareJs updates
  92. RedisManager.expandDocUpdates docUpdates, (error, rawUpdates) ->
  93. return callback(error) if error?
  94. UpdatesManager.compressAndSaveRawUpdates project_id, doc_id, rawUpdates, temporary, (error) ->
  95. return callback(error) if error?
  96. logger.log project_id: project_id, doc_id: doc_id, "compressed and saved doc updates"
  97. # delete the applied updates from redis
  98. RedisManager.deleteAppliedDocUpdates project_id, doc_id, docUpdates, (error) ->
  99. return callback(error) if error?
  100. if length == UpdatesManager.REDIS_READ_BATCH_SIZE
  101. # There might be more updates
  102. logger.log project_id: project_id, doc_id: doc_id, "continuing processing updates"
  103. setTimeout () ->
  104. UpdatesManager.processUncompressedUpdates project_id, doc_id, callback
  105. , 0
  106. else
  107. logger.log project_id: project_id, doc_id: doc_id, "all raw updates processed"
  108. callback()
  109. processUncompressedUpdatesWithLock: (project_id, doc_id, callback = (error) ->) ->
  110. LockManager.runWithLock(
  111. "HistoryLock:#{doc_id}",
  112. (releaseLock) ->
  113. UpdatesManager.processUncompressedUpdates project_id, doc_id, releaseLock
  114. callback
  115. )
  116. processUncompressedUpdatesForProject: (project_id, callback = (error) ->) ->
  117. RedisManager.getDocIdsWithHistoryOps project_id, (error, doc_ids) ->
  118. return callback(error) if error?
  119. jobs = []
  120. for doc_id in doc_ids
  121. do (doc_id) ->
  122. jobs.push (callback) ->
  123. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, callback
  124. async.parallelLimit jobs, 5, callback
  125. getDocUpdates: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  126. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, (error) ->
  127. return callback(error) if error?
  128. MongoManager.getDocUpdates doc_id, options, callback
  129. getDocUpdatesWithUserInfo: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  130. UpdatesManager.getDocUpdates project_id, doc_id, options, (error, updates) ->
  131. return callback(error) if error?
  132. UpdatesManager.fillUserInfo updates, (error, updates) ->
  133. return callback(error) if error?
  134. callback null, updates
  135. getProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
  136. UpdatesManager.processUncompressedUpdatesForProject project_id, (error) ->
  137. return callback(error) if error?
  138. MongoManager.getProjectUpdates project_id, options, (error, updates) ->
  139. jobs = []
  140. for update in updates
  141. if update.inS3
  142. do (update) ->
  143. jobs.push (callback) -> DocArchiveManager.unArchiveDocChanges update.project_id, update.doc_id, callback
  144. if jobs.length?
  145. async.series jobs, (err) ->
  146. MongoManager.getProjectUpdates project_id, options, callback
  147. else
  148. callback(error, updates)
  149. getProjectUpdatesWithUserInfo: (project_id, options = {}, callback = (error, updates) ->) ->
  150. UpdatesManager.getProjectUpdates project_id, options, (error, updates) ->
  151. return callback(error) if error?
  152. UpdatesManager.fillUserInfo updates, (error, updates) ->
  153. return callback(error) if error?
  154. callback null, updates
  155. getSummarizedProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
  156. options.min_count ||= 25
  157. summarizedUpdates = []
  158. before = options.before
  159. do fetchNextBatch = () ->
  160. UpdatesManager._extendBatchOfSummarizedUpdates project_id, summarizedUpdates, before, options.min_count, (error, updates, nextBeforeUpdate) ->
  161. return callback(error) if error?
  162. if !nextBeforeUpdate? or updates.length >= options.min_count
  163. callback null, updates, nextBeforeUpdate
  164. else
  165. before = nextBeforeUpdate
  166. summarizedUpdates = updates
  167. fetchNextBatch()
  168. _extendBatchOfSummarizedUpdates: (
  169. project_id,
  170. existingSummarizedUpdates,
  171. before, desiredLength,
  172. callback = (error, summarizedUpdates, endOfDatabase) ->
  173. ) ->
  174. UpdatesManager.getProjectUpdatesWithUserInfo project_id, { before: before, limit: 3 * desiredLength }, (error, updates) ->
  175. return callback(error) if error?
  176. # Suppose in this request we have fetch the solid updates. In the next request we need
  177. # to fetch the dotted updates. These are defined by having an end timestamp less than
  178. # the last update's end timestamp (updates are ordered by descending end_ts). I.e.
  179. # start_ts--v v--end_ts
  180. # doc1: |......| |...| |-------|
  181. # doc2: |------------------|
  182. # ^----- Next time, fetch all updates with an
  183. # end_ts less than this
  184. #
  185. if updates? and updates.length > 0
  186. nextBeforeTimestamp = updates[updates.length - 1].meta.end_ts
  187. if nextBeforeTimestamp >= before
  188. error = new Error("history order is broken")
  189. logger.error err: error, project_id:project_id, nextBeforeTimestamp: nextBeforeTimestamp, before:before, "error in project history"
  190. return callback(error)
  191. else
  192. nextBeforeTimestamp = null
  193. summarizedUpdates = UpdatesManager._summarizeUpdates(
  194. updates, existingSummarizedUpdates
  195. )
  196. callback null,
  197. summarizedUpdates,
  198. nextBeforeTimestamp
  199. fillUserInfo: (updates, callback = (error, updates) ->) ->
  200. users = {}
  201. for update in updates
  202. if UpdatesManager._validUserId(update.meta.user_id)
  203. users[update.meta.user_id] = true
  204. jobs = []
  205. for user_id of users
  206. do (user_id) ->
  207. jobs.push (callback) ->
  208. WebApiManager.getUserInfo user_id, (error, userInfo) ->
  209. return callback(error) if error?
  210. users[user_id] = userInfo
  211. callback()
  212. async.series jobs, (error) ->
  213. return callback(error) if error?
  214. for update in updates
  215. user_id = update.meta.user_id
  216. delete update.meta.user_id
  217. if UpdatesManager._validUserId(user_id)
  218. update.meta.user = users[user_id]
  219. callback null, updates
  220. _validUserId: (user_id) ->
  221. if !user_id?
  222. return false
  223. else
  224. return !!user_id.match(/^[a-f0-9]{24}$/)
  225. TIME_BETWEEN_DISTINCT_UPDATES: fiveMinutes = 5 * 60 * 1000
  226. _summarizeUpdates: (updates, existingSummarizedUpdates = []) ->
  227. summarizedUpdates = existingSummarizedUpdates.slice()
  228. for update in updates
  229. earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  230. if earliestUpdate and earliestUpdate.meta.start_ts - update.meta.end_ts < @TIME_BETWEEN_DISTINCT_UPDATES
  231. # check if the user in this update is already present in the earliest update,
  232. # if not, add them to the users list of the earliest update
  233. userExists = false
  234. for user in earliestUpdate.meta.users
  235. if (!user and !update.meta.user) or (user?.id == update.meta.user?.id)
  236. userExists = true
  237. break
  238. if !userExists
  239. earliestUpdate.meta.users.push update.meta.user
  240. doc_id = update.doc_id.toString()
  241. doc = earliestUpdate.docs[doc_id]
  242. if doc?
  243. doc.fromV = Math.min(doc.fromV, update.v)
  244. doc.toV = Math.max(doc.toV, update.v)
  245. else
  246. earliestUpdate.docs[doc_id] =
  247. fromV: update.v
  248. toV: update.v
  249. earliestUpdate.meta.start_ts = Math.min(earliestUpdate.meta.start_ts, update.meta.start_ts)
  250. earliestUpdate.meta.end_ts = Math.max(earliestUpdate.meta.end_ts, update.meta.end_ts)
  251. else
  252. newUpdate =
  253. meta:
  254. users: []
  255. start_ts: update.meta.start_ts
  256. end_ts: update.meta.end_ts
  257. docs: {}
  258. newUpdate.docs[update.doc_id.toString()] =
  259. fromV: update.v
  260. toV: update.v
  261. newUpdate.meta.users.push update.meta.user
  262. summarizedUpdates.push newUpdate
  263. return summarizedUpdates