UpdatesManager.coffee 9.7 KB

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