UpdateManager.js 13 KB

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