PackManager.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS205: Consider reworking code to avoid use of IIFEs
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let PackManager
  17. const async = require('async')
  18. const _ = require('underscore')
  19. const { db, ObjectId, BSON } = require('./mongojs')
  20. const logger = require('logger-sharelatex')
  21. const LockManager = require('./LockManager')
  22. const MongoAWS = require('./MongoAWS')
  23. const Metrics = require('metrics-sharelatex')
  24. const ProjectIterator = require('./ProjectIterator')
  25. const Settings = require('settings-sharelatex')
  26. const keys = Settings.redis.lock.key_schema
  27. // Sharejs operations are stored in a 'pack' object
  28. //
  29. // e.g. a single sharejs update looks like
  30. //
  31. // {
  32. // "doc_id" : 549dae9e0a2a615c0c7f0c98,
  33. // "project_id" : 549dae9c0a2a615c0c7f0c8c,
  34. // "op" : [ {"p" : 6981, "d" : "?" } ],
  35. // "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  36. // "v" : 17082
  37. // }
  38. //
  39. // and a pack looks like this
  40. //
  41. // {
  42. // "doc_id" : 549dae9e0a2a615c0c7f0c98,
  43. // "project_id" : 549dae9c0a2a615c0c7f0c8c,
  44. // "pack" : [ U1, U2, U3, ...., UN],
  45. // "meta" : { "user_id" : 52933..., "start_ts" : 1422310693931, "end_ts" : 1422310693931 },
  46. // "v" : 17082
  47. // "v_end" : ...
  48. // }
  49. //
  50. // where U1, U2, U3, .... are single updates stripped of their
  51. // doc_id and project_id fields (which are the same for all the
  52. // updates in the pack).
  53. //
  54. // The pack itself has v and meta fields, this makes it possible to
  55. // treat packs and single updates in a similar way.
  56. //
  57. // The v field of the pack itself is from the first entry U1, the
  58. // v_end field from UN. The meta.end_ts field of the pack itself is
  59. // from the last entry UN, the meta.start_ts field from U1.
  60. const DAYS = 24 * 3600 * 1000 // one day in milliseconds
  61. module.exports = PackManager = {
  62. MAX_SIZE: 1024 * 1024, // make these configurable parameters
  63. MAX_COUNT: 1024,
  64. insertCompressedUpdates(
  65. project_id,
  66. doc_id,
  67. lastUpdate,
  68. newUpdates,
  69. temporary,
  70. callback
  71. ) {
  72. if (callback == null) {
  73. callback = function (error) {}
  74. }
  75. if (newUpdates.length === 0) {
  76. return callback()
  77. }
  78. // never append permanent ops to a pack that will expire
  79. if (
  80. (lastUpdate != null ? lastUpdate.expiresAt : undefined) != null &&
  81. !temporary
  82. ) {
  83. lastUpdate = null
  84. }
  85. const updatesToFlush = []
  86. const updatesRemaining = newUpdates.slice()
  87. let n = (lastUpdate != null ? lastUpdate.n : undefined) || 0
  88. let sz = (lastUpdate != null ? lastUpdate.sz : undefined) || 0
  89. while (
  90. updatesRemaining.length &&
  91. n < PackManager.MAX_COUNT &&
  92. sz < PackManager.MAX_SIZE
  93. ) {
  94. const nextUpdate = updatesRemaining[0]
  95. const nextUpdateSize = BSON.calculateObjectSize(nextUpdate)
  96. if (nextUpdateSize + sz > PackManager.MAX_SIZE && n > 0) {
  97. break
  98. }
  99. n++
  100. sz += nextUpdateSize
  101. updatesToFlush.push(updatesRemaining.shift())
  102. }
  103. return PackManager.flushCompressedUpdates(
  104. project_id,
  105. doc_id,
  106. lastUpdate,
  107. updatesToFlush,
  108. temporary,
  109. function (error) {
  110. if (error != null) {
  111. return callback(error)
  112. }
  113. return PackManager.insertCompressedUpdates(
  114. project_id,
  115. doc_id,
  116. null,
  117. updatesRemaining,
  118. temporary,
  119. callback
  120. )
  121. }
  122. )
  123. },
  124. flushCompressedUpdates(
  125. project_id,
  126. doc_id,
  127. lastUpdate,
  128. newUpdates,
  129. temporary,
  130. callback
  131. ) {
  132. if (callback == null) {
  133. callback = function (error) {}
  134. }
  135. if (newUpdates.length === 0) {
  136. return callback()
  137. }
  138. let canAppend = false
  139. // check if it is safe to append to an existing pack
  140. if (lastUpdate != null) {
  141. if (!temporary && lastUpdate.expiresAt == null) {
  142. // permanent pack appends to permanent pack
  143. canAppend = true
  144. }
  145. const age =
  146. Date.now() -
  147. (lastUpdate.meta != null ? lastUpdate.meta.start_ts : undefined)
  148. if (temporary && lastUpdate.expiresAt != null && age < 1 * DAYS) {
  149. // temporary pack appends to temporary pack if same day
  150. canAppend = true
  151. }
  152. }
  153. if (canAppend) {
  154. return PackManager.appendUpdatesToExistingPack(
  155. project_id,
  156. doc_id,
  157. lastUpdate,
  158. newUpdates,
  159. temporary,
  160. callback
  161. )
  162. } else {
  163. return PackManager.insertUpdatesIntoNewPack(
  164. project_id,
  165. doc_id,
  166. newUpdates,
  167. temporary,
  168. callback
  169. )
  170. }
  171. },
  172. insertUpdatesIntoNewPack(
  173. project_id,
  174. doc_id,
  175. newUpdates,
  176. temporary,
  177. callback
  178. ) {
  179. if (callback == null) {
  180. callback = function (error) {}
  181. }
  182. const first = newUpdates[0]
  183. const last = newUpdates[newUpdates.length - 1]
  184. const n = newUpdates.length
  185. const sz = BSON.calculateObjectSize(newUpdates)
  186. const newPack = {
  187. project_id: ObjectId(project_id.toString()),
  188. doc_id: ObjectId(doc_id.toString()),
  189. pack: newUpdates,
  190. n,
  191. sz,
  192. meta: {
  193. start_ts: first.meta.start_ts,
  194. end_ts: last.meta.end_ts
  195. },
  196. v: first.v,
  197. v_end: last.v,
  198. temporary
  199. }
  200. if (temporary) {
  201. newPack.expiresAt = new Date(Date.now() + 7 * DAYS)
  202. newPack.last_checked = new Date(Date.now() + 30 * DAYS) // never check temporary packs
  203. }
  204. logger.log(
  205. { project_id, doc_id, newUpdates },
  206. 'inserting updates into new pack'
  207. )
  208. return db.docHistory.save(newPack, function (err, result) {
  209. if (err != null) {
  210. return callback(err)
  211. }
  212. Metrics.inc(`insert-pack-${temporary ? 'temporary' : 'permanent'}`)
  213. if (temporary) {
  214. return callback()
  215. } else {
  216. return PackManager.updateIndex(project_id, doc_id, callback)
  217. }
  218. })
  219. },
  220. appendUpdatesToExistingPack(
  221. project_id,
  222. doc_id,
  223. lastUpdate,
  224. newUpdates,
  225. temporary,
  226. callback
  227. ) {
  228. if (callback == null) {
  229. callback = function (error) {}
  230. }
  231. const first = newUpdates[0]
  232. const last = newUpdates[newUpdates.length - 1]
  233. const n = newUpdates.length
  234. const sz = BSON.calculateObjectSize(newUpdates)
  235. const query = {
  236. _id: lastUpdate._id,
  237. project_id: ObjectId(project_id.toString()),
  238. doc_id: ObjectId(doc_id.toString()),
  239. pack: { $exists: true }
  240. }
  241. const update = {
  242. $push: {
  243. pack: { $each: newUpdates }
  244. },
  245. $inc: {
  246. n: n,
  247. sz: sz
  248. },
  249. $set: {
  250. 'meta.end_ts': last.meta.end_ts,
  251. v_end: last.v
  252. }
  253. }
  254. if (lastUpdate.expiresAt && temporary) {
  255. update.$set.expiresAt = new Date(Date.now() + 7 * DAYS)
  256. }
  257. logger.log(
  258. { project_id, doc_id, lastUpdate, newUpdates },
  259. 'appending updates to existing pack'
  260. )
  261. Metrics.inc(`append-pack-${temporary ? 'temporary' : 'permanent'}`)
  262. return db.docHistory.findAndModify(
  263. { query, update, new: true, fields: { meta: 1, v_end: 1 } },
  264. callback
  265. )
  266. },
  267. // Retrieve all changes for a document
  268. getOpsByVersionRange(project_id, doc_id, fromVersion, toVersion, callback) {
  269. if (callback == null) {
  270. callback = function (error, updates) {}
  271. }
  272. return PackManager.loadPacksByVersionRange(
  273. project_id,
  274. doc_id,
  275. fromVersion,
  276. toVersion,
  277. function (error) {
  278. const query = { doc_id: ObjectId(doc_id.toString()) }
  279. if (toVersion != null) {
  280. query.v = { $lte: toVersion }
  281. }
  282. if (fromVersion != null) {
  283. query.v_end = { $gte: fromVersion }
  284. }
  285. // console.log "query:", query
  286. return db.docHistory
  287. .find(query)
  288. .sort({ v: -1 })
  289. .toArray(function (err, result) {
  290. if (err != null) {
  291. return callback(err)
  292. }
  293. // console.log "getOpsByVersionRange:", err, result
  294. const updates = []
  295. const opInRange = function (op, from, to) {
  296. if (fromVersion != null && op.v < fromVersion) {
  297. return false
  298. }
  299. if (toVersion != null && op.v > toVersion) {
  300. return false
  301. }
  302. return true
  303. }
  304. for (const docHistory of Array.from(result)) {
  305. // console.log 'adding', docHistory.pack
  306. for (const op of Array.from(docHistory.pack.reverse())) {
  307. if (opInRange(op, fromVersion, toVersion)) {
  308. op.project_id = docHistory.project_id
  309. op.doc_id = docHistory.doc_id
  310. // console.log "added op", op.v, fromVersion, toVersion
  311. updates.push(op)
  312. }
  313. }
  314. }
  315. return callback(null, updates)
  316. })
  317. }
  318. )
  319. },
  320. loadPacksByVersionRange(
  321. project_id,
  322. doc_id,
  323. fromVersion,
  324. toVersion,
  325. callback
  326. ) {
  327. return PackManager.getIndex(doc_id, function (err, indexResult) {
  328. let pack
  329. if (err != null) {
  330. return callback(err)
  331. }
  332. const indexPacks =
  333. (indexResult != null ? indexResult.packs : undefined) || []
  334. const packInRange = function (pack, from, to) {
  335. if (fromVersion != null && pack.v_end < fromVersion) {
  336. return false
  337. }
  338. if (toVersion != null && pack.v > toVersion) {
  339. return false
  340. }
  341. return true
  342. }
  343. const neededIds = (() => {
  344. const result = []
  345. for (pack of Array.from(indexPacks)) {
  346. if (packInRange(pack, fromVersion, toVersion)) {
  347. result.push(pack._id)
  348. }
  349. }
  350. return result
  351. })()
  352. if (neededIds.length) {
  353. return PackManager.fetchPacksIfNeeded(
  354. project_id,
  355. doc_id,
  356. neededIds,
  357. callback
  358. )
  359. } else {
  360. return callback()
  361. }
  362. })
  363. },
  364. fetchPacksIfNeeded(project_id, doc_id, pack_ids, callback) {
  365. let id
  366. return db.docHistory
  367. .find({ _id: { $in: pack_ids.map(ObjectId) } }, { _id: 1 })
  368. .toArray(function (err, loadedPacks) {
  369. if (err != null) {
  370. return callback(err)
  371. }
  372. const allPackIds = (() => {
  373. const result1 = []
  374. for (id of Array.from(pack_ids)) {
  375. result1.push(id.toString())
  376. }
  377. return result1
  378. })()
  379. const loadedPackIds = Array.from(loadedPacks).map((pack) =>
  380. pack._id.toString()
  381. )
  382. const packIdsToFetch = _.difference(allPackIds, loadedPackIds)
  383. logger.log(
  384. { project_id, doc_id, loadedPackIds, allPackIds, packIdsToFetch },
  385. 'analysed packs'
  386. )
  387. if (packIdsToFetch.length === 0) {
  388. return callback()
  389. }
  390. return async.eachLimit(
  391. packIdsToFetch,
  392. 4,
  393. (pack_id, cb) =>
  394. MongoAWS.unArchivePack(project_id, doc_id, pack_id, cb),
  395. function (err) {
  396. if (err != null) {
  397. return callback(err)
  398. }
  399. logger.log({ project_id, doc_id }, 'done unarchiving')
  400. return callback()
  401. }
  402. )
  403. })
  404. },
  405. // Retrieve all changes across a project
  406. makeProjectIterator(project_id, before, callback) {
  407. // get all the docHistory Entries
  408. return db.docHistory
  409. .find({ project_id: ObjectId(project_id) }, { pack: false })
  410. .sort({ 'meta.end_ts': -1 })
  411. .toArray(function (err, packs) {
  412. let pack
  413. if (err != null) {
  414. return callback(err)
  415. }
  416. const allPacks = []
  417. const seenIds = {}
  418. for (pack of Array.from(packs)) {
  419. allPacks.push(pack)
  420. seenIds[pack._id] = true
  421. }
  422. return db.docHistoryIndex
  423. .find({ project_id: ObjectId(project_id) })
  424. .toArray(function (err, indexes) {
  425. if (err != null) {
  426. return callback(err)
  427. }
  428. for (const index of Array.from(indexes)) {
  429. for (pack of Array.from(index.packs)) {
  430. if (!seenIds[pack._id]) {
  431. pack.project_id = index.project_id
  432. pack.doc_id = index._id
  433. pack.fromIndex = true
  434. allPacks.push(pack)
  435. seenIds[pack._id] = true
  436. }
  437. }
  438. }
  439. return callback(
  440. null,
  441. new ProjectIterator(allPacks, before, PackManager.getPackById)
  442. )
  443. })
  444. })
  445. },
  446. getPackById(project_id, doc_id, pack_id, callback) {
  447. return db.docHistory.findOne({ _id: pack_id }, function (err, pack) {
  448. if (err != null) {
  449. return callback(err)
  450. }
  451. if (pack == null) {
  452. return MongoAWS.unArchivePack(project_id, doc_id, pack_id, callback)
  453. } else if (pack.expiresAt != null && pack.temporary === false) {
  454. // we only need to touch the TTL when listing the changes in the project
  455. // because diffs on individual documents are always done after that
  456. return PackManager.increaseTTL(pack, callback)
  457. // only do this for cached packs, not temporary ones to avoid older packs
  458. // being kept longer than newer ones (which messes up the last update version)
  459. } else {
  460. return callback(null, pack)
  461. }
  462. })
  463. },
  464. increaseTTL(pack, callback) {
  465. if (pack.expiresAt < new Date(Date.now() + 6 * DAYS)) {
  466. // update cache expiry since we are using this pack
  467. return db.docHistory.findAndModify(
  468. {
  469. query: { _id: pack._id },
  470. update: { $set: { expiresAt: new Date(Date.now() + 7 * DAYS) } }
  471. },
  472. (err) => callback(err, pack)
  473. )
  474. } else {
  475. return callback(null, pack)
  476. }
  477. },
  478. // Manage docHistoryIndex collection
  479. getIndex(doc_id, callback) {
  480. return db.docHistoryIndex.findOne(
  481. { _id: ObjectId(doc_id.toString()) },
  482. callback
  483. )
  484. },
  485. getPackFromIndex(doc_id, pack_id, callback) {
  486. return db.docHistoryIndex.findOne(
  487. { _id: ObjectId(doc_id.toString()), 'packs._id': pack_id },
  488. { 'packs.$': 1 },
  489. callback
  490. )
  491. },
  492. getLastPackFromIndex(doc_id, callback) {
  493. return db.docHistoryIndex.findOne(
  494. { _id: ObjectId(doc_id.toString()) },
  495. { packs: { $slice: -1 } },
  496. function (err, indexPack) {
  497. if (err != null) {
  498. return callback(err)
  499. }
  500. if (indexPack == null) {
  501. return callback()
  502. }
  503. return callback(null, indexPack[0])
  504. }
  505. )
  506. },
  507. getIndexWithKeys(doc_id, callback) {
  508. return PackManager.getIndex(doc_id, function (err, index) {
  509. if (err != null) {
  510. return callback(err)
  511. }
  512. if (index == null) {
  513. return callback()
  514. }
  515. for (const pack of Array.from(
  516. (index != null ? index.packs : undefined) || []
  517. )) {
  518. index[pack._id] = pack
  519. }
  520. return callback(null, index)
  521. })
  522. },
  523. initialiseIndex(project_id, doc_id, callback) {
  524. return PackManager.findCompletedPacks(project_id, doc_id, function (
  525. err,
  526. packs
  527. ) {
  528. // console.log 'err', err, 'packs', packs, packs?.length
  529. if (err != null) {
  530. return callback(err)
  531. }
  532. if (packs == null) {
  533. return callback()
  534. }
  535. return PackManager.insertPacksIntoIndexWithLock(
  536. project_id,
  537. doc_id,
  538. packs,
  539. callback
  540. )
  541. })
  542. },
  543. updateIndex(project_id, doc_id, callback) {
  544. // find all packs prior to current pack
  545. return PackManager.findUnindexedPacks(project_id, doc_id, function (
  546. err,
  547. newPacks
  548. ) {
  549. if (err != null) {
  550. return callback(err)
  551. }
  552. if (newPacks == null || newPacks.length === 0) {
  553. return callback()
  554. }
  555. return PackManager.insertPacksIntoIndexWithLock(
  556. project_id,
  557. doc_id,
  558. newPacks,
  559. function (err) {
  560. if (err != null) {
  561. return callback(err)
  562. }
  563. logger.log(
  564. { project_id, doc_id, newPacks },
  565. 'added new packs to index'
  566. )
  567. return callback()
  568. }
  569. )
  570. })
  571. },
  572. findCompletedPacks(project_id, doc_id, callback) {
  573. const query = {
  574. doc_id: ObjectId(doc_id.toString()),
  575. expiresAt: { $exists: false }
  576. }
  577. return db.docHistory
  578. .find(query, { pack: false })
  579. .sort({ v: 1 })
  580. .toArray(function (err, packs) {
  581. if (err != null) {
  582. return callback(err)
  583. }
  584. if (packs == null) {
  585. return callback()
  586. }
  587. if (!(packs != null ? packs.length : undefined)) {
  588. return callback()
  589. }
  590. const last = packs.pop() // discard the last pack, if it's still in progress
  591. if (last.finalised) {
  592. packs.push(last)
  593. } // it's finalised so we push it back to archive it
  594. return callback(null, packs)
  595. })
  596. },
  597. findPacks(project_id, doc_id, callback) {
  598. const query = {
  599. doc_id: ObjectId(doc_id.toString()),
  600. expiresAt: { $exists: false }
  601. }
  602. return db.docHistory
  603. .find(query, { pack: false })
  604. .sort({ v: 1 })
  605. .toArray(function (err, packs) {
  606. if (err != null) {
  607. return callback(err)
  608. }
  609. if (packs == null) {
  610. return callback()
  611. }
  612. if (!(packs != null ? packs.length : undefined)) {
  613. return callback()
  614. }
  615. return callback(null, packs)
  616. })
  617. },
  618. findUnindexedPacks(project_id, doc_id, callback) {
  619. return PackManager.getIndexWithKeys(doc_id, function (err, indexResult) {
  620. if (err != null) {
  621. return callback(err)
  622. }
  623. return PackManager.findCompletedPacks(project_id, doc_id, function (
  624. err,
  625. historyPacks
  626. ) {
  627. let pack
  628. if (err != null) {
  629. return callback(err)
  630. }
  631. if (historyPacks == null) {
  632. return callback()
  633. }
  634. // select only the new packs not already in the index
  635. let newPacks = (() => {
  636. const result = []
  637. for (pack of Array.from(historyPacks)) {
  638. if (
  639. (indexResult != null ? indexResult[pack._id] : undefined) == null
  640. ) {
  641. result.push(pack)
  642. }
  643. }
  644. return result
  645. })()
  646. newPacks = (() => {
  647. const result1 = []
  648. for (pack of Array.from(newPacks)) {
  649. result1.push(
  650. _.omit(
  651. pack,
  652. 'doc_id',
  653. 'project_id',
  654. 'n',
  655. 'sz',
  656. 'last_checked',
  657. 'finalised'
  658. )
  659. )
  660. }
  661. return result1
  662. })()
  663. if (newPacks.length) {
  664. logger.log(
  665. { project_id, doc_id, n: newPacks.length },
  666. 'found new packs'
  667. )
  668. }
  669. return callback(null, newPacks)
  670. })
  671. })
  672. },
  673. insertPacksIntoIndexWithLock(project_id, doc_id, newPacks, callback) {
  674. return LockManager.runWithLock(
  675. keys.historyIndexLock({ doc_id }),
  676. (releaseLock) =>
  677. PackManager._insertPacksIntoIndex(
  678. project_id,
  679. doc_id,
  680. newPacks,
  681. releaseLock
  682. ),
  683. callback
  684. )
  685. },
  686. _insertPacksIntoIndex(project_id, doc_id, newPacks, callback) {
  687. return db.docHistoryIndex.findAndModify(
  688. {
  689. query: { _id: ObjectId(doc_id.toString()) },
  690. update: {
  691. $setOnInsert: { project_id: ObjectId(project_id.toString()) },
  692. $push: {
  693. packs: { $each: newPacks, $sort: { v: 1 } }
  694. }
  695. },
  696. upsert: true
  697. },
  698. callback
  699. )
  700. },
  701. // Archiving packs to S3
  702. archivePack(project_id, doc_id, pack_id, callback) {
  703. const clearFlagOnError = function (err, cb) {
  704. if (err != null) {
  705. // clear the inS3 flag on error
  706. return PackManager.clearPackAsArchiveInProgress(
  707. project_id,
  708. doc_id,
  709. pack_id,
  710. function (err2) {
  711. if (err2 != null) {
  712. return cb(err2)
  713. }
  714. return cb(err)
  715. }
  716. )
  717. } else {
  718. return cb()
  719. }
  720. }
  721. return async.series(
  722. [
  723. (cb) =>
  724. PackManager.checkArchiveNotInProgress(
  725. project_id,
  726. doc_id,
  727. pack_id,
  728. cb
  729. ),
  730. (cb) =>
  731. PackManager.markPackAsArchiveInProgress(
  732. project_id,
  733. doc_id,
  734. pack_id,
  735. cb
  736. ),
  737. (cb) =>
  738. MongoAWS.archivePack(project_id, doc_id, pack_id, (err) =>
  739. clearFlagOnError(err, cb)
  740. ),
  741. (cb) =>
  742. PackManager.checkArchivedPack(project_id, doc_id, pack_id, (err) =>
  743. clearFlagOnError(err, cb)
  744. ),
  745. (cb) => PackManager.markPackAsArchived(project_id, doc_id, pack_id, cb),
  746. (cb) =>
  747. PackManager.setTTLOnArchivedPack(
  748. project_id,
  749. doc_id,
  750. pack_id,
  751. callback
  752. )
  753. ],
  754. callback
  755. )
  756. },
  757. checkArchivedPack(project_id, doc_id, pack_id, callback) {
  758. return db.docHistory.findOne({ _id: pack_id }, function (err, pack) {
  759. if (err != null) {
  760. return callback(err)
  761. }
  762. if (pack == null) {
  763. return callback(new Error('pack not found'))
  764. }
  765. return MongoAWS.readArchivedPack(project_id, doc_id, pack_id, function (
  766. err,
  767. result
  768. ) {
  769. delete result.last_checked
  770. delete pack.last_checked
  771. // need to compare ids as ObjectIds with .equals()
  772. for (const key of ['_id', 'project_id', 'doc_id']) {
  773. if (result[key].equals(pack[key])) {
  774. result[key] = pack[key]
  775. }
  776. }
  777. for (let i = 0; i < result.pack.length; i++) {
  778. const op = result.pack[i]
  779. if (op._id != null && op._id.equals(pack.pack[i]._id)) {
  780. op._id = pack.pack[i]._id
  781. }
  782. }
  783. if (_.isEqual(pack, result)) {
  784. return callback()
  785. } else {
  786. logger.err(
  787. {
  788. pack,
  789. result,
  790. jsondiff: JSON.stringify(pack) === JSON.stringify(result)
  791. },
  792. 'difference when comparing packs'
  793. )
  794. return callback(
  795. new Error('pack retrieved from s3 does not match pack in mongo')
  796. )
  797. }
  798. })
  799. })
  800. },
  801. // Extra methods to test archive/unarchive for a doc_id
  802. pushOldPacks(project_id, doc_id, callback) {
  803. return PackManager.findPacks(project_id, doc_id, function (err, packs) {
  804. if (err != null) {
  805. return callback(err)
  806. }
  807. if (!(packs != null ? packs.length : undefined)) {
  808. return callback()
  809. }
  810. return PackManager.processOldPack(
  811. project_id,
  812. doc_id,
  813. packs[0]._id,
  814. callback
  815. )
  816. })
  817. },
  818. pullOldPacks(project_id, doc_id, callback) {
  819. return PackManager.loadPacksByVersionRange(
  820. project_id,
  821. doc_id,
  822. null,
  823. null,
  824. callback
  825. )
  826. },
  827. // Processing old packs via worker
  828. processOldPack(project_id, doc_id, pack_id, callback) {
  829. const markAsChecked = (err) =>
  830. PackManager.markPackAsChecked(project_id, doc_id, pack_id, function (
  831. err2
  832. ) {
  833. if (err2 != null) {
  834. return callback(err2)
  835. }
  836. return callback(err)
  837. })
  838. logger.log({ project_id, doc_id }, 'processing old packs')
  839. return db.docHistory.findOne({ _id: pack_id }, function (err, pack) {
  840. if (err != null) {
  841. return markAsChecked(err)
  842. }
  843. if (pack == null) {
  844. return markAsChecked()
  845. }
  846. if (pack.expiresAt != null) {
  847. return callback()
  848. } // return directly
  849. return PackManager.finaliseIfNeeded(
  850. project_id,
  851. doc_id,
  852. pack._id,
  853. pack,
  854. function (err) {
  855. if (err != null) {
  856. return markAsChecked(err)
  857. }
  858. return PackManager.updateIndexIfNeeded(project_id, doc_id, function (
  859. err
  860. ) {
  861. if (err != null) {
  862. return markAsChecked(err)
  863. }
  864. return PackManager.findUnarchivedPacks(
  865. project_id,
  866. doc_id,
  867. function (err, unarchivedPacks) {
  868. if (err != null) {
  869. return markAsChecked(err)
  870. }
  871. if (
  872. !(unarchivedPacks != null
  873. ? unarchivedPacks.length
  874. : undefined)
  875. ) {
  876. logger.log({ project_id, doc_id }, 'no packs need archiving')
  877. return markAsChecked()
  878. }
  879. return async.eachSeries(
  880. unarchivedPacks,
  881. (pack, cb) =>
  882. PackManager.archivePack(project_id, doc_id, pack._id, cb),
  883. function (err) {
  884. if (err != null) {
  885. return markAsChecked(err)
  886. }
  887. logger.log({ project_id, doc_id }, 'done processing')
  888. return markAsChecked()
  889. }
  890. )
  891. }
  892. )
  893. })
  894. }
  895. )
  896. })
  897. },
  898. finaliseIfNeeded(project_id, doc_id, pack_id, pack, callback) {
  899. const sz = pack.sz / (1024 * 1024) // in fractions of a megabyte
  900. const n = pack.n / 1024 // in fraction of 1024 ops
  901. const age = (Date.now() - pack.meta.end_ts) / DAYS
  902. if (age < 30) {
  903. // always keep if less than 1 month old
  904. logger.log({ project_id, doc_id, pack_id, age }, 'less than 30 days old')
  905. return callback()
  906. }
  907. // compute an archiving threshold which decreases for each month of age
  908. const archive_threshold = 30 / age
  909. if (sz > archive_threshold || n > archive_threshold || age > 90) {
  910. logger.log(
  911. { project_id, doc_id, pack_id, age, archive_threshold, sz, n },
  912. 'meets archive threshold'
  913. )
  914. return PackManager.markPackAsFinalisedWithLock(
  915. project_id,
  916. doc_id,
  917. pack_id,
  918. callback
  919. )
  920. } else {
  921. logger.log(
  922. { project_id, doc_id, pack_id, age, archive_threshold, sz, n },
  923. 'does not meet archive threshold'
  924. )
  925. return callback()
  926. }
  927. },
  928. markPackAsFinalisedWithLock(project_id, doc_id, pack_id, callback) {
  929. return LockManager.runWithLock(
  930. keys.historyLock({ doc_id }),
  931. (releaseLock) =>
  932. PackManager._markPackAsFinalised(
  933. project_id,
  934. doc_id,
  935. pack_id,
  936. releaseLock
  937. ),
  938. callback
  939. )
  940. },
  941. _markPackAsFinalised(project_id, doc_id, pack_id, callback) {
  942. logger.log({ project_id, doc_id, pack_id }, 'marking pack as finalised')
  943. return db.docHistory.findAndModify(
  944. {
  945. query: { _id: pack_id },
  946. update: { $set: { finalised: true } }
  947. },
  948. callback
  949. )
  950. },
  951. updateIndexIfNeeded(project_id, doc_id, callback) {
  952. logger.log({ project_id, doc_id }, 'archiving old packs')
  953. return PackManager.getIndexWithKeys(doc_id, function (err, index) {
  954. if (err != null) {
  955. return callback(err)
  956. }
  957. if (index == null) {
  958. return PackManager.initialiseIndex(project_id, doc_id, callback)
  959. } else {
  960. return PackManager.updateIndex(project_id, doc_id, callback)
  961. }
  962. })
  963. },
  964. markPackAsChecked(project_id, doc_id, pack_id, callback) {
  965. logger.log({ project_id, doc_id, pack_id }, 'marking pack as checked')
  966. return db.docHistory.findAndModify(
  967. {
  968. query: { _id: pack_id },
  969. update: { $currentDate: { last_checked: true } }
  970. },
  971. callback
  972. )
  973. },
  974. findUnarchivedPacks(project_id, doc_id, callback) {
  975. return PackManager.getIndex(doc_id, function (err, indexResult) {
  976. if (err != null) {
  977. return callback(err)
  978. }
  979. const indexPacks =
  980. (indexResult != null ? indexResult.packs : undefined) || []
  981. const unArchivedPacks = (() => {
  982. const result = []
  983. for (const pack of Array.from(indexPacks)) {
  984. if (pack.inS3 == null) {
  985. result.push(pack)
  986. }
  987. }
  988. return result
  989. })()
  990. if (unArchivedPacks.length) {
  991. logger.log(
  992. { project_id, doc_id, n: unArchivedPacks.length },
  993. 'find unarchived packs'
  994. )
  995. }
  996. return callback(null, unArchivedPacks)
  997. })
  998. },
  999. // Archive locking flags
  1000. checkArchiveNotInProgress(project_id, doc_id, pack_id, callback) {
  1001. logger.log(
  1002. { project_id, doc_id, pack_id },
  1003. 'checking if archive in progress'
  1004. )
  1005. return PackManager.getPackFromIndex(doc_id, pack_id, function (
  1006. err,
  1007. result
  1008. ) {
  1009. if (err != null) {
  1010. return callback(err)
  1011. }
  1012. if (result == null) {
  1013. return callback(new Error('pack not found in index'))
  1014. }
  1015. if (result.inS3) {
  1016. return callback(new Error('pack archiving already done'))
  1017. } else if (result.inS3 != null) {
  1018. return callback(new Error('pack archiving already in progress'))
  1019. } else {
  1020. return callback()
  1021. }
  1022. })
  1023. },
  1024. markPackAsArchiveInProgress(project_id, doc_id, pack_id, callback) {
  1025. logger.log(
  1026. { project_id, doc_id },
  1027. 'marking pack as archive in progress status'
  1028. )
  1029. return db.docHistoryIndex.findAndModify(
  1030. {
  1031. query: {
  1032. _id: ObjectId(doc_id.toString()),
  1033. packs: { $elemMatch: { _id: pack_id, inS3: { $exists: false } } }
  1034. },
  1035. fields: { 'packs.$': 1 },
  1036. update: { $set: { 'packs.$.inS3': false } }
  1037. },
  1038. function (err, result) {
  1039. if (err != null) {
  1040. return callback(err)
  1041. }
  1042. if (result == null) {
  1043. return callback(new Error('archive is already in progress'))
  1044. }
  1045. logger.log(
  1046. { project_id, doc_id, pack_id },
  1047. 'marked as archive in progress'
  1048. )
  1049. return callback()
  1050. }
  1051. )
  1052. },
  1053. clearPackAsArchiveInProgress(project_id, doc_id, pack_id, callback) {
  1054. logger.log(
  1055. { project_id, doc_id, pack_id },
  1056. 'clearing as archive in progress'
  1057. )
  1058. return db.docHistoryIndex.findAndModify(
  1059. {
  1060. query: {
  1061. _id: ObjectId(doc_id.toString()),
  1062. packs: { $elemMatch: { _id: pack_id, inS3: false } }
  1063. },
  1064. fields: { 'packs.$': 1 },
  1065. update: { $unset: { 'packs.$.inS3': true } }
  1066. },
  1067. callback
  1068. )
  1069. },
  1070. markPackAsArchived(project_id, doc_id, pack_id, callback) {
  1071. logger.log({ project_id, doc_id, pack_id }, 'marking pack as archived')
  1072. return db.docHistoryIndex.findAndModify(
  1073. {
  1074. query: {
  1075. _id: ObjectId(doc_id.toString()),
  1076. packs: { $elemMatch: { _id: pack_id, inS3: false } }
  1077. },
  1078. fields: { 'packs.$': 1 },
  1079. update: { $set: { 'packs.$.inS3': true } }
  1080. },
  1081. function (err, result) {
  1082. if (err != null) {
  1083. return callback(err)
  1084. }
  1085. if (result == null) {
  1086. return callback(new Error('archive is not marked as progress'))
  1087. }
  1088. logger.log({ project_id, doc_id, pack_id }, 'marked as archived')
  1089. return callback()
  1090. }
  1091. )
  1092. },
  1093. setTTLOnArchivedPack(project_id, doc_id, pack_id, callback) {
  1094. return db.docHistory.findAndModify(
  1095. {
  1096. query: { _id: pack_id },
  1097. update: { $set: { expiresAt: new Date(Date.now() + 1 * DAYS) } }
  1098. },
  1099. function (err) {
  1100. logger.log({ project_id, doc_id, pack_id }, 'set expiry on pack')
  1101. return callback()
  1102. }
  1103. )
  1104. }
  1105. }
  1106. // _getOneDayInFutureWithRandomDelay: ->
  1107. // thirtyMins = 1000 * 60 * 30
  1108. // randomThirtyMinMax = Math.ceil(Math.random() * thirtyMins)
  1109. // return new Date(Date.now() + randomThirtyMinMax + 1*DAYS)