UpdatesManager.coffee 7.9 KB

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