UpdatesManager.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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. * DS103: Rewrite code to no longer use __guard__
  13. * DS205: Consider reworking code to avoid use of IIFEs
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. let fiveMinutes, UpdatesManager;
  18. const MongoManager = require("./MongoManager");
  19. const PackManager = require("./PackManager");
  20. const RedisManager = require("./RedisManager");
  21. const UpdateCompressor = require("./UpdateCompressor");
  22. const LockManager = require("./LockManager");
  23. const WebApiManager = require("./WebApiManager");
  24. const UpdateTrimmer = require("./UpdateTrimmer");
  25. const logger = require("logger-sharelatex");
  26. const async = require("async");
  27. const _ = require("underscore");
  28. const Settings = require("settings-sharelatex");
  29. const keys = Settings.redis.lock.key_schema;
  30. module.exports = (UpdatesManager = {
  31. compressAndSaveRawUpdates(project_id, doc_id, rawUpdates, temporary, callback) {
  32. let i;
  33. if (callback == null) { callback = function(error) {}; }
  34. const { length } = rawUpdates;
  35. if (length === 0) {
  36. return callback();
  37. }
  38. // check that ops are in the correct order
  39. for (i = 0; i < rawUpdates.length; i++) {
  40. const op = rawUpdates[i];
  41. if (i > 0) {
  42. const thisVersion = op != null ? op.v : undefined;
  43. const prevVersion = __guard__(rawUpdates[i-1], x => x.v);
  44. if (!(prevVersion < thisVersion)) {
  45. logger.error({project_id, doc_id, rawUpdates, temporary, thisVersion, prevVersion}, "op versions out of order");
  46. }
  47. }
  48. }
  49. // FIXME: we no longer need the lastCompressedUpdate, so change functions not to need it
  50. // CORRECTION: we do use it to log the time in case of error
  51. return MongoManager.peekLastCompressedUpdate(doc_id, function(error, lastCompressedUpdate, lastVersion) {
  52. // lastCompressedUpdate is the most recent update in Mongo, and
  53. // lastVersion is its sharejs version number.
  54. //
  55. // The peekLastCompressedUpdate method may pass the update back
  56. // as 'null' (for example if the previous compressed update has
  57. // been archived). In this case it can still pass back the
  58. // lastVersion from the update to allow us to check consistency.
  59. let op;
  60. if (error != null) { return callback(error); }
  61. // Ensure that raw updates start where lastVersion left off
  62. if (lastVersion != null) {
  63. const discardedUpdates = [];
  64. rawUpdates = rawUpdates.slice(0);
  65. while ((rawUpdates[0] != null) && (rawUpdates[0].v <= lastVersion)) {
  66. discardedUpdates.push(rawUpdates.shift());
  67. }
  68. if (discardedUpdates.length) {
  69. logger.error({project_id, doc_id, discardedUpdates, temporary, lastVersion}, "discarded updates already present");
  70. }
  71. if ((rawUpdates[0] != null) && (rawUpdates[0].v !== (lastVersion + 1))) {
  72. const ts = __guard__(lastCompressedUpdate != null ? lastCompressedUpdate.meta : undefined, x1 => x1.end_ts);
  73. const last_timestamp = (ts != null) ? new Date(ts) : 'unknown time';
  74. error = new Error(`Tried to apply raw op at version ${rawUpdates[0].v} to last compressed update with version ${lastVersion} from ${last_timestamp}`);
  75. logger.error({err: error, doc_id, project_id, prev_end_ts: ts, temporary, lastCompressedUpdate}, "inconsistent doc versions");
  76. if ((Settings.trackchanges != null ? Settings.trackchanges.continueOnError : undefined) && (rawUpdates[0].v > (lastVersion + 1))) {
  77. // we have lost some ops - continue to write into the database, we can't recover at this point
  78. lastCompressedUpdate = null;
  79. } else {
  80. return callback(error);
  81. }
  82. }
  83. }
  84. if (rawUpdates.length === 0) {
  85. return callback();
  86. }
  87. // some old large ops in redis need to be rejected, they predate
  88. // the size limit that now prevents them going through the system
  89. const REJECT_LARGE_OP_SIZE = 4 * 1024 * 1024;
  90. for (var rawUpdate of Array.from(rawUpdates)) {
  91. const opSizes = ((() => {
  92. const result = [];
  93. for (op of Array.from((rawUpdate != null ? rawUpdate.op : undefined) || [])) { result.push(((op.i != null ? op.i.length : undefined) || (op.d != null ? op.d.length : undefined)));
  94. }
  95. return result;
  96. })());
  97. const size = _.max(opSizes);
  98. if (size > REJECT_LARGE_OP_SIZE) {
  99. error = new Error(`dropped op exceeding maximum allowed size of ${REJECT_LARGE_OP_SIZE}`);
  100. logger.error({err: error, doc_id, project_id, size, rawUpdate}, "dropped op - too big");
  101. rawUpdate.op = [];
  102. }
  103. }
  104. const compressedUpdates = UpdateCompressor.compressRawUpdates(null, rawUpdates);
  105. return PackManager.insertCompressedUpdates(project_id, doc_id, lastCompressedUpdate, compressedUpdates, temporary, function(error, result) {
  106. if (error != null) { return callback(error); }
  107. if (result != null) { logger.log({project_id, doc_id, orig_v: (lastCompressedUpdate != null ? lastCompressedUpdate.v : undefined), new_v: result.v}, "inserted updates into pack"); }
  108. return callback();
  109. });
  110. });
  111. },
  112. // Check whether the updates are temporary (per-project property)
  113. _prepareProjectForUpdates(project_id, callback) {
  114. if (callback == null) { callback = function(error, temporary) {}; }
  115. return UpdateTrimmer.shouldTrimUpdates(project_id, function(error, temporary) {
  116. if (error != null) { return callback(error); }
  117. return callback(null, temporary);
  118. });
  119. },
  120. // Check for project id on document history (per-document property)
  121. _prepareDocForUpdates(project_id, doc_id, callback) {
  122. if (callback == null) { callback = function(error) {}; }
  123. return MongoManager.backportProjectId(project_id, doc_id, function(error) {
  124. if (error != null) { return callback(error); }
  125. return callback(null);
  126. });
  127. },
  128. // Apply updates for specific project/doc after preparing at project and doc level
  129. REDIS_READ_BATCH_SIZE: 100,
  130. processUncompressedUpdates(project_id, doc_id, temporary, callback) {
  131. // get the updates as strings from redis (so we can delete them after they are applied)
  132. if (callback == null) { callback = function(error) {}; }
  133. return RedisManager.getOldestDocUpdates(doc_id, UpdatesManager.REDIS_READ_BATCH_SIZE, function(error, docUpdates) {
  134. if (error != null) { return callback(error); }
  135. const { length } = docUpdates;
  136. // parse the redis strings into ShareJs updates
  137. return RedisManager.expandDocUpdates(docUpdates, function(error, rawUpdates) {
  138. if (error != null) {
  139. logger.err({project_id, doc_id, docUpdates}, "failed to parse docUpdates");
  140. return callback(error);
  141. }
  142. logger.log({project_id, doc_id, rawUpdates}, "retrieved raw updates from redis");
  143. return UpdatesManager.compressAndSaveRawUpdates(project_id, doc_id, rawUpdates, temporary, function(error) {
  144. if (error != null) { return callback(error); }
  145. logger.log({project_id, doc_id}, "compressed and saved doc updates");
  146. // delete the applied updates from redis
  147. return RedisManager.deleteAppliedDocUpdates(project_id, doc_id, docUpdates, function(error) {
  148. if (error != null) { return callback(error); }
  149. if (length === UpdatesManager.REDIS_READ_BATCH_SIZE) {
  150. // There might be more updates
  151. logger.log({project_id, doc_id}, "continuing processing updates");
  152. return setTimeout(() => UpdatesManager.processUncompressedUpdates(project_id, doc_id, temporary, callback)
  153. , 0);
  154. } else {
  155. logger.log({project_id, doc_id}, "all raw updates processed");
  156. return callback();
  157. }
  158. });
  159. });
  160. });
  161. });
  162. },
  163. // Process updates for a doc when we flush it individually
  164. processUncompressedUpdatesWithLock(project_id, doc_id, callback) {
  165. if (callback == null) { callback = function(error) {}; }
  166. return UpdatesManager._prepareProjectForUpdates(project_id, function(error, temporary) {
  167. if (error != null) { return callback(error); }
  168. return UpdatesManager._processUncompressedUpdatesForDocWithLock(project_id, doc_id, temporary, callback);
  169. });
  170. },
  171. // Process updates for a doc when the whole project is flushed (internal method)
  172. _processUncompressedUpdatesForDocWithLock(project_id, doc_id, temporary, callback) {
  173. if (callback == null) { callback = function(error) {}; }
  174. return UpdatesManager._prepareDocForUpdates(project_id, doc_id, function(error) {
  175. if (error != null) { return callback(error); }
  176. return LockManager.runWithLock(
  177. keys.historyLock({doc_id}),
  178. releaseLock => UpdatesManager.processUncompressedUpdates(project_id, doc_id, temporary, releaseLock),
  179. callback
  180. );
  181. });
  182. },
  183. // Process all updates for a project, only check project-level information once
  184. processUncompressedUpdatesForProject(project_id, callback) {
  185. if (callback == null) { callback = function(error) {}; }
  186. return RedisManager.getDocIdsWithHistoryOps(project_id, function(error, doc_ids) {
  187. if (error != null) { return callback(error); }
  188. return UpdatesManager._prepareProjectForUpdates(project_id, function(error, temporary) {
  189. const jobs = [];
  190. for (const doc_id of Array.from(doc_ids)) {
  191. (doc_id =>
  192. jobs.push(cb => UpdatesManager._processUncompressedUpdatesForDocWithLock(project_id, doc_id, temporary, cb))
  193. )(doc_id);
  194. }
  195. return async.parallelLimit(jobs, 5, callback);
  196. });
  197. });
  198. },
  199. // flush all outstanding changes
  200. flushAll(limit, callback) {
  201. if (callback == null) { callback = function(error, result) {}; }
  202. return RedisManager.getProjectIdsWithHistoryOps(function(error, project_ids) {
  203. let project_id;
  204. if (error != null) { return callback(error); }
  205. logger.log({count: (project_ids != null ? project_ids.length : undefined), project_ids}, "found projects");
  206. const jobs = [];
  207. project_ids = _.shuffle(project_ids); // randomise to avoid hitting same projects each time
  208. const selectedProjects = limit < 0 ? project_ids : project_ids.slice(0, limit);
  209. for (project_id of Array.from(selectedProjects)) {
  210. (project_id =>
  211. jobs.push(cb =>
  212. UpdatesManager.processUncompressedUpdatesForProject(project_id, err => cb(null, {failed: (err != null), project_id}))
  213. )
  214. )(project_id);
  215. }
  216. return async.series(jobs, function(error, result) {
  217. let x;
  218. if (error != null) { return callback(error); }
  219. const failedProjects = ((() => {
  220. const result1 = [];
  221. for (x of Array.from(result)) { if (x.failed) {
  222. result1.push(x.project_id);
  223. }
  224. }
  225. return result1;
  226. })());
  227. const succeededProjects = ((() => {
  228. const result2 = [];
  229. for (x of Array.from(result)) { if (!x.failed) {
  230. result2.push(x.project_id);
  231. }
  232. }
  233. return result2;
  234. })());
  235. return callback(null, {failed: failedProjects, succeeded: succeededProjects, all: project_ids});
  236. });
  237. });
  238. },
  239. getDanglingUpdates(callback) {
  240. if (callback == null) { callback = function(error, doc_ids) {}; }
  241. return RedisManager.getAllDocIdsWithHistoryOps(function(error, all_doc_ids) {
  242. if (error != null) { return callback(error); }
  243. return RedisManager.getProjectIdsWithHistoryOps(function(error, all_project_ids) {
  244. if (error != null) { return callback(error); }
  245. // function to get doc_ids for each project
  246. const task = cb => async.concatSeries(all_project_ids, RedisManager.getDocIdsWithHistoryOps, cb);
  247. // find the dangling doc ids
  248. return task(function(error, project_doc_ids) {
  249. const dangling_doc_ids = _.difference(all_doc_ids, project_doc_ids);
  250. logger.log({all_doc_ids, all_project_ids, project_doc_ids, dangling_doc_ids}, "checking for dangling doc ids");
  251. return callback(null, dangling_doc_ids);
  252. });
  253. });
  254. });
  255. },
  256. getDocUpdates(project_id, doc_id, options, callback) {
  257. if (options == null) { options = {}; }
  258. if (callback == null) { callback = function(error, updates) {}; }
  259. return UpdatesManager.processUncompressedUpdatesWithLock(project_id, doc_id, function(error) {
  260. if (error != null) { return callback(error); }
  261. // console.log "options", options
  262. return PackManager.getOpsByVersionRange(project_id, doc_id, options.from, options.to, function(error, updates) {
  263. if (error != null) { return callback(error); }
  264. return callback(null, updates);
  265. });
  266. });
  267. },
  268. getDocUpdatesWithUserInfo(project_id, doc_id, options, callback) {
  269. if (options == null) { options = {}; }
  270. if (callback == null) { callback = function(error, updates) {}; }
  271. return UpdatesManager.getDocUpdates(project_id, doc_id, options, function(error, updates) {
  272. if (error != null) { return callback(error); }
  273. return UpdatesManager.fillUserInfo(updates, function(error, updates) {
  274. if (error != null) { return callback(error); }
  275. return callback(null, updates);
  276. });
  277. });
  278. },
  279. getSummarizedProjectUpdates(project_id, options, callback) {
  280. if (options == null) { options = {}; }
  281. if (callback == null) { callback = function(error, updates) {}; }
  282. if (!options.min_count) { options.min_count = 25; }
  283. let summarizedUpdates = [];
  284. const { before } = options;
  285. let nextBeforeTimestamp = null;
  286. return UpdatesManager.processUncompressedUpdatesForProject(project_id, function(error) {
  287. if (error != null) { return callback(error); }
  288. return PackManager.makeProjectIterator(project_id, before, function(err, iterator) {
  289. if (err != null) { return callback(err); }
  290. // repeatedly get updates and pass them through the summariser to get an final output with user info
  291. return async.whilst(() =>
  292. // console.log "checking iterator.done", iterator.done()
  293. (summarizedUpdates.length < options.min_count) && !iterator.done()
  294. , cb =>
  295. iterator.next(function(err, partialUpdates) {
  296. if (err != null) { return callback(err); }
  297. // logger.log {partialUpdates}, 'got partialUpdates'
  298. if (partialUpdates.length === 0) { return cb(); } // # FIXME should try to avoid this happening
  299. nextBeforeTimestamp = partialUpdates[partialUpdates.length - 1].meta.end_ts;
  300. // add the updates to the summary list
  301. summarizedUpdates = UpdatesManager._summarizeUpdates(partialUpdates, summarizedUpdates);
  302. return cb();
  303. })
  304. , () =>
  305. // finally done all updates
  306. // console.log 'summarized Updates', summarizedUpdates
  307. UpdatesManager.fillSummarizedUserInfo(summarizedUpdates, function(err, results) {
  308. if (err != null) { return callback(err); }
  309. return callback(null, results, !iterator.done() ? nextBeforeTimestamp : undefined);
  310. })
  311. );
  312. });
  313. });
  314. },
  315. fetchUserInfo(users, callback) {
  316. if (callback == null) { callback = function(error, fetchedUserInfo) {}; }
  317. const jobs = [];
  318. const fetchedUserInfo = {};
  319. for (const user_id in users) {
  320. (user_id =>
  321. jobs.push(callback =>
  322. WebApiManager.getUserInfo(user_id, function(error, userInfo) {
  323. if (error != null) { return callback(error); }
  324. fetchedUserInfo[user_id] = userInfo;
  325. return callback();
  326. })
  327. )
  328. )(user_id);
  329. }
  330. return async.series(jobs, function(err) {
  331. if (err != null) { return callback(err); }
  332. return callback(null, fetchedUserInfo);
  333. });
  334. },
  335. fillUserInfo(updates, callback) {
  336. let update, user_id;
  337. if (callback == null) { callback = function(error, updates) {}; }
  338. const users = {};
  339. for (update of Array.from(updates)) {
  340. ({ user_id } = update.meta);
  341. if (UpdatesManager._validUserId(user_id)) {
  342. users[user_id] = true;
  343. }
  344. }
  345. return UpdatesManager.fetchUserInfo(users, function(error, fetchedUserInfo) {
  346. if (error != null) { return callback(error); }
  347. for (update of Array.from(updates)) {
  348. ({ user_id } = update.meta);
  349. delete update.meta.user_id;
  350. if (UpdatesManager._validUserId(user_id)) {
  351. update.meta.user = fetchedUserInfo[user_id];
  352. }
  353. }
  354. return callback(null, updates);
  355. });
  356. },
  357. fillSummarizedUserInfo(updates, callback) {
  358. let update, user_id, user_ids;
  359. if (callback == null) { callback = function(error, updates) {}; }
  360. const users = {};
  361. for (update of Array.from(updates)) {
  362. user_ids = update.meta.user_ids || [];
  363. for (user_id of Array.from(user_ids)) {
  364. if (UpdatesManager._validUserId(user_id)) {
  365. users[user_id] = true;
  366. }
  367. }
  368. }
  369. return UpdatesManager.fetchUserInfo(users, function(error, fetchedUserInfo) {
  370. if (error != null) { return callback(error); }
  371. for (update of Array.from(updates)) {
  372. user_ids = update.meta.user_ids || [];
  373. update.meta.users = [];
  374. delete update.meta.user_ids;
  375. for (user_id of Array.from(user_ids)) {
  376. if (UpdatesManager._validUserId(user_id)) {
  377. update.meta.users.push(fetchedUserInfo[user_id]);
  378. } else {
  379. update.meta.users.push(null);
  380. }
  381. }
  382. }
  383. return callback(null, updates);
  384. });
  385. },
  386. _validUserId(user_id) {
  387. if ((user_id == null)) {
  388. return false;
  389. } else {
  390. return !!user_id.match(/^[a-f0-9]{24}$/);
  391. }
  392. },
  393. TIME_BETWEEN_DISTINCT_UPDATES: (fiveMinutes = 5 * 60 * 1000),
  394. SPLIT_ON_DELETE_SIZE: 16, // characters
  395. _summarizeUpdates(updates, existingSummarizedUpdates) {
  396. if (existingSummarizedUpdates == null) { existingSummarizedUpdates = []; }
  397. const summarizedUpdates = existingSummarizedUpdates.slice();
  398. let previousUpdateWasBigDelete = false;
  399. for (const update of Array.from(updates)) {
  400. var doc_id;
  401. const earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1];
  402. let shouldConcat = false;
  403. // If a user inserts some text, then deletes a big chunk including that text,
  404. // the update we show might concat the insert and delete, and there will be no sign
  405. // of that insert having happened, or be able to restore to it (restoring after a big delete is common).
  406. // So, we split the summary on 'big' deletes. However, we've stepping backwards in time with
  407. // most recent changes considered first, so if this update is a big delete, we want to start
  408. // a new summarized update next timge, hence we monitor the previous update.
  409. if (previousUpdateWasBigDelete) {
  410. shouldConcat = false;
  411. } else if (earliestUpdate && ((earliestUpdate.meta.end_ts - update.meta.start_ts) < this.TIME_BETWEEN_DISTINCT_UPDATES)) {
  412. // We're going backwards in time through the updates, so only combine if this update starts less than 5 minutes before
  413. // the end of current summarized block, so no block spans more than 5 minutes.
  414. shouldConcat = true;
  415. }
  416. let isBigDelete = false;
  417. for (const op of Array.from(update.op || [])) {
  418. if ((op.d != null) && (op.d.length > this.SPLIT_ON_DELETE_SIZE)) {
  419. isBigDelete = true;
  420. }
  421. }
  422. previousUpdateWasBigDelete = isBigDelete;
  423. if (shouldConcat) {
  424. // check if the user in this update is already present in the earliest update,
  425. // if not, add them to the users list of the earliest update
  426. earliestUpdate.meta.user_ids = _.union(earliestUpdate.meta.user_ids, [update.meta.user_id]);
  427. doc_id = update.doc_id.toString();
  428. const doc = earliestUpdate.docs[doc_id];
  429. if (doc != null) {
  430. doc.fromV = Math.min(doc.fromV, update.v);
  431. doc.toV = Math.max(doc.toV, update.v);
  432. } else {
  433. earliestUpdate.docs[doc_id] = {
  434. fromV: update.v,
  435. toV: update.v
  436. };
  437. }
  438. earliestUpdate.meta.start_ts = Math.min(earliestUpdate.meta.start_ts, update.meta.start_ts);
  439. earliestUpdate.meta.end_ts = Math.max(earliestUpdate.meta.end_ts, update.meta.end_ts);
  440. } else {
  441. const newUpdate = {
  442. meta: {
  443. user_ids: [],
  444. start_ts: update.meta.start_ts,
  445. end_ts: update.meta.end_ts
  446. },
  447. docs: {}
  448. };
  449. newUpdate.docs[update.doc_id.toString()] = {
  450. fromV: update.v,
  451. toV: update.v
  452. };
  453. newUpdate.meta.user_ids.push(update.meta.user_id);
  454. summarizedUpdates.push(newUpdate);
  455. }
  456. }
  457. return summarizedUpdates;
  458. }
  459. });
  460. function __guard__(value, transform) {
  461. return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
  462. }