UpdatesManager.coffee 12 KB

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