PackManager.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  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(project_id, doc_id, lastUpdate, newUpdates, temporary, callback) {
  65. if (callback == null) { callback = function(error) {}; }
  66. if (newUpdates.length === 0) { return callback(); }
  67. // never append permanent ops to a pack that will expire
  68. if (((lastUpdate != null ? lastUpdate.expiresAt : undefined) != null) && !temporary) { lastUpdate = null; }
  69. const updatesToFlush = [];
  70. const updatesRemaining = newUpdates.slice();
  71. let n = (lastUpdate != null ? lastUpdate.n : undefined) || 0;
  72. let sz = (lastUpdate != null ? lastUpdate.sz : undefined) || 0;
  73. while (updatesRemaining.length && (n < PackManager.MAX_COUNT) && (sz < PackManager.MAX_SIZE)) {
  74. const nextUpdate = updatesRemaining[0];
  75. const nextUpdateSize = BSON.calculateObjectSize(nextUpdate);
  76. if (((nextUpdateSize + sz) > PackManager.MAX_SIZE) && (n > 0)) {
  77. break;
  78. }
  79. n++;
  80. sz += nextUpdateSize;
  81. updatesToFlush.push(updatesRemaining.shift());
  82. }
  83. return PackManager.flushCompressedUpdates(project_id, doc_id, lastUpdate, updatesToFlush, temporary, function(error) {
  84. if (error != null) { return callback(error); }
  85. return PackManager.insertCompressedUpdates(project_id, doc_id, null, updatesRemaining, temporary, callback);
  86. });
  87. },
  88. flushCompressedUpdates(project_id, doc_id, lastUpdate, newUpdates, temporary, callback) {
  89. if (callback == null) { callback = function(error) {}; }
  90. if (newUpdates.length === 0) { return callback(); }
  91. let canAppend = false;
  92. // check if it is safe to append to an existing pack
  93. if (lastUpdate != null) {
  94. if (!temporary && (lastUpdate.expiresAt == null)) {
  95. // permanent pack appends to permanent pack
  96. canAppend = true;
  97. }
  98. const age = Date.now() - (lastUpdate.meta != null ? lastUpdate.meta.start_ts : undefined);
  99. if (temporary && (lastUpdate.expiresAt != null) && (age < (1 * DAYS))) {
  100. // temporary pack appends to temporary pack if same day
  101. canAppend = true;
  102. }
  103. }
  104. if (canAppend) {
  105. return PackManager.appendUpdatesToExistingPack(project_id, doc_id, lastUpdate, newUpdates, temporary, callback);
  106. } else {
  107. return PackManager.insertUpdatesIntoNewPack(project_id, doc_id, newUpdates, temporary, callback);
  108. }
  109. },
  110. insertUpdatesIntoNewPack(project_id, doc_id, newUpdates, temporary, callback) {
  111. if (callback == null) { callback = function(error) {}; }
  112. const first = newUpdates[0];
  113. const last = newUpdates[newUpdates.length - 1];
  114. const n = newUpdates.length;
  115. const sz = BSON.calculateObjectSize(newUpdates);
  116. const newPack = {
  117. project_id: ObjectId(project_id.toString()),
  118. doc_id: ObjectId(doc_id.toString()),
  119. pack: newUpdates,
  120. n,
  121. sz,
  122. meta: {
  123. start_ts: first.meta.start_ts,
  124. end_ts: last.meta.end_ts
  125. },
  126. v: first.v,
  127. v_end: last.v,
  128. temporary
  129. };
  130. if (temporary) {
  131. newPack.expiresAt = new Date(Date.now() + (7 * DAYS));
  132. newPack.last_checked = new Date(Date.now() + (30 * DAYS)); // never check temporary packs
  133. }
  134. logger.log({project_id, doc_id, newUpdates}, "inserting updates into new pack");
  135. return db.docHistory.save(newPack, function(err, result) {
  136. if (err != null) { return callback(err); }
  137. Metrics.inc(`insert-pack-${temporary ? "temporary" : "permanent"}`);
  138. if (temporary) {
  139. return callback();
  140. } else {
  141. return PackManager.updateIndex(project_id, doc_id, callback);
  142. }
  143. });
  144. },
  145. appendUpdatesToExistingPack(project_id, doc_id, lastUpdate, newUpdates, temporary, callback) {
  146. if (callback == null) { callback = function(error) {}; }
  147. const first = newUpdates[0];
  148. const last = newUpdates[newUpdates.length - 1];
  149. const n = newUpdates.length;
  150. const sz = BSON.calculateObjectSize(newUpdates);
  151. const query = {
  152. _id: lastUpdate._id,
  153. project_id: ObjectId(project_id.toString()),
  154. doc_id: ObjectId(doc_id.toString()),
  155. pack: {$exists: true}
  156. };
  157. const update = {
  158. $push: {
  159. "pack": {$each: newUpdates}
  160. },
  161. $inc: {
  162. "n": n,
  163. "sz": sz
  164. },
  165. $set: {
  166. "meta.end_ts": last.meta.end_ts,
  167. "v_end": last.v
  168. }
  169. };
  170. if (lastUpdate.expiresAt && temporary) {
  171. update.$set.expiresAt = new Date(Date.now() + (7 * DAYS));
  172. }
  173. logger.log({project_id, doc_id, lastUpdate, newUpdates}, "appending updates to existing pack");
  174. Metrics.inc(`append-pack-${temporary ? "temporary" : "permanent"}`);
  175. return db.docHistory.findAndModify({query, update, new:true, fields:{meta:1,v_end:1}}, callback);
  176. },
  177. // Retrieve all changes for a document
  178. getOpsByVersionRange(project_id, doc_id, fromVersion, toVersion, callback) {
  179. if (callback == null) { callback = function(error, updates) {}; }
  180. return PackManager.loadPacksByVersionRange(project_id, doc_id, fromVersion, toVersion, function(error) {
  181. const query = {doc_id:ObjectId(doc_id.toString())};
  182. if (toVersion != null) { query.v = {$lte:toVersion}; }
  183. if (fromVersion != null) { query.v_end = {$gte:fromVersion}; }
  184. // console.log "query:", query
  185. return db.docHistory.find(query).sort({v:-1}, function(err, result) {
  186. if (err != null) { return callback(err); }
  187. // console.log "getOpsByVersionRange:", err, result
  188. const updates = [];
  189. const opInRange = function(op, from, to) {
  190. if ((fromVersion != null) && (op.v < fromVersion)) { return false; }
  191. if ((toVersion != null) && (op.v > toVersion)) { return false; }
  192. return true;
  193. };
  194. for (const docHistory of Array.from(result)) {
  195. // console.log 'adding', docHistory.pack
  196. for (const op of Array.from(docHistory.pack.reverse())) {
  197. if (opInRange(op, fromVersion, toVersion)) {
  198. op.project_id = docHistory.project_id;
  199. op.doc_id = docHistory.doc_id;
  200. // console.log "added op", op.v, fromVersion, toVersion
  201. updates.push(op);
  202. }
  203. }
  204. }
  205. return callback(null, updates);
  206. });
  207. });
  208. },
  209. loadPacksByVersionRange(project_id, doc_id, fromVersion, toVersion, callback) {
  210. return PackManager.getIndex(doc_id, function(err, indexResult) {
  211. let pack;
  212. if (err != null) { return callback(err); }
  213. const indexPacks = (indexResult != null ? indexResult.packs : undefined) || [];
  214. const packInRange = function(pack, from, to) {
  215. if ((fromVersion != null) && (pack.v_end < fromVersion)) { return false; }
  216. if ((toVersion != null) && (pack.v > toVersion)) { return false; }
  217. return true;
  218. };
  219. const neededIds = ((() => {
  220. const result = [];
  221. for (pack of Array.from(indexPacks)) { if (packInRange(pack, fromVersion, toVersion)) {
  222. result.push(pack._id);
  223. }
  224. }
  225. return result;
  226. })());
  227. if (neededIds.length) {
  228. return PackManager.fetchPacksIfNeeded(project_id, doc_id, neededIds, callback);
  229. } else {
  230. return callback();
  231. }
  232. });
  233. },
  234. fetchPacksIfNeeded(project_id, doc_id, pack_ids, callback) {
  235. let id;
  236. return db.docHistory.find({_id: {$in: ((() => {
  237. const result = [];
  238. for (id of Array.from(pack_ids)) { result.push(ObjectId(id));
  239. }
  240. return result;
  241. })())}}, {_id:1}, function(err, loadedPacks) {
  242. if (err != null) { return callback(err); }
  243. const allPackIds = ((() => {
  244. const result1 = [];
  245. for (id of Array.from(pack_ids)) { result1.push(id.toString());
  246. }
  247. return result1;
  248. })());
  249. const loadedPackIds = (Array.from(loadedPacks).map((pack) => pack._id.toString()));
  250. const packIdsToFetch = _.difference(allPackIds, loadedPackIds);
  251. logger.log({project_id, doc_id, loadedPackIds, allPackIds, packIdsToFetch}, "analysed packs");
  252. if (packIdsToFetch.length === 0) { return callback(); }
  253. return async.eachLimit(packIdsToFetch, 4, (pack_id, cb) => MongoAWS.unArchivePack(project_id, doc_id, pack_id, cb)
  254. , function(err) {
  255. if (err != null) { return callback(err); }
  256. logger.log({project_id, doc_id}, "done unarchiving");
  257. return callback();
  258. });
  259. });
  260. },
  261. // Retrieve all changes across a project
  262. makeProjectIterator(project_id, before, callback) {
  263. // get all the docHistory Entries
  264. return db.docHistory.find({project_id: ObjectId(project_id)},{pack:false}).sort({"meta.end_ts":-1}, function(err, packs) {
  265. let pack;
  266. if (err != null) { return callback(err); }
  267. const allPacks = [];
  268. const seenIds = {};
  269. for (pack of Array.from(packs)) {
  270. allPacks.push(pack);
  271. seenIds[pack._id] = true;
  272. }
  273. return db.docHistoryIndex.find({project_id: ObjectId(project_id)}, function(err, indexes) {
  274. if (err != null) { return callback(err); }
  275. for (const index of Array.from(indexes)) {
  276. for (pack of Array.from(index.packs)) {
  277. if (!seenIds[pack._id]) {
  278. pack.project_id = index.project_id;
  279. pack.doc_id = index._id;
  280. pack.fromIndex = true;
  281. allPacks.push(pack);
  282. seenIds[pack._id] = true;
  283. }
  284. }
  285. }
  286. return callback(null, new ProjectIterator(allPacks, before, PackManager.getPackById));
  287. });
  288. });
  289. },
  290. getPackById(project_id, doc_id, pack_id, callback) {
  291. return db.docHistory.findOne({_id: pack_id}, function(err, pack) {
  292. if (err != null) { return callback(err); }
  293. if ((pack == null)) {
  294. return MongoAWS.unArchivePack(project_id, doc_id, pack_id, callback);
  295. } else if ((pack.expiresAt != null) && (pack.temporary === false)) {
  296. // we only need to touch the TTL when listing the changes in the project
  297. // because diffs on individual documents are always done after that
  298. return PackManager.increaseTTL(pack, callback);
  299. // only do this for cached packs, not temporary ones to avoid older packs
  300. // being kept longer than newer ones (which messes up the last update version)
  301. } else {
  302. return callback(null, pack);
  303. }
  304. });
  305. },
  306. increaseTTL(pack, callback) {
  307. if (pack.expiresAt < new Date(Date.now() + (6 * DAYS))) {
  308. // update cache expiry since we are using this pack
  309. return db.docHistory.findAndModify({
  310. query: {_id: pack._id},
  311. update: {$set: {expiresAt: new Date(Date.now() + (7 * DAYS))}}
  312. }, err => callback(err, pack));
  313. } else {
  314. return callback(null, pack);
  315. }
  316. },
  317. // Manage docHistoryIndex collection
  318. getIndex(doc_id, callback) {
  319. return db.docHistoryIndex.findOne({_id:ObjectId(doc_id.toString())}, callback);
  320. },
  321. getPackFromIndex(doc_id, pack_id, callback) {
  322. return db.docHistoryIndex.findOne({_id:ObjectId(doc_id.toString()), "packs._id": pack_id}, {"packs.$":1}, callback);
  323. },
  324. getLastPackFromIndex(doc_id, callback) {
  325. return db.docHistoryIndex.findOne({_id: ObjectId(doc_id.toString())}, {packs:{$slice:-1}}, function(err, indexPack) {
  326. if (err != null) { return callback(err); }
  327. if ((indexPack == null)) { return callback(); }
  328. return callback(null,indexPack[0]);
  329. });
  330. },
  331. getIndexWithKeys(doc_id, callback) {
  332. return PackManager.getIndex(doc_id, function(err, index) {
  333. if (err != null) { return callback(err); }
  334. if ((index == null)) { return callback(); }
  335. for (const pack of Array.from((index != null ? index.packs : undefined) || [])) {
  336. index[pack._id] = pack;
  337. }
  338. return callback(null, index);
  339. });
  340. },
  341. initialiseIndex(project_id, doc_id, callback) {
  342. return PackManager.findCompletedPacks(project_id, doc_id, function(err, packs) {
  343. // console.log 'err', err, 'packs', packs, packs?.length
  344. if (err != null) { return callback(err); }
  345. if ((packs == null)) { return callback(); }
  346. return PackManager.insertPacksIntoIndexWithLock(project_id, doc_id, packs, callback);
  347. });
  348. },
  349. updateIndex(project_id, doc_id, callback) {
  350. // find all packs prior to current pack
  351. return PackManager.findUnindexedPacks(project_id, doc_id, function(err, newPacks) {
  352. if (err != null) { return callback(err); }
  353. if ((newPacks == null) || (newPacks.length === 0)) { return callback(); }
  354. return PackManager.insertPacksIntoIndexWithLock(project_id, doc_id, newPacks, function(err) {
  355. if (err != null) { return callback(err); }
  356. logger.log({project_id, doc_id, newPacks}, "added new packs to index");
  357. return callback();
  358. });
  359. });
  360. },
  361. findCompletedPacks(project_id, doc_id, callback) {
  362. const query = { doc_id: ObjectId(doc_id.toString()), expiresAt: {$exists:false} };
  363. return db.docHistory.find(query, {pack:false}).sort({v:1}, function(err, packs) {
  364. if (err != null) { return callback(err); }
  365. if ((packs == null)) { return callback(); }
  366. if (!(packs != null ? packs.length : undefined)) { return callback(); }
  367. const last = packs.pop(); // discard the last pack, if it's still in progress
  368. if (last.finalised) { packs.push(last); } // it's finalised so we push it back to archive it
  369. return callback(null, packs);
  370. });
  371. },
  372. findPacks(project_id, doc_id, callback) {
  373. const query = { doc_id: ObjectId(doc_id.toString()), expiresAt: {$exists:false} };
  374. return db.docHistory.find(query, {pack:false}).sort({v:1}, function(err, packs) {
  375. if (err != null) { return callback(err); }
  376. if ((packs == null)) { return callback(); }
  377. if (!(packs != null ? packs.length : undefined)) { return callback(); }
  378. return callback(null, packs);
  379. });
  380. },
  381. findUnindexedPacks(project_id, doc_id, callback) {
  382. return PackManager.getIndexWithKeys(doc_id, function(err, indexResult) {
  383. if (err != null) { return callback(err); }
  384. return PackManager.findCompletedPacks(project_id, doc_id, function(err, historyPacks) {
  385. let pack;
  386. if (err != null) { return callback(err); }
  387. if ((historyPacks == null)) { return callback(); }
  388. // select only the new packs not already in the index
  389. let newPacks = ((() => {
  390. const result = [];
  391. for (pack of Array.from(historyPacks)) { if (((indexResult != null ? indexResult[pack._id] : undefined) == null)) {
  392. result.push(pack);
  393. }
  394. }
  395. return result;
  396. })());
  397. newPacks = ((() => {
  398. const result1 = [];
  399. for (pack of Array.from(newPacks)) { result1.push(_.omit(pack, 'doc_id', 'project_id', 'n', 'sz', 'last_checked', 'finalised'));
  400. }
  401. return result1;
  402. })());
  403. if (newPacks.length) {
  404. logger.log({project_id, doc_id, n: newPacks.length}, "found new packs");
  405. }
  406. return callback(null, newPacks);
  407. });
  408. });
  409. },
  410. insertPacksIntoIndexWithLock(project_id, doc_id, newPacks, callback) {
  411. return LockManager.runWithLock(
  412. keys.historyIndexLock({doc_id}),
  413. releaseLock => PackManager._insertPacksIntoIndex(project_id, doc_id, newPacks, releaseLock),
  414. callback
  415. );
  416. },
  417. _insertPacksIntoIndex(project_id, doc_id, newPacks, callback) {
  418. return db.docHistoryIndex.findAndModify({
  419. query: {_id:ObjectId(doc_id.toString())},
  420. update: {
  421. $setOnInsert: { project_id: ObjectId(project_id.toString())
  422. },
  423. $push: {
  424. packs: {$each: newPacks, $sort: {v: 1}}
  425. }
  426. },
  427. upsert: true
  428. }, callback);
  429. },
  430. // Archiving packs to S3
  431. archivePack(project_id, doc_id, pack_id, callback) {
  432. const clearFlagOnError = function(err, cb) {
  433. if (err != null) { // clear the inS3 flag on error
  434. return PackManager.clearPackAsArchiveInProgress(project_id, doc_id, pack_id, function(err2) {
  435. if (err2 != null) { return cb(err2); }
  436. return cb(err);
  437. });
  438. } else {
  439. return cb();
  440. }
  441. };
  442. return async.series([
  443. cb => PackManager.checkArchiveNotInProgress(project_id, doc_id, pack_id, cb),
  444. cb => PackManager.markPackAsArchiveInProgress(project_id, doc_id, pack_id, cb),
  445. cb =>
  446. MongoAWS.archivePack(project_id, doc_id, pack_id, err => clearFlagOnError(err, cb))
  447. ,
  448. cb =>
  449. PackManager.checkArchivedPack(project_id, doc_id, pack_id, err => clearFlagOnError(err, cb))
  450. ,
  451. cb => PackManager.markPackAsArchived(project_id, doc_id, pack_id, cb),
  452. cb => PackManager.setTTLOnArchivedPack(project_id, doc_id, pack_id, callback)
  453. ], callback);
  454. },
  455. checkArchivedPack(project_id, doc_id, pack_id, callback) {
  456. return db.docHistory.findOne({_id: pack_id}, function(err, pack) {
  457. if (err != null) { return callback(err); }
  458. if ((pack == null)) { return callback(new Error("pack not found")); }
  459. return MongoAWS.readArchivedPack(project_id, doc_id, pack_id, function(err, result) {
  460. delete result.last_checked;
  461. delete pack.last_checked;
  462. // need to compare ids as ObjectIds with .equals()
  463. for (const key of ['_id', 'project_id', 'doc_id']) {
  464. if (result[key].equals(pack[key])) { result[key] = pack[key]; }
  465. }
  466. for (let i = 0; i < result.pack.length; i++) {
  467. const op = result.pack[i];
  468. if ((op._id != null) && op._id.equals(pack.pack[i]._id)) { op._id = pack.pack[i]._id; }
  469. }
  470. if (_.isEqual(pack, result)) {
  471. return callback();
  472. } else {
  473. logger.err({pack, result, jsondiff: JSON.stringify(pack) === JSON.stringify(result)}, "difference when comparing packs");
  474. return callback(new Error("pack retrieved from s3 does not match pack in mongo"));
  475. }
  476. });
  477. });
  478. },
  479. // Extra methods to test archive/unarchive for a doc_id
  480. pushOldPacks(project_id, doc_id, callback) {
  481. return PackManager.findPacks(project_id, doc_id, function(err, packs) {
  482. if (err != null) { return callback(err); }
  483. if (!(packs != null ? packs.length : undefined)) { return callback(); }
  484. return PackManager.processOldPack(project_id, doc_id, packs[0]._id, callback);
  485. });
  486. },
  487. pullOldPacks(project_id, doc_id, callback) {
  488. return PackManager.loadPacksByVersionRange(project_id, doc_id, null, null, callback);
  489. },
  490. // Processing old packs via worker
  491. processOldPack(project_id, doc_id, pack_id, callback) {
  492. const markAsChecked = err =>
  493. PackManager.markPackAsChecked(project_id, doc_id, pack_id, function(err2) {
  494. if (err2 != null) { return callback(err2); }
  495. return callback(err);
  496. })
  497. ;
  498. logger.log({project_id, doc_id}, "processing old packs");
  499. return db.docHistory.findOne({_id:pack_id}, function(err, pack) {
  500. if (err != null) { return markAsChecked(err); }
  501. if ((pack == null)) { return markAsChecked(); }
  502. if (pack.expiresAt != null) { return callback(); } // return directly
  503. return PackManager.finaliseIfNeeded(project_id, doc_id, pack._id, pack, function(err) {
  504. if (err != null) { return markAsChecked(err); }
  505. return PackManager.updateIndexIfNeeded(project_id, doc_id, function(err) {
  506. if (err != null) { return markAsChecked(err); }
  507. return PackManager.findUnarchivedPacks(project_id, doc_id, function(err, unarchivedPacks) {
  508. if (err != null) { return markAsChecked(err); }
  509. if (!(unarchivedPacks != null ? unarchivedPacks.length : undefined)) {
  510. logger.log({project_id, doc_id}, "no packs need archiving");
  511. return markAsChecked();
  512. }
  513. return async.eachSeries(unarchivedPacks, (pack, cb) => PackManager.archivePack(project_id, doc_id, pack._id, cb)
  514. , function(err) {
  515. if (err != null) { return markAsChecked(err); }
  516. logger.log({project_id, doc_id}, "done processing");
  517. return markAsChecked();
  518. });
  519. });
  520. });
  521. });
  522. });
  523. },
  524. finaliseIfNeeded(project_id, doc_id, pack_id, pack, callback) {
  525. const sz = pack.sz / (1024 * 1024); // in fractions of a megabyte
  526. const n = pack.n / 1024; // in fraction of 1024 ops
  527. const age = (Date.now() - pack.meta.end_ts) / DAYS;
  528. if (age < 30) { // always keep if less than 1 month old
  529. logger.log({project_id, doc_id, pack_id, age}, "less than 30 days old");
  530. return callback();
  531. }
  532. // compute an archiving threshold which decreases for each month of age
  533. const archive_threshold = 30 / age;
  534. if ((sz > archive_threshold) || (n > archive_threshold) || (age > 90)) {
  535. logger.log({project_id, doc_id, pack_id, age, archive_threshold, sz, n}, "meets archive threshold");
  536. return PackManager.markPackAsFinalisedWithLock(project_id, doc_id, pack_id, callback);
  537. } else {
  538. logger.log({project_id, doc_id, pack_id, age, archive_threshold, sz, n}, "does not meet archive threshold");
  539. return callback();
  540. }
  541. },
  542. markPackAsFinalisedWithLock(project_id, doc_id, pack_id, callback) {
  543. return LockManager.runWithLock(
  544. keys.historyLock({doc_id}),
  545. releaseLock => PackManager._markPackAsFinalised(project_id, doc_id, pack_id, releaseLock),
  546. callback
  547. );
  548. },
  549. _markPackAsFinalised(project_id, doc_id, pack_id, callback) {
  550. logger.log({project_id, doc_id, pack_id}, "marking pack as finalised");
  551. return db.docHistory.findAndModify({
  552. query: {_id: pack_id},
  553. update: {$set: {finalised: true}}
  554. }, callback);
  555. },
  556. updateIndexIfNeeded(project_id, doc_id, callback) {
  557. logger.log({project_id, doc_id}, "archiving old packs");
  558. return PackManager.getIndexWithKeys(doc_id, function(err, index) {
  559. if (err != null) { return callback(err); }
  560. if ((index == null)) {
  561. return PackManager.initialiseIndex(project_id, doc_id, callback);
  562. } else {
  563. return PackManager.updateIndex(project_id, doc_id, callback);
  564. }
  565. });
  566. },
  567. markPackAsChecked(project_id, doc_id, pack_id, callback) {
  568. logger.log({project_id, doc_id, pack_id}, "marking pack as checked");
  569. return db.docHistory.findAndModify({
  570. query: {_id: pack_id},
  571. update: {$currentDate: {"last_checked":true}}
  572. }, callback);
  573. },
  574. findUnarchivedPacks(project_id, doc_id, callback) {
  575. return PackManager.getIndex(doc_id, function(err, indexResult) {
  576. if (err != null) { return callback(err); }
  577. const indexPacks = (indexResult != null ? indexResult.packs : undefined) || [];
  578. const unArchivedPacks = ((() => {
  579. const result = [];
  580. for (const pack of Array.from(indexPacks)) { if ((pack.inS3 == null)) {
  581. result.push(pack);
  582. }
  583. }
  584. return result;
  585. })());
  586. if (unArchivedPacks.length) {
  587. logger.log({project_id, doc_id, n: unArchivedPacks.length}, "find unarchived packs");
  588. }
  589. return callback(null, unArchivedPacks);
  590. });
  591. },
  592. // Archive locking flags
  593. checkArchiveNotInProgress(project_id, doc_id, pack_id, callback) {
  594. logger.log({project_id, doc_id, pack_id}, "checking if archive in progress");
  595. return PackManager.getPackFromIndex(doc_id, pack_id, function(err, result) {
  596. if (err != null) { return callback(err); }
  597. if ((result == null)) { return callback(new Error("pack not found in index")); }
  598. if (result.inS3) {
  599. return callback(new Error("pack archiving already done"));
  600. } else if (result.inS3 != null) {
  601. return callback(new Error("pack archiving already in progress"));
  602. } else {
  603. return callback();
  604. }
  605. });
  606. },
  607. markPackAsArchiveInProgress(project_id, doc_id, pack_id, callback) {
  608. logger.log({project_id, doc_id}, "marking pack as archive in progress status");
  609. return db.docHistoryIndex.findAndModify({
  610. query: {_id:ObjectId(doc_id.toString()), packs: {$elemMatch: {"_id": pack_id, inS3: {$exists:false}}}},
  611. fields: { "packs.$": 1 },
  612. update: {$set: {"packs.$.inS3":false}}
  613. }, function(err, result) {
  614. if (err != null) { return callback(err); }
  615. if ((result == null)) { return callback(new Error("archive is already in progress")); }
  616. logger.log({project_id, doc_id, pack_id}, "marked as archive in progress");
  617. return callback();
  618. });
  619. },
  620. clearPackAsArchiveInProgress(project_id, doc_id, pack_id, callback) {
  621. logger.log({project_id, doc_id, pack_id}, "clearing as archive in progress");
  622. return db.docHistoryIndex.findAndModify({
  623. query: {_id:ObjectId(doc_id.toString()), "packs" : {$elemMatch: {"_id": pack_id, inS3: false}}},
  624. fields: { "packs.$": 1 },
  625. update: {$unset: {"packs.$.inS3":true}}
  626. }, callback);
  627. },
  628. markPackAsArchived(project_id, doc_id, pack_id, callback) {
  629. logger.log({project_id, doc_id, pack_id}, "marking pack as archived");
  630. return db.docHistoryIndex.findAndModify({
  631. query: {_id:ObjectId(doc_id.toString()), "packs" : {$elemMatch: {"_id": pack_id, inS3: false}}},
  632. fields: { "packs.$": 1 },
  633. update: {$set: {"packs.$.inS3":true}}
  634. }, function(err, result) {
  635. if (err != null) { return callback(err); }
  636. if ((result == null)) { return callback(new Error("archive is not marked as progress")); }
  637. logger.log({project_id, doc_id, pack_id}, "marked as archived");
  638. return callback();
  639. });
  640. },
  641. setTTLOnArchivedPack(project_id, doc_id, pack_id, callback) {
  642. return db.docHistory.findAndModify({
  643. query: {_id: pack_id},
  644. update: {$set: {expiresAt: new Date(Date.now() + (1*DAYS))}}
  645. }, function(err) {
  646. logger.log({project_id, doc_id, pack_id}, "set expiry on pack");
  647. return callback();
  648. });
  649. }
  650. });
  651. // _getOneDayInFutureWithRandomDelay: ->
  652. // thirtyMins = 1000 * 60 * 30
  653. // randomThirtyMinMax = Math.ceil(Math.random() * thirtyMins)
  654. // return new Date(Date.now() + randomThirtyMinMax + 1*DAYS)