UpdatesManager.coffee 8.8 KB

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