UpdatesManager.coffee 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. DocArchiveManager = require "./DocArchiveManager"
  11. _ = require "underscore"
  12. Settings = require "settings-sharelatex"
  13. module.exports = UpdatesManager =
  14. compressAndSaveRawUpdates: (project_id, doc_id, rawUpdates, temporary, callback = (error) ->) ->
  15. length = rawUpdates.length
  16. if length == 0
  17. return callback()
  18. MongoManager.peekLastCompressedUpdate doc_id, (error, lastCompressedUpdate, lastVersion) ->
  19. # lastCompressedUpdate is the most recent update in Mongo, and
  20. # lastVersion is its sharejs version number.
  21. #
  22. # The peekLastCompressedUpdate method may pass the update back
  23. # as 'null' (for example if the previous compressed update has
  24. # been archived). In this case it can still pass back the
  25. # lastVersion from the update to allow us to check consistency.
  26. return callback(error) if error?
  27. # Ensure that raw updates start where lastVersion left off
  28. if lastVersion?
  29. rawUpdates = rawUpdates.slice(0)
  30. while rawUpdates[0]? and rawUpdates[0].v <= lastVersion
  31. rawUpdates.shift()
  32. if rawUpdates[0]? and rawUpdates[0].v != lastVersion + 1
  33. ts = lastCompressedUpdate?.meta?.end_ts
  34. last_timestamp = if ts? then new Date(ts) else 'unknown time'
  35. error = new Error("Tried to apply raw op at version #{rawUpdates[0].v} to last compressed update with version #{lastVersion} from #{last_timestamp}")
  36. logger.error err: error, doc_id: doc_id, project_id: project_id, prev_end_ts: ts, "inconsistent doc versions"
  37. if Settings.trackchanges?.continueOnError and rawUpdates[0].v > lastVersion + 1
  38. # we have lost some ops - continue to write into the database, we can't recover at this point
  39. lastCompressedUpdate = null
  40. else
  41. return callback error
  42. if rawUpdates.length == 0
  43. return callback()
  44. # some old large ops in redis need to be rejected, they predate
  45. # the size limit that now prevents them going through the system
  46. REJECT_LARGE_OP_SIZE = 4 * 1024 * 1024
  47. for rawUpdate in rawUpdates
  48. opSizes = ((op.i?.length || op.d?.length) for op in rawUpdate?.op or [])
  49. size = _.max opSizes
  50. if size > REJECT_LARGE_OP_SIZE
  51. error = new Error("dropped op exceeding maximum allowed size of #{REJECT_LARGE_OP_SIZE}")
  52. logger.error err: error, doc_id: doc_id, project_id: project_id, size: size, rawUpdate: rawUpdate, "dropped op - too big"
  53. rawUpdate.op = []
  54. if (not lastCompressedUpdate?) or lastCompressedUpdate.pack? # handle pack append as a special case
  55. UpdatesManager._updatePack project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback
  56. else #use the existing op code
  57. UpdatesManager._updateOp project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback
  58. _updatePack: (project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback) ->
  59. compressedUpdates = UpdateCompressor.compressRawUpdates null, rawUpdates
  60. PackManager.insertCompressedUpdates project_id, doc_id, lastCompressedUpdate, compressedUpdates, temporary, (error, result) ->
  61. return callback(error) if error?
  62. logger.log {project_id, doc_id, orig_v: lastCompressedUpdate?.v, new_v: result.v}, "inserted updates into pack" if result?
  63. callback()
  64. _updateOp: (project_id, doc_id, rawUpdates, temporary, lastCompressedUpdate, lastVersion, callback) ->
  65. compressedUpdates = UpdateCompressor.compressRawUpdates lastCompressedUpdate, rawUpdates
  66. if not lastCompressedUpdate?
  67. # no existing update, insert everything
  68. updateToModify = null
  69. updatesToInsert = compressedUpdates
  70. else
  71. # there are existing updates, see what happens when we
  72. # compress them together with the new ones
  73. [firstUpdate, additionalUpdates...] = compressedUpdates
  74. if firstUpdate.v == lastCompressedUpdate.v and _.isEqual(firstUpdate, lastCompressedUpdate)
  75. # first update version hasn't changed, skip it and insert remaining updates
  76. # this is an optimisation, we could update the existing op with itself
  77. updateToModify = null
  78. updatesToInsert = additionalUpdates
  79. else
  80. # first update version did changed, modify it and insert remaining updates
  81. updateToModify = firstUpdate
  82. updatesToInsert = additionalUpdates
  83. MongoManager.modifyCompressedUpdate lastCompressedUpdate, updateToModify, (error, result) ->
  84. return callback(error) if error?
  85. logger.log {project_id, doc_id, orig_v: lastCompressedUpdate.v, new_v: result.v}, "applied update in-place" if result?
  86. MongoManager.insertCompressedUpdates project_id, doc_id, updatesToInsert, temporary,(error) ->
  87. return callback(error) if error?
  88. logger.log project_id: project_id, doc_id: doc_id, rawUpdatesLength: rawUpdates.length, compressedUpdatesLength: compressedUpdates.length, "compressed doc updates"
  89. callback()
  90. REDIS_READ_BATCH_SIZE: 100
  91. processUncompressedUpdates: (project_id, doc_id, callback = (error) ->) ->
  92. UpdateTrimmer.shouldTrimUpdates project_id, (error, temporary) ->
  93. return callback(error) if error?
  94. MongoManager.backportProjectId project_id, doc_id, (error) ->
  95. return callback(error) if error?
  96. # get the updates as strings from redis (so we can delete them after they are applied)
  97. RedisManager.getOldestDocUpdates doc_id, UpdatesManager.REDIS_READ_BATCH_SIZE, (error, docUpdates) ->
  98. return callback(error) if error?
  99. length = docUpdates.length
  100. # parse the redis strings into ShareJs updates
  101. RedisManager.expandDocUpdates docUpdates, (error, rawUpdates) ->
  102. return callback(error) if error?
  103. logger.log project_id: project_id, doc_id: doc_id, rawUpdates: rawUpdates, "retrieved raw updates from redis"
  104. UpdatesManager.compressAndSaveRawUpdates project_id, doc_id, rawUpdates, temporary, (error) ->
  105. return callback(error) if error?
  106. logger.log project_id: project_id, doc_id: doc_id, "compressed and saved doc updates"
  107. # delete the applied updates from redis
  108. RedisManager.deleteAppliedDocUpdates project_id, doc_id, docUpdates, (error) ->
  109. return callback(error) if error?
  110. if length == UpdatesManager.REDIS_READ_BATCH_SIZE
  111. # There might be more updates
  112. logger.log project_id: project_id, doc_id: doc_id, "continuing processing updates"
  113. setTimeout () ->
  114. UpdatesManager.processUncompressedUpdates project_id, doc_id, callback
  115. , 0
  116. else
  117. logger.log project_id: project_id, doc_id: doc_id, "all raw updates processed"
  118. callback()
  119. processUncompressedUpdatesWithLock: (project_id, doc_id, callback = (error) ->) ->
  120. LockManager.runWithLock(
  121. "HistoryLock:#{doc_id}",
  122. (releaseLock) ->
  123. UpdatesManager.processUncompressedUpdates project_id, doc_id, releaseLock
  124. callback
  125. )
  126. processUncompressedUpdatesForProject: (project_id, callback = (error) ->) ->
  127. RedisManager.getDocIdsWithHistoryOps project_id, (error, doc_ids) ->
  128. return callback(error) if error?
  129. jobs = []
  130. for doc_id in doc_ids
  131. do (doc_id) ->
  132. jobs.push (callback) ->
  133. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, callback
  134. async.parallelLimit jobs, 5, callback
  135. getDocUpdates: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  136. UpdatesManager.processUncompressedUpdatesWithLock project_id, doc_id, (error) ->
  137. return callback(error) if error?
  138. MongoManager.getDocUpdates doc_id, options, callback
  139. getDocUpdatesWithUserInfo: (project_id, doc_id, options = {}, callback = (error, updates) ->) ->
  140. UpdatesManager.getDocUpdates project_id, doc_id, options, (error, updates) ->
  141. return callback(error) if error?
  142. UpdatesManager.fillUserInfo updates, (error, updates) ->
  143. return callback(error) if error?
  144. callback null, updates
  145. getProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
  146. UpdatesManager.processUncompressedUpdatesForProject project_id, (error) ->
  147. return callback(error) if error?
  148. MongoManager.getProjectUpdates project_id, options, (error, updates) ->
  149. jobs = []
  150. for update in updates
  151. if update.inS3
  152. do (update) ->
  153. jobs.push (callback) -> DocArchiveManager.unArchiveDocChanges update.project_id, update.doc_id, callback
  154. if jobs.length?
  155. async.series jobs, (err) ->
  156. MongoManager.getProjectUpdates project_id, options, callback
  157. else
  158. callback(error, updates)
  159. getProjectUpdatesWithUserInfo: (project_id, options = {}, callback = (error, updates) ->) ->
  160. UpdatesManager.getProjectUpdates project_id, options, (error, updates) ->
  161. return callback(error) if error?
  162. UpdatesManager.fillUserInfo updates, (error, updates) ->
  163. return callback(error) if error?
  164. callback null, updates
  165. getSummarizedProjectUpdates: (project_id, options = {}, callback = (error, updates) ->) ->
  166. options.min_count ||= 25
  167. summarizedUpdates = []
  168. before = options.before
  169. do fetchNextBatch = () ->
  170. UpdatesManager._extendBatchOfSummarizedUpdates project_id, summarizedUpdates, before, options.min_count, (error, updates, nextBeforeUpdate) ->
  171. return callback(error) if error?
  172. if !nextBeforeUpdate? or updates.length >= options.min_count
  173. callback null, updates, nextBeforeUpdate
  174. else
  175. before = nextBeforeUpdate
  176. summarizedUpdates = updates
  177. fetchNextBatch()
  178. _extendBatchOfSummarizedUpdates: (
  179. project_id,
  180. existingSummarizedUpdates,
  181. before, desiredLength,
  182. callback = (error, summarizedUpdates, endOfDatabase) ->
  183. ) ->
  184. UpdatesManager.getProjectUpdatesWithUserInfo project_id, { before: before, limit: 3 * desiredLength }, (error, updates) ->
  185. return callback(error) if error?
  186. # Suppose in this request we have fetch the solid updates. In the next request we need
  187. # to fetch the dotted updates. These are defined by having an end timestamp less than
  188. # the last update's end timestamp (updates are ordered by descending end_ts). I.e.
  189. # start_ts--v v--end_ts
  190. # doc1: |......| |...| |-------|
  191. # doc2: |------------------|
  192. # ^----- Next time, fetch all updates with an
  193. # end_ts less than this
  194. #
  195. if updates? and updates.length > 0
  196. nextBeforeTimestamp = updates[updates.length - 1].meta.end_ts
  197. if nextBeforeTimestamp >= before
  198. error = new Error("history order is broken")
  199. logger.error err: error, project_id:project_id, nextBeforeTimestamp: nextBeforeTimestamp, before:before, "error in project history"
  200. return callback(error)
  201. else
  202. nextBeforeTimestamp = null
  203. summarizedUpdates = UpdatesManager._summarizeUpdates(
  204. updates, existingSummarizedUpdates
  205. )
  206. callback null,
  207. summarizedUpdates,
  208. nextBeforeTimestamp
  209. fillUserInfo: (updates, callback = (error, updates) ->) ->
  210. users = {}
  211. for update in updates
  212. if UpdatesManager._validUserId(update.meta.user_id)
  213. users[update.meta.user_id] = true
  214. jobs = []
  215. for user_id of users
  216. do (user_id) ->
  217. jobs.push (callback) ->
  218. WebApiManager.getUserInfo user_id, (error, userInfo) ->
  219. return callback(error) if error?
  220. users[user_id] = userInfo
  221. callback()
  222. async.series jobs, (error) ->
  223. return callback(error) if error?
  224. for update in updates
  225. user_id = update.meta.user_id
  226. delete update.meta.user_id
  227. if UpdatesManager._validUserId(user_id)
  228. update.meta.user = users[user_id]
  229. callback null, updates
  230. _validUserId: (user_id) ->
  231. if !user_id?
  232. return false
  233. else
  234. return !!user_id.match(/^[a-f0-9]{24}$/)
  235. TIME_BETWEEN_DISTINCT_UPDATES: fiveMinutes = 5 * 60 * 1000
  236. _summarizeUpdates: (updates, existingSummarizedUpdates = []) ->
  237. summarizedUpdates = existingSummarizedUpdates.slice()
  238. for update in updates
  239. earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  240. if earliestUpdate and earliestUpdate.meta.start_ts - update.meta.end_ts < @TIME_BETWEEN_DISTINCT_UPDATES
  241. # check if the user in this update is already present in the earliest update,
  242. # if not, add them to the users list of the earliest update
  243. userExists = false
  244. for user in earliestUpdate.meta.users
  245. if (!user and !update.meta.user) or (user?.id == update.meta.user?.id)
  246. userExists = true
  247. break
  248. if !userExists
  249. earliestUpdate.meta.users.push update.meta.user
  250. doc_id = update.doc_id.toString()
  251. doc = earliestUpdate.docs[doc_id]
  252. if doc?
  253. doc.fromV = Math.min(doc.fromV, update.v)
  254. doc.toV = Math.max(doc.toV, update.v)
  255. else
  256. earliestUpdate.docs[doc_id] =
  257. fromV: update.v
  258. toV: update.v
  259. earliestUpdate.meta.start_ts = Math.min(earliestUpdate.meta.start_ts, update.meta.start_ts)
  260. earliestUpdate.meta.end_ts = Math.max(earliestUpdate.meta.end_ts, update.meta.end_ts)
  261. else
  262. newUpdate =
  263. meta:
  264. users: []
  265. start_ts: update.meta.start_ts
  266. end_ts: update.meta.end_ts
  267. docs: {}
  268. newUpdate.docs[update.doc_id.toString()] =
  269. fromV: update.v
  270. toV: update.v
  271. newUpdate.meta.users.push update.meta.user
  272. summarizedUpdates.push newUpdate
  273. return summarizedUpdates