UpdatesManager.coffee 11 KB

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