UpdatesManager.coffee 10 KB

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