UpdateManager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /* eslint-disable
  2. no-unused-vars,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS201: Simplify complex destructure assignments
  11. * DS205: Consider reworking code to avoid use of IIFEs
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let UpdateManager
  16. const LockManager = require('./LockManager')
  17. const RedisManager = require('./RedisManager')
  18. const RealTimeRedisManager = require('./RealTimeRedisManager')
  19. const ShareJsUpdateManager = require('./ShareJsUpdateManager')
  20. const HistoryManager = require('./HistoryManager')
  21. const Settings = require('@overleaf/settings')
  22. const _ = require('lodash')
  23. const async = require('async')
  24. const logger = require('@overleaf/logger')
  25. const Metrics = require('./Metrics')
  26. const Errors = require('./Errors')
  27. const DocumentManager = require('./DocumentManager')
  28. const RangesManager = require('./RangesManager')
  29. const SnapshotManager = require('./SnapshotManager')
  30. const Profiler = require('./Profiler')
  31. module.exports = UpdateManager = {
  32. processOutstandingUpdates(projectId, docId, callback) {
  33. if (callback == null) {
  34. callback = function () {}
  35. }
  36. const timer = new Metrics.Timer('updateManager.processOutstandingUpdates')
  37. return UpdateManager.fetchAndApplyUpdates(
  38. projectId,
  39. docId,
  40. function (error) {
  41. timer.done()
  42. if (error != null) {
  43. return callback(error)
  44. }
  45. return callback()
  46. }
  47. )
  48. },
  49. processOutstandingUpdatesWithLock(projectId, docId, callback) {
  50. if (callback == null) {
  51. callback = function () {}
  52. }
  53. const profile = new Profiler('processOutstandingUpdatesWithLock', {
  54. project_id: projectId,
  55. doc_id: docId,
  56. })
  57. return LockManager.tryLock(docId, (error, gotLock, lockValue) => {
  58. if (error != null) {
  59. return callback(error)
  60. }
  61. if (!gotLock) {
  62. return callback()
  63. }
  64. profile.log('tryLock')
  65. return UpdateManager.processOutstandingUpdates(
  66. projectId,
  67. docId,
  68. function (error) {
  69. if (error != null) {
  70. return UpdateManager._handleErrorInsideLock(
  71. docId,
  72. lockValue,
  73. error,
  74. callback
  75. )
  76. }
  77. profile.log('processOutstandingUpdates')
  78. return LockManager.releaseLock(docId, lockValue, error => {
  79. if (error != null) {
  80. return callback(error)
  81. }
  82. profile.log('releaseLock').end()
  83. return UpdateManager.continueProcessingUpdatesWithLock(
  84. projectId,
  85. docId,
  86. callback
  87. )
  88. })
  89. }
  90. )
  91. })
  92. },
  93. continueProcessingUpdatesWithLock(projectId, docId, callback) {
  94. if (callback == null) {
  95. callback = function () {}
  96. }
  97. return RealTimeRedisManager.getUpdatesLength(docId, (error, length) => {
  98. if (error != null) {
  99. return callback(error)
  100. }
  101. if (length > 0) {
  102. return UpdateManager.processOutstandingUpdatesWithLock(
  103. projectId,
  104. docId,
  105. callback
  106. )
  107. } else {
  108. return callback()
  109. }
  110. })
  111. },
  112. fetchAndApplyUpdates(projectId, docId, callback) {
  113. if (callback == null) {
  114. callback = function () {}
  115. }
  116. const profile = new Profiler('fetchAndApplyUpdates', {
  117. project_id: projectId,
  118. doc_id: docId,
  119. })
  120. return RealTimeRedisManager.getPendingUpdatesForDoc(
  121. docId,
  122. (error, updates) => {
  123. if (error != null) {
  124. return callback(error)
  125. }
  126. logger.debug(
  127. { projectId, docId, count: updates.length },
  128. 'processing updates'
  129. )
  130. if (updates.length === 0) {
  131. return callback()
  132. }
  133. profile.log('getPendingUpdatesForDoc')
  134. const doUpdate = (update, cb) =>
  135. UpdateManager.applyUpdate(projectId, docId, update, function (err) {
  136. profile.log('applyUpdate')
  137. return cb(err)
  138. })
  139. const finalCallback = function (err) {
  140. profile.log('async done').end()
  141. return callback(err)
  142. }
  143. return async.eachSeries(updates, doUpdate, finalCallback)
  144. }
  145. )
  146. },
  147. applyUpdate(projectId, docId, update, _callback) {
  148. if (_callback == null) {
  149. _callback = function () {}
  150. }
  151. const callback = function (error) {
  152. if (error != null) {
  153. RealTimeRedisManager.sendData({
  154. project_id: projectId,
  155. doc_id: docId,
  156. error: error.message || error,
  157. })
  158. profile.log('sendData')
  159. }
  160. profile.end()
  161. return _callback(error)
  162. }
  163. const profile = new Profiler('applyUpdate', {
  164. project_id: projectId,
  165. doc_id: docId,
  166. })
  167. UpdateManager._sanitizeUpdate(update)
  168. profile.log('sanitizeUpdate', { sync: true })
  169. return DocumentManager.getDoc(
  170. projectId,
  171. docId,
  172. function (error, lines, version, ranges, pathname, projectHistoryId) {
  173. profile.log('getDoc')
  174. if (error != null) {
  175. return callback(error)
  176. }
  177. if (lines == null || version == null) {
  178. return callback(
  179. new Errors.NotFoundError(`document not found: ${docId}`)
  180. )
  181. }
  182. const previousVersion = version
  183. const incomingUpdateVersion = update.v
  184. return ShareJsUpdateManager.applyUpdate(
  185. projectId,
  186. docId,
  187. update,
  188. lines,
  189. version,
  190. function (error, updatedDocLines, version, appliedOps) {
  191. profile.log('sharejs.applyUpdate', {
  192. // only synchronous when the update applies directly to the
  193. // doc version, otherwise getPreviousDocOps is called.
  194. sync: incomingUpdateVersion === previousVersion,
  195. })
  196. if (error != null) {
  197. return callback(error)
  198. }
  199. return RangesManager.applyUpdate(
  200. projectId,
  201. docId,
  202. ranges,
  203. appliedOps,
  204. updatedDocLines,
  205. function (error, newRanges, rangesWereCollapsed) {
  206. UpdateManager._addProjectHistoryMetadataToOps(
  207. appliedOps,
  208. pathname,
  209. projectHistoryId,
  210. lines
  211. )
  212. profile.log('RangesManager.applyUpdate', { sync: true })
  213. if (error != null) {
  214. return callback(error)
  215. }
  216. return RedisManager.updateDocument(
  217. projectId,
  218. docId,
  219. updatedDocLines,
  220. version,
  221. appliedOps,
  222. newRanges,
  223. update.meta,
  224. function (error, docOpsLength, projectOpsLength) {
  225. profile.log('RedisManager.updateDocument')
  226. if (error != null) {
  227. return callback(error)
  228. }
  229. return HistoryManager.recordAndFlushHistoryOps(
  230. projectId,
  231. docId,
  232. appliedOps,
  233. docOpsLength,
  234. projectOpsLength,
  235. function (error) {
  236. profile.log('recordAndFlushHistoryOps')
  237. if (error != null) {
  238. return callback(error)
  239. }
  240. if (rangesWereCollapsed) {
  241. Metrics.inc('doc-snapshot')
  242. logger.debug(
  243. {
  244. projectId,
  245. docId,
  246. previousVersion,
  247. lines,
  248. ranges,
  249. update,
  250. },
  251. 'update collapsed some ranges, snapshotting previous content'
  252. )
  253. // Do this last, since it's a mongo call, and so potentially longest running
  254. // If it overruns the lock, it's ok, since all of our redis work is done
  255. return SnapshotManager.recordSnapshot(
  256. projectId,
  257. docId,
  258. previousVersion,
  259. pathname,
  260. lines,
  261. ranges,
  262. function (error) {
  263. if (error != null) {
  264. logger.error(
  265. {
  266. err: error,
  267. projectId,
  268. docId,
  269. version,
  270. lines,
  271. ranges,
  272. },
  273. 'error recording snapshot'
  274. )
  275. return callback(error)
  276. } else {
  277. return callback()
  278. }
  279. }
  280. )
  281. } else {
  282. return callback()
  283. }
  284. }
  285. )
  286. }
  287. )
  288. }
  289. )
  290. }
  291. )
  292. }
  293. )
  294. },
  295. lockUpdatesAndDo(method, projectId, docId, ...rest) {
  296. const adjustedLength = Math.max(rest.length, 1)
  297. const args = rest.slice(0, adjustedLength - 1)
  298. const callback = rest[adjustedLength - 1]
  299. const profile = new Profiler('lockUpdatesAndDo', {
  300. project_id: projectId,
  301. doc_id: docId,
  302. })
  303. return LockManager.getLock(docId, function (error, lockValue) {
  304. profile.log('getLock')
  305. if (error != null) {
  306. return callback(error)
  307. }
  308. return UpdateManager.processOutstandingUpdates(
  309. projectId,
  310. docId,
  311. function (error) {
  312. if (error != null) {
  313. return UpdateManager._handleErrorInsideLock(
  314. docId,
  315. lockValue,
  316. error,
  317. callback
  318. )
  319. }
  320. profile.log('processOutstandingUpdates')
  321. return method(
  322. projectId,
  323. docId,
  324. ...Array.from(args),
  325. function (error, ...responseArgs) {
  326. if (error != null) {
  327. return UpdateManager._handleErrorInsideLock(
  328. docId,
  329. lockValue,
  330. error,
  331. callback
  332. )
  333. }
  334. profile.log('method')
  335. return LockManager.releaseLock(
  336. docId,
  337. lockValue,
  338. function (error) {
  339. if (error != null) {
  340. return callback(error)
  341. }
  342. profile.log('releaseLock').end()
  343. callback(null, ...Array.from(responseArgs))
  344. // We held the lock for a while so updates might have queued up
  345. return UpdateManager.continueProcessingUpdatesWithLock(
  346. projectId,
  347. docId,
  348. err => {
  349. if (err) {
  350. // The processing may fail for invalid user updates.
  351. // This can be very noisy, put them on level DEBUG
  352. // and record a metric.
  353. Metrics.inc('background-processing-updates-error')
  354. logger.debug(
  355. { err, projectId, docId },
  356. 'error processing updates in background'
  357. )
  358. }
  359. }
  360. )
  361. }
  362. )
  363. }
  364. )
  365. }
  366. )
  367. })
  368. },
  369. _handleErrorInsideLock(docId, lockValue, originalError, callback) {
  370. if (callback == null) {
  371. callback = function () {}
  372. }
  373. return LockManager.releaseLock(docId, lockValue, lockError =>
  374. callback(originalError)
  375. )
  376. },
  377. _sanitizeUpdate(update) {
  378. // In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
  379. //
  380. // From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
  381. // "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
  382. // for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
  383. // and one Low Surrogate. A single surrogate code point will never be assigned a character.""
  384. //
  385. // The main offender seems to be \uD835 as a stand alone character, which would be the first
  386. // 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
  387. // Something must be going on client side that is screwing up the encoding and splitting the
  388. // two 16-bit characters so that \uD835 is standalone.
  389. for (const op of Array.from(update.op || [])) {
  390. if (op.i != null) {
  391. // Replace high and low surrogate characters with 'replacement character' (\uFFFD)
  392. op.i = op.i.replace(/[\uD800-\uDFFF]/g, '\uFFFD')
  393. }
  394. }
  395. return update
  396. },
  397. _addProjectHistoryMetadataToOps(updates, pathname, projectHistoryId, lines) {
  398. let docLength = _.reduce(lines, (chars, line) => chars + line.length, 0)
  399. docLength += lines.length - 1 // count newline characters
  400. return updates.forEach(function (update) {
  401. update.projectHistoryId = projectHistoryId
  402. if (!update.meta) {
  403. update.meta = {}
  404. }
  405. update.meta.pathname = pathname
  406. update.meta.doc_length = docLength
  407. // Each update may contain multiple ops, i.e.
  408. // [{
  409. // ops: [{i: "foo", p: 4}, {d: "bar", p:8}]
  410. // }, {
  411. // ops: [{d: "baz", p: 40}, {i: "qux", p:8}]
  412. // }]
  413. // We want to include the doc_length at the start of each update,
  414. // before it's ops are applied. However, we need to track any
  415. // changes to it for the next update.
  416. return (() => {
  417. const result = []
  418. for (const op of Array.from(update.op)) {
  419. if (op.i != null) {
  420. docLength += op.i.length
  421. }
  422. if (op.d != null) {
  423. result.push((docLength -= op.d.length))
  424. } else {
  425. result.push(undefined)
  426. }
  427. }
  428. return result
  429. })()
  430. })
  431. },
  432. }