UpdateManager.js 14 KB

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