UpdateManager.js 15 KB

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