UpdatesManager.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. MongoManager = require "./MongoManager"
  2. PackManager = require "./PackManager"
  3. RedisManager = require "./RedisManager"
  4. UpdateCompressor = require "./UpdateCompressor"
  5. LockManager = require "./LockManager"
  6. WebApiManager = require "./WebApiManager"
  7. UpdateTrimmer = require "./UpdateTrimmer"
  8. logger = require "logger-sharelatex"
  9. async = require "async"
  10. _ = require "underscore"
  11. Settings = require "settings-sharelatex"
  12. module.exports = UpdatesManager =
  13. compressAndSaveRawUpdates: (project_id, doc_id, rawUpdates, temporary, callback = (error) ->) ->
  14. length = rawUpdates.length
  15. if length == 0
  16. return callback()
  17. # FIXME: we no longer need the lastCompressedUpdate, so change functions not to need it
  18. # CORRECTION: we do use it to log the time in case of error
  19. MongoManager.peekLastCompressedUpdate doc_id, (error, lastCompressedUpdate, lastVersion) ->
  20. # lastCompressedUpdate is the most recent update in Mongo, and
  21. # lastVersion is its sharejs version number.
  22. #
  23. # The peekLastCompressedUpdate method may pass the update back
  24. # as 'null' (for example if the previous compressed update has
  25. # been archived). In this case it can still pass back the
  26. # lastVersion from the update to allow us to check consistency.
  27. return callback(error) if error?
  28. # Ensure that raw updates start where lastVersion left off
  29. if lastVersion?
  30. discardedUpdates = []
  31. rawUpdates = rawUpdates.slice(0)
  32. while rawUpdates[0]? and rawUpdates[0].v <= lastVersion
  33. discardedUpdates.push rawUpdates.shift()
  34. if discardedUpdates.length
  35. logger.error project_id: project_id, doc_id: doc_id, discardedUpdates: discardedUpdates, temporary: temporary, lastVersion: lastVersion, "discarded updates already present"
  36. if rawUpdates[0]? and rawUpdates[0].v != lastVersion + 1
  37. ts = lastCompressedUpdate?.meta?.end_ts
  38. last_timestamp = if ts? then new Date(ts) else 'unknown time'
  39. error = new Error("Tried to apply raw op at version #{rawUpdates[0].v} to last compressed update with version #{lastVersion} from #{last_timestamp}")
  40. logger.error err: error, doc_id: doc_id, project_id: project_id, prev_end_ts: ts, temporary: temporary, lastCompressedUpdate: lastCompressedUpdate, "inconsistent doc versions"
  41. if Settings.trackchanges?.continueOnError and rawUpdates[0].v > lastVersion + 1
  42. # we have lost some ops - continue to write into the database, we can't recover at this point
  43. lastCompressedUpdate = null
  44. else
  45. return callback error
  46. if rawUpdates.length == 0
  47. return callback()
  48. # some old large ops in redis need to be rejected, they predate
  49. # the size limit that now prevents them going through the system
  50. REJECT_LARGE_OP_SIZE = 4 * 1024 * 1024
  51. for rawUpdate in rawUpdates
  52. opSizes = ((op.i?.length || op.d?.length) for op in rawUpdate?.op or [])
  53. size = _.max opSizes
  54. if size > REJECT_LARGE_OP_SIZE
  55. error = new Error("dropped op exceeding maximum allowed size of #{REJECT_LARGE_OP_SIZE}")
  56. logger.error err: error, doc_id: doc_id, project_id: project_id, size: size, rawUpdate: rawUpdate, "dropped op - too big"
  57. rawUpdate.op = []
  58. compressedUpdates = UpdateCompressor.compressRawUpdates null, rawUpdates
  59. PackManager.insertCompressedUpdates project_id, doc_id, lastCompressedUpdate, compressedUpdates, temporary, (error, result) ->
  60. return callback(error) if error?
  61. logger.log {project_id, doc_id, orig_v: lastCompressedUpdate?.v, new_v: result.v}, "inserted updates into pack" if result?
  62. callback()
  63. REDIS_READ_BATCH_SIZE: 100
  64. processUncompressedUpdates: (project_id, doc_id, callback = (error) ->) ->
  65. UpdateTrimmer.shouldTrimUpdates project_id, (error, temporary) ->
  66. return callback(error) if error?
  67. MongoManager.backportProjectId project_id, doc_id, (error) ->
  68. return callback(error) if error?
  69. # get the updates as strings from redis (so we can delete them after they are applied)
  70. RedisManager.getOldestDocUpdates doc_id, UpdatesManager.REDIS_READ_BATCH_SIZE, (error, docUpdates) ->
  71. return callback(error) if error?
  72. length = docUpdates.length
  73. # parse the redis strings into ShareJs updates
  74. RedisManager.expandDocUpdates docUpdates, (error, rawUpdates) ->
  75. return callback(error) if error?
  76. logger.log project_id: project_id, doc_id: doc_id, rawUpdates: rawUpdates, "retrieved raw updates from redis"
  77. UpdatesManager.compressAndSaveRawUpdates project_id, doc_id, rawUpdates, temporary, (error) ->
  78. return callback(error) if error?
  79. logger.log project_id: project_id, doc_id: doc_id, "compressed and saved doc updates"
  80. # delete the applied updates from redis
  81. RedisManager.deleteAppliedDocUpdates project_id, doc_id, docUpdates, (error) ->
  82. return callback(error) if error?
  83. if length == UpdatesManager.REDIS_READ_BATCH_SIZE
  84. # There might be more updates
  85. logger.log project_id: project_id, doc_id: doc_id, "continuing processing updates"
  86. setTimeout () ->
  87. UpdatesManager.processUncompressedUpdates project_id, doc_id, callback
  88. , 0
  89. else
  90. logger.log project_id: project_id, doc_id: doc_id, "all raw updates processed"
  91. callback()
  92. processUncompressedUpdatesWithLock: (project_id, doc_id, callback = (error) ->) ->
  93. LockManager.runWithLock(
  94. "HistoryLock:#{doc_id}",
  95. (releaseLock) ->
  96. UpdatesManager.processUncompressedUpdates project_id, doc_id, releaseLock
  97. callback
  98. )
  99. processUncompressedUpdatesForProject: (project_id, callback = (error) ->) ->
  100. RedisManager.getDocIdsWithHistoryOps project_id, (error, doc_ids) ->
  101. return callback(error) if error?
  102. jobs = []
  103. for doc_id in doc_ids
  104. do (doc_id) ->
  105. jobs.push (callback) ->
  106. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, callback
  107. async.parallelLimit jobs, 5, callback
  108. getDocUpdates: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  109. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, (error) ->
  110. return callback(error) if error?
  111. #console.log "options", options
  112. PackManager.getOpsByVersionRange project_id, doc_id, options.from, options.to, (error, updates) ->
  113. return callback(error) if error?
  114. callback null, updates
  115. getDocUpdatesWithUserInfo: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  116. UpdatesManager.getDocUpdates project_id, doc_id, options, (error, updates) ->
  117. return callback(error) if error?
  118. UpdatesManager.fillUserInfo updates, (error, updates) ->
  119. return callback(error) if error?
  120. callback null, updates
  121. getSummarizedProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
  122. options.min_count ||= 25
  123. summarizedUpdates = []
  124. before = options.before
  125. nextBeforeTimestamp = null
  126. UpdatesManager.processUncompressedUpdatesForProject project_id, (error) ->
  127. return callback(error) if error?
  128. PackManager.makeProjectIterator project_id, before, (err, iterator) ->
  129. return callback(err) if err?
  130. # repeatedly get updates and pass them through the summariser to get an final output with user info
  131. async.whilst () ->
  132. #console.log "checking iterator.done", iterator.done()
  133. return summarizedUpdates.length < options.min_count and not iterator.done()
  134. , (cb) ->
  135. iterator.next (err, partialUpdates) ->
  136. return callback(err) if err?
  137. #logger.log {partialUpdates}, 'got partialUpdates'
  138. return cb() if partialUpdates.length is 0 ## FIXME should try to avoid this happening
  139. nextBeforeTimestamp = partialUpdates[partialUpdates.length - 1].meta.end_ts
  140. # add the updates to the summary list
  141. summarizedUpdates = UpdatesManager._summarizeUpdates partialUpdates, summarizedUpdates
  142. cb()
  143. , () ->
  144. # finally done all updates
  145. #console.log 'summarized Updates', summarizedUpdates
  146. UpdatesManager.fillSummarizedUserInfo summarizedUpdates, (err, results) ->
  147. return callback(err) if err?
  148. callback null, results, if not iterator.done() then nextBeforeTimestamp else undefined
  149. fetchUserInfo: (users, callback = (error, fetchedUserInfo) ->) ->
  150. jobs = []
  151. fetchedUserInfo = {}
  152. for user_id of users
  153. do (user_id) ->
  154. jobs.push (callback) ->
  155. WebApiManager.getUserInfo user_id, (error, userInfo) ->
  156. return callback(error) if error?
  157. fetchedUserInfo[user_id] = userInfo
  158. callback()
  159. async.series jobs, (err) ->
  160. return callback(err) if err?
  161. callback(null, fetchedUserInfo)
  162. fillUserInfo: (updates, callback = (error, updates) ->) ->
  163. users = {}
  164. for update in updates
  165. user_id = update.meta.user_id
  166. if UpdatesManager._validUserId(user_id)
  167. users[user_id] = true
  168. UpdatesManager.fetchUserInfo users, (error, fetchedUserInfo) ->
  169. return callback(error) if error?
  170. for update in updates
  171. user_id = update.meta.user_id
  172. delete update.meta.user_id
  173. if UpdatesManager._validUserId(user_id)
  174. update.meta.user = fetchedUserInfo[user_id]
  175. callback null, updates
  176. fillSummarizedUserInfo: (updates, callback = (error, updates) ->) ->
  177. users = {}
  178. for update in updates
  179. user_ids = update.meta.user_ids or []
  180. for user_id in user_ids
  181. if UpdatesManager._validUserId(user_id)
  182. users[user_id] = true
  183. UpdatesManager.fetchUserInfo users, (error, fetchedUserInfo) ->
  184. return callback(error) if error?
  185. for update in updates
  186. user_ids = update.meta.user_ids or []
  187. update.meta.users = []
  188. delete update.meta.user_ids
  189. for user_id in user_ids
  190. if UpdatesManager._validUserId(user_id)
  191. update.meta.users.push fetchedUserInfo[user_id]
  192. else
  193. update.meta.users.push null
  194. callback null, updates
  195. _validUserId: (user_id) ->
  196. if !user_id?
  197. return false
  198. else
  199. return !!user_id.match(/^[a-f0-9]{24}$/)
  200. TIME_BETWEEN_DISTINCT_UPDATES: fiveMinutes = 5 * 60 * 1000
  201. _summarizeUpdates: (updates, existingSummarizedUpdates = []) ->
  202. summarizedUpdates = existingSummarizedUpdates.slice()
  203. for update in updates
  204. earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  205. if earliestUpdate and earliestUpdate.meta.start_ts - update.meta.end_ts < @TIME_BETWEEN_DISTINCT_UPDATES
  206. # check if the user in this update is already present in the earliest update,
  207. # if not, add them to the users list of the earliest update
  208. earliestUpdate.meta.user_ids = _.union earliestUpdate.meta.user_ids, [update.meta.user_id]
  209. doc_id = update.doc_id.toString()
  210. doc = earliestUpdate.docs[doc_id]
  211. if doc?
  212. doc.fromV = Math.min(doc.fromV, update.v)
  213. doc.toV = Math.max(doc.toV, update.v)
  214. else
  215. earliestUpdate.docs[doc_id] =
  216. fromV: update.v
  217. toV: update.v
  218. earliestUpdate.meta.start_ts = Math.min(earliestUpdate.meta.start_ts, update.meta.start_ts)
  219. earliestUpdate.meta.end_ts = Math.max(earliestUpdate.meta.end_ts, update.meta.end_ts)
  220. else
  221. newUpdate =
  222. meta:
  223. user_ids: []
  224. start_ts: update.meta.start_ts
  225. end_ts: update.meta.end_ts
  226. docs: {}
  227. newUpdate.docs[update.doc_id.toString()] =
  228. fromV: update.v
  229. toV: update.v
  230. newUpdate.meta.user_ids.push update.meta.user_id
  231. summarizedUpdates.push newUpdate
  232. return summarizedUpdates