PackManager.coffee 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. async = require "async"
  2. _ = require "underscore"
  3. {db, ObjectId, BSON} = require "./mongojs"
  4. logger = require "logger-sharelatex"
  5. LockManager = require "./LockManager"
  6. MongoAWS = require "./MongoAWS"
  7. ProjectIterator = require "./ProjectIterator"
  8. # Sharejs operations are stored in a 'pack' object
  9. #
  10. # e.g. a single sharejs update looks like
  11. #
  12. # {
  13. # "doc_id" : 549dae9e0a2a615c0c7f0c98,
  14. # "project_id" : 549dae9c0a2a615c0c7f0c8c,
  15. # "op" : [ {"p" : 6981, "d" : "?" } ],
  16. # "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  17. # "v" : 17082
  18. # }
  19. #
  20. # and a pack looks like this
  21. #
  22. # {
  23. # "doc_id" : 549dae9e0a2a615c0c7f0c98,
  24. # "project_id" : 549dae9c0a2a615c0c7f0c8c,
  25. # "pack" : [ U1, U2, U3, ...., UN],
  26. # "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  27. # "v" : 17082
  28. # "v_end" : ...
  29. # }
  30. #
  31. # where U1, U2, U3, .... are single updates stripped of their
  32. # doc_id and project_id fields (which are the same for all the
  33. # updates in the pack).
  34. #
  35. # The pack itself has v and meta fields, this makes it possible to
  36. # treat packs and single updates in a similar way.
  37. #
  38. # The v field of the pack itself is from the first entry U1, the
  39. # v_end field from UN. The meta.end_ts field of the pack itself is
  40. # from the last entry UN, the meta.start_ts field from U1.
  41. DAYS = 24 * 3600 * 1000 # one day in milliseconds
  42. module.exports = PackManager =
  43. MAX_SIZE: 1024*1024 # make these configurable parameters
  44. MAX_COUNT: 1024
  45. insertCompressedUpdates: (project_id, doc_id, lastUpdate, newUpdates, temporary, callback = (error) ->) ->
  46. return callback() if newUpdates.length == 0
  47. updatesToFlush = []
  48. updatesRemaining = newUpdates.slice()
  49. n = lastUpdate?.n || 0
  50. sz = lastUpdate?.sz || 0
  51. while updatesRemaining.length and n < PackManager.MAX_COUNT and sz < PackManager.MAX_SIZE
  52. nextUpdate = updatesRemaining[0]
  53. nextUpdateSize = BSON.calculateObjectSize(nextUpdate)
  54. if nextUpdateSize + sz > PackManager.MAX_SIZE and n > 0
  55. break
  56. n++
  57. sz += nextUpdateSize
  58. updatesToFlush.push updatesRemaining.shift()
  59. PackManager.flushCompressedUpdates project_id, doc_id, lastUpdate, updatesToFlush, temporary, (error) ->
  60. return callback(error) if error?
  61. PackManager.insertCompressedUpdates project_id, doc_id, null, updatesRemaining, temporary, callback
  62. flushCompressedUpdates: (project_id, doc_id, lastUpdate, newUpdates, temporary, callback = (error) ->) ->
  63. return callback() if newUpdates.length == 0
  64. if lastUpdate? and not (temporary and ((Date.now() - lastUpdate.meta?.start_ts) > 1 * DAYS))
  65. PackManager.appendUpdatesToExistingPack project_id, doc_id, lastUpdate, newUpdates, temporary, callback
  66. else
  67. PackManager.insertUpdatesIntoNewPack project_id, doc_id, newUpdates, temporary, callback
  68. insertUpdatesIntoNewPack: (project_id, doc_id, newUpdates, temporary, callback = (error) ->) ->
  69. first = newUpdates[0]
  70. last = newUpdates[newUpdates.length - 1]
  71. n = newUpdates.length
  72. sz = BSON.calculateObjectSize(newUpdates)
  73. newPack =
  74. project_id: ObjectId(project_id.toString())
  75. doc_id: ObjectId(doc_id.toString())
  76. pack: newUpdates
  77. n: n
  78. sz: sz
  79. meta:
  80. start_ts: first.meta.start_ts
  81. end_ts: last.meta.end_ts
  82. v: first.v
  83. v_end: last.v
  84. if temporary
  85. newPack.expiresAt = new Date(Date.now() + 7 * DAYS)
  86. logger.log {project_id, doc_id, newUpdates}, "inserting updates into new pack"
  87. db.docHistory.save newPack, (err, result) ->
  88. return callback(err) if err?
  89. if temporary
  90. return callback()
  91. else
  92. PackManager.updateIndex project_id, doc_id, callback
  93. appendUpdatesToExistingPack: (project_id, doc_id, lastUpdate, newUpdates, temporary, callback = (error) ->) ->
  94. first = newUpdates[0]
  95. last = newUpdates[newUpdates.length - 1]
  96. n = newUpdates.length
  97. sz = BSON.calculateObjectSize(newUpdates)
  98. query =
  99. _id: lastUpdate._id
  100. project_id: ObjectId(project_id.toString())
  101. doc_id: ObjectId(doc_id.toString())
  102. pack: {$exists: true}
  103. update =
  104. $push:
  105. "pack": {$each: newUpdates}
  106. $inc:
  107. "n": n
  108. "sz": sz
  109. $set:
  110. "meta.end_ts": last.meta.end_ts
  111. "v_end": last.v
  112. if lastUpdate.expiresAt and temporary
  113. update.$set.expiresAt = new Date(Date.now() + 7 * DAYS)
  114. logger.log {project_id, doc_id, lastUpdate, newUpdates}, "appending updates to existing pack"
  115. db.docHistory.findAndModify {query, update, new:true, fields:{meta:1,v_end:1}}, callback
  116. # Retrieve all changes for a document
  117. getOpsByVersionRange: (project_id, doc_id, fromVersion, toVersion, callback = (error, updates) ->) ->
  118. PackManager.loadPacksByVersionRange project_id, doc_id, fromVersion, toVersion, (error) ->
  119. query = {doc_id:ObjectId(doc_id.toString())}
  120. query.v = {$lte:toVersion} if toVersion?
  121. query.v_end = {$gte:fromVersion} if fromVersion?
  122. #console.log "query:", query
  123. db.docHistory.find(query).sort {v:-1}, (err, result) ->
  124. return callback(err) if err?
  125. #console.log "getOpsByVersionRange:", err, result
  126. updates = []
  127. opInRange = (op, from, to) ->
  128. return false if fromVersion? and op.v < fromVersion
  129. return false if toVersion? and op.v > toVersion
  130. return true
  131. for docHistory in result
  132. #console.log 'adding', docHistory.pack
  133. for op in docHistory.pack.reverse() when opInRange(op, fromVersion, toVersion)
  134. op.project_id = docHistory.project_id
  135. op.doc_id = docHistory.doc_id
  136. #console.log "added op", op.v, fromVersion, toVersion
  137. updates.push op
  138. callback(null, updates)
  139. loadPacksByVersionRange: (project_id, doc_id, fromVersion, toVersion, callback) ->
  140. PackManager.getIndex doc_id, (err, indexResult) ->
  141. return callback(err) if err?
  142. indexPacks = indexResult?.packs or []
  143. packInRange = (pack, from, to) ->
  144. return false if fromVersion? and pack.v_end < fromVersion
  145. return false if toVersion? and pack.v > toVersion
  146. return true
  147. neededIds = (pack._id for pack in indexPacks when packInRange(pack, fromVersion, toVersion))
  148. PackManager.fetchPacksIfNeeded project_id, doc_id, neededIds, callback
  149. fetchPacksIfNeeded: (project_id, doc_id, pack_ids, callback) ->
  150. db.docHistory.find {_id: {$in: (ObjectId(id) for id in pack_ids)}}, {_id:1}, (err, loadedPacks) ->
  151. return callback(err) if err?
  152. allPackIds = (id.toString() for id in pack_ids)
  153. loadedPackIds = (pack._id.toString() for pack in loadedPacks)
  154. packIdsToFetch = _.difference allPackIds, loadedPackIds
  155. logger.log {loadedPackIds, allPackIds, packIdsToFetch}, "analysed packs"
  156. async.eachLimit packIdsToFetch, 4, (pack_id, cb) ->
  157. MongoAWS.unArchivePack project_id, doc_id, pack_id, cb
  158. , (err) ->
  159. return callback(err) if err?
  160. logger.log "done unarchiving"
  161. callback()
  162. # Retrieve all changes across a project
  163. makeProjectIterator: (project_id, before, callback) ->
  164. # get all the docHistory Entries
  165. db.docHistory.find({project_id: ObjectId(project_id)},{pack:false}).sort {"meta.end_ts":-1}, (err, packs) ->
  166. return callback(err) if err?
  167. allPacks = []
  168. seenIds = {}
  169. for pack in packs
  170. allPacks.push pack
  171. seenIds[pack._id] = true
  172. db.docHistoryIndex.find {project_id: ObjectId(project_id)}, (err, indexes) ->
  173. return callback(err) if err?
  174. for index in indexes
  175. for pack in index.packs when not seenIds[pack._id]
  176. pack.project_id = index.project_id
  177. pack.doc_id = index._id
  178. pack.fromIndex = true
  179. allPacks.push pack
  180. seenIds[pack._id] = true
  181. callback(null, new ProjectIterator(allPacks, before, PackManager.getPackById))
  182. getPackById: (project_id, doc_id, pack_id, callback) ->
  183. db.docHistory.findOne {_id: pack_id}, (err, pack) ->
  184. return callback(err) if err?
  185. if not pack?
  186. MongoAWS.unArchivePack project_id, doc_id, pack_id, callback
  187. else if pack.expiresAt?
  188. # we only need to touch the TTL on the listing of changes in the project
  189. # because diffs on individual documents are always done after that
  190. PackManager.increaseTTL pack, callback
  191. else
  192. callback(null, pack)
  193. increaseTTL: (pack, callback) ->
  194. if pack.expiresAt < new Date(Date.now() + 6 * DAYS)
  195. # update cache expiry since we are using this pack
  196. db.docHistory.findAndModify {
  197. query: {_id: pack._id}
  198. update: {$set: {expiresAt: new Date(Date.now() + 7 * DAYS)}}
  199. }, (err) ->
  200. return callback(err, pack)
  201. else
  202. callback(null, pack)
  203. # Manage docHistoryIndex collection
  204. getIndex: (doc_id, callback) ->
  205. db.docHistoryIndex.findOne {_id:ObjectId(doc_id.toString())}, callback
  206. getPackFromIndex: (doc_id, pack_id, callback) ->
  207. db.docHistoryIndex.findOne {_id:ObjectId(doc_id.toString()), "packs._id": pack_id}, {"packs.$":1}, callback
  208. getLastPackFromIndex: (doc_id, callback) ->
  209. db.docHistoryIndex.findOne {_id: ObjectId(doc_id.toString())}, {packs:{$slice:-1}}, (err, indexPack) ->
  210. return callback(err) if err?
  211. return callback() if not indexPack?
  212. callback(null,indexPack[0])
  213. getIndexWithKeys: (doc_id, callback) ->
  214. PackManager.getIndex doc_id, (err, index) ->
  215. return callback(err) if err?
  216. return callback() if not index?
  217. for pack in index?.packs or []
  218. index[pack._id] = pack
  219. callback(null, index)
  220. initialiseIndex: (project_id, doc_id, callback) ->
  221. PackManager.findCompletedPacks project_id, doc_id, (err, packs) ->
  222. #console.log 'err', err, 'packs', packs, packs?.length
  223. return callback(err) if err?
  224. return callback() if not packs?
  225. PackManager.insertPacksIntoIndexWithLock project_id, doc_id, packs, callback
  226. updateIndex: (project_id, doc_id, callback) ->
  227. # find all packs prior to current pack
  228. PackManager.findUnindexedPacks project_id, doc_id, (err, newPacks) ->
  229. return callback(err) if err?
  230. return callback() if not newPacks?
  231. PackManager.insertPacksIntoIndexWithLock project_id, doc_id, newPacks, (err) ->
  232. return callback(err) if err?
  233. logger.log {project_id, doc_id, newPacks}, "added new packs to index"
  234. callback()
  235. findCompletedPacks: (project_id, doc_id, callback) ->
  236. query = { doc_id: ObjectId(doc_id.toString()) }
  237. db.docHistory.find(query, {pack:false}).sort {v:1}, (err, packs) ->
  238. return callback(err) if err?
  239. return callback() if not packs?
  240. return callback() if packs?.length <= 1
  241. packs.pop() # discard the last pack, it's still in progress
  242. callback(null, packs)
  243. # findPacks: (project_id, doc_id, queryFilter, callback) ->
  244. # query = { doc_id: ObjectId(doc_id.toString()) }
  245. # query = _.defaults query, queryFilter if queryFilter?
  246. # db.docHistory.find(query, {pack:false}).sort {v:1}, callback
  247. findUnindexedPacks: (project_id, doc_id, callback) ->
  248. PackManager.getIndexWithKeys doc_id, (err, indexResult) ->
  249. return callback(err) if err?
  250. PackManager.findCompletedPacks project_id, doc_id, (err, historyPacks) ->
  251. return callback(err) if err?
  252. return callback() if not historyPacks?
  253. # select only the new packs not already in the index
  254. newPacks = (pack for pack in historyPacks when not indexResult?[pack._id]?)
  255. newPacks = (_.omit(pack, 'doc_id', 'project_id', 'n', 'sz') for pack in newPacks)
  256. logger.log {project_id, doc_id, n: newPacks.length}, "found new packs"
  257. callback(null, newPacks)
  258. insertPacksIntoIndexWithLock: (project_id, doc_id, newPacks, callback) ->
  259. LockManager.runWithLock(
  260. "HistoryIndexLock:#{doc_id}",
  261. (releaseLock) ->
  262. PackManager._insertPacksIntoIndex project_id, doc_id, newPacks, releaseLock
  263. callback
  264. )
  265. _insertPacksIntoIndex: (project_id, doc_id, newPacks, callback) ->
  266. db.docHistoryIndex.findAndModify {
  267. query: {_id:ObjectId(doc_id.toString())}
  268. update:
  269. $setOnInsert: project_id: ObjectId(project_id.toString())
  270. $push:
  271. packs: {$each: newPacks, $sort: {v: 1}}
  272. upsert: true
  273. }, callback
  274. # Archiving packs to S3
  275. archivePack: (project_id, doc_id, pack_id, callback) ->
  276. clearFlagOnError = (err, cb) ->
  277. if err? # clear the inS3 flag on error
  278. PackManager.clearPackAsArchiveInProgress project_id, doc_id, pack_id, (err2) ->
  279. return cb(err2) if err2?
  280. return cb(err)
  281. else
  282. cb()
  283. async.series [
  284. (cb) ->
  285. PackManager.checkArchiveNotInProgress project_id, doc_id, pack_id, cb
  286. (cb) ->
  287. PackManager.markPackAsArchiveInProgress project_id, doc_id, pack_id, cb
  288. (cb) ->
  289. MongoAWS.archivePack project_id, doc_id, pack_id, (err) ->
  290. clearFlagOnError(err, cb)
  291. (cb) ->
  292. PackManager.checkArchivedPack project_id, doc_id, pack_id, (err) ->
  293. clearFlagOnError(err, cb)
  294. (cb) ->
  295. PackManager.markPackAsArchived project_id, doc_id, pack_id, cb
  296. (cb) ->
  297. PackManager.setTTLOnArchivedPack project_id, doc_id, pack_id, callback
  298. ], callback
  299. checkArchivedPack: (project_id, doc_id, pack_id, callback) ->
  300. db.docHistory.findOne {_id: pack_id}, (err, pack) ->
  301. return callback(err) if err?
  302. return callback new Error("pack not found") if not pack?
  303. MongoAWS.readArchivedPack project_id, doc_id, pack_id, (err, result) ->
  304. delete result.last_checked
  305. delete pack.last_checked
  306. # need to compare ids as ObjectIds with .equals()
  307. for key in ['_id', 'project_id', 'doc_id']
  308. result[key] = pack[key] if result[key].equals(pack[key])
  309. for op, i in result.pack
  310. op._id = pack.pack[i]._id if op._id? and op._id.equals(pack.pack[i]._id)
  311. if _.isEqual pack, result
  312. callback()
  313. else
  314. logger.err {pack, result, jsondiff: JSON.stringify(pack) is JSON.stringify(result)}, "difference when comparing packs"
  315. callback new Error("pack retrieved from s3 does not match pack in mongo")
  316. # Processing old packs via worker
  317. processOldPack: (project_id, doc_id, pack_id, callback) ->
  318. markAsChecked = (err) ->
  319. PackManager.markPackAsChecked project_id, doc_id, pack_id, (err2) ->
  320. return callback(err2) if err2?
  321. callback(err)
  322. logger.log {project_id, doc_id}, "processing old packs"
  323. db.docHistory.findOne {_id:pack_id}, (err, pack) ->
  324. return markAsChecked(err) if err?
  325. return markAsChecked() if not pack?
  326. return callback() if pack.expiresAt? # return directly
  327. PackManager.updateIndexIfNeeded project_id, doc_id, (err) ->
  328. return markAsChecked(err) if err?
  329. PackManager.findUnarchivedPacks project_id, doc_id, (err, unarchivedPacks) ->
  330. return markAsChecked(err) if err?
  331. if not unarchivedPacks?.length
  332. logger.log "no packs need archiving"
  333. return markAsChecked()
  334. async.eachSeries unarchivedPacks, (pack, cb) ->
  335. PackManager.archivePack project_id, doc_id, pack._id, cb
  336. , (err) ->
  337. return markAsChecked(err) if err?
  338. logger.log "done processing"
  339. markAsChecked()
  340. updateIndexIfNeeded: (project_id, doc_id, callback) ->
  341. logger.log {project_id, doc_id}, "archiving old packs"
  342. PackManager.getIndexWithKeys doc_id, (err, index) ->
  343. return callback(err) if err?
  344. if not index?
  345. PackManager.initialiseIndex project_id, doc_id, callback
  346. else
  347. PackManager.updateIndex project_id, doc_id, callback
  348. markPackAsChecked: (project_id, doc_id, pack_id, callback) ->
  349. logger.log {project_id, doc_id, pack_id}, "marking pack as checked"
  350. db.docHistory.findAndModify {
  351. query: {_id: pack_id}
  352. update: {$currentDate: {"last_checked":true}}
  353. }, callback
  354. findUnarchivedPacks: (project_id, doc_id, callback) ->
  355. PackManager.getIndex doc_id, (err, indexResult) ->
  356. return callback(err) if err?
  357. indexPacks = indexResult?.packs or []
  358. unArchivedPacks = (pack for pack in indexPacks when not pack.inS3?)
  359. logger.log {project_id, doc_id, n: unArchivedPacks.length}, "find unarchived packs"
  360. callback(null, unArchivedPacks)
  361. # Archive locking flags
  362. checkArchiveNotInProgress: (project_id, doc_id, pack_id, callback) ->
  363. logger.log {project_id, doc_id, pack_id}, "checking if archive in progress"
  364. PackManager.getPackFromIndex doc_id, pack_id, (err, result) ->
  365. return callback(err) if err?
  366. return callback new Error("pack not found in index") if not result?
  367. if result.inS3?
  368. callback new Error("pack archiving already in progress")
  369. else
  370. callback()
  371. markPackAsArchiveInProgress: (project_id, doc_id, pack_id, callback) ->
  372. logger.log {project_id, doc_id}, "marking pack as archive in progress status"
  373. db.docHistoryIndex.findAndModify {
  374. query: {_id:ObjectId(doc_id.toString()), packs: {$elemMatch: {"_id": pack_id, inS3: {$exists:false}}}}
  375. fields: { "packs.$": 1 }
  376. update: {$set: {"packs.$.inS3":false}}
  377. }, (err, result) ->
  378. return callback(err) if err?
  379. return callback new Error("archive is already in progress") if not result?
  380. logger.log {project_id, doc_id, pack_id}, "marked as archive in progress"
  381. callback()
  382. clearPackAsArchiveInProgress: (project_id, doc_id, pack_id, callback) ->
  383. logger.log {project_id, doc_id, pack_id}, "clearing as archive in progress"
  384. db.docHistoryIndex.findAndModify {
  385. query: {_id:ObjectId(doc_id.toString()), "packs" : {$elemMatch: {"_id": pack_id, inS3: false}}}
  386. fields: { "packs.$": 1 }
  387. update: {$unset: {"packs.$.inS3":true}}
  388. }, callback
  389. markPackAsArchived: (project_id, doc_id, pack_id, callback) ->
  390. logger.log {project_id, doc_id, pack_id}, "marking pack as archived"
  391. db.docHistoryIndex.findAndModify {
  392. query: {_id:ObjectId(doc_id.toString()), "packs" : {$elemMatch: {"_id": pack_id, inS3: false}}}
  393. fields: { "packs.$": 1 }
  394. update: {$set: {"packs.$.inS3":true}}
  395. }, (err, result) ->
  396. return callback(err) if err?
  397. return callback new Error("archive is not marked as progress") if not result?
  398. logger.log {project_id, doc_id, pack_id}, "marked as archived"
  399. callback()
  400. setTTLOnArchivedPack: (project_id, doc_id, pack_id, callback) ->
  401. db.docHistory.findAndModify {
  402. query: {_id: pack_id}
  403. update: {$set: {expiresAt: new Date(Date.now() + 1*DAYS)}}
  404. }, (err) ->
  405. logger.log {project_id, doc_id, pack_id}, "set expiry on pack"
  406. callback()