DocumentManager.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. const { promisifyAll } = require('@overleaf/promise-utils')
  2. const RedisManager = require('./RedisManager')
  3. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  4. const PersistenceManager = require('./PersistenceManager')
  5. const DiffCodec = require('./DiffCodec')
  6. const logger = require('@overleaf/logger')
  7. const Metrics = require('./Metrics')
  8. const HistoryManager = require('./HistoryManager')
  9. const Errors = require('./Errors')
  10. const RangesManager = require('./RangesManager')
  11. const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change
  12. const DocumentManager = {
  13. getDoc(projectId, docId, _callback) {
  14. const timer = new Metrics.Timer('docManager.getDoc')
  15. const callback = (...args) => {
  16. timer.done()
  17. _callback(...args)
  18. }
  19. RedisManager.getDoc(
  20. projectId,
  21. docId,
  22. (
  23. error,
  24. lines,
  25. version,
  26. ranges,
  27. pathname,
  28. projectHistoryId,
  29. unflushedTime,
  30. lastUpdatedAt,
  31. lastUpdatedBy,
  32. historyRangesSupport
  33. ) => {
  34. if (error) {
  35. return callback(error)
  36. }
  37. if (lines == null || version == null) {
  38. logger.debug(
  39. { projectId, docId },
  40. 'doc not in redis so getting from persistence API'
  41. )
  42. PersistenceManager.getDoc(
  43. projectId,
  44. docId,
  45. (
  46. error,
  47. lines,
  48. version,
  49. ranges,
  50. pathname,
  51. projectHistoryId,
  52. historyRangesSupport
  53. ) => {
  54. if (error) {
  55. return callback(error)
  56. }
  57. logger.debug(
  58. {
  59. projectId,
  60. docId,
  61. lines,
  62. version,
  63. pathname,
  64. projectHistoryId,
  65. historyRangesSupport,
  66. },
  67. 'got doc from persistence API'
  68. )
  69. RedisManager.putDocInMemory(
  70. projectId,
  71. docId,
  72. lines,
  73. version,
  74. ranges,
  75. pathname,
  76. projectHistoryId,
  77. historyRangesSupport,
  78. error => {
  79. if (error) {
  80. return callback(error)
  81. }
  82. callback(
  83. null,
  84. lines,
  85. version,
  86. ranges || {},
  87. pathname,
  88. projectHistoryId,
  89. null,
  90. false,
  91. historyRangesSupport
  92. )
  93. }
  94. )
  95. }
  96. )
  97. } else {
  98. callback(
  99. null,
  100. lines,
  101. version,
  102. ranges,
  103. pathname,
  104. projectHistoryId,
  105. unflushedTime,
  106. true,
  107. historyRangesSupport
  108. )
  109. }
  110. }
  111. )
  112. },
  113. getDocAndRecentOps(projectId, docId, fromVersion, _callback) {
  114. const timer = new Metrics.Timer('docManager.getDocAndRecentOps')
  115. const callback = (...args) => {
  116. timer.done()
  117. _callback(...args)
  118. }
  119. DocumentManager.getDoc(
  120. projectId,
  121. docId,
  122. (error, lines, version, ranges, pathname, projectHistoryId) => {
  123. if (error) {
  124. return callback(error)
  125. }
  126. if (fromVersion === -1) {
  127. callback(null, lines, version, [], ranges, pathname, projectHistoryId)
  128. } else {
  129. RedisManager.getPreviousDocOps(
  130. docId,
  131. fromVersion,
  132. version,
  133. (error, ops) => {
  134. if (error) {
  135. return callback(error)
  136. }
  137. callback(
  138. null,
  139. lines,
  140. version,
  141. ops,
  142. ranges,
  143. pathname,
  144. projectHistoryId
  145. )
  146. }
  147. )
  148. }
  149. }
  150. )
  151. },
  152. setDoc(projectId, docId, newLines, source, userId, undoing, _callback) {
  153. const timer = new Metrics.Timer('docManager.setDoc')
  154. const callback = (...args) => {
  155. timer.done()
  156. _callback(...args)
  157. }
  158. if (newLines == null) {
  159. return callback(new Error('No lines were provided to setDoc'))
  160. }
  161. const UpdateManager = require('./UpdateManager')
  162. DocumentManager.getDoc(
  163. projectId,
  164. docId,
  165. (
  166. error,
  167. oldLines,
  168. version,
  169. ranges,
  170. pathname,
  171. projectHistoryId,
  172. unflushedTime,
  173. alreadyLoaded
  174. ) => {
  175. if (error) {
  176. return callback(error)
  177. }
  178. if (
  179. oldLines != null &&
  180. oldLines.length > 0 &&
  181. oldLines[0].text != null
  182. ) {
  183. logger.debug(
  184. { docId, projectId, oldLines, newLines },
  185. 'document is JSON so not updating'
  186. )
  187. return callback(null)
  188. }
  189. logger.debug(
  190. { docId, projectId, oldLines, newLines },
  191. 'setting a document via http'
  192. )
  193. DiffCodec.diffAsShareJsOp(oldLines, newLines, (error, op) => {
  194. if (error) {
  195. return callback(error)
  196. }
  197. if (undoing) {
  198. for (const o of op || []) {
  199. o.u = true
  200. } // Turn on undo flag for each op for track changes
  201. }
  202. const update = {
  203. doc: docId,
  204. op,
  205. v: version,
  206. meta: {
  207. type: 'external',
  208. source,
  209. user_id: userId,
  210. },
  211. }
  212. // Keep track of external updates, whether they are for live documents
  213. // (flush) or unloaded documents (evict), and whether the update is a no-op.
  214. Metrics.inc('external-update', 1, {
  215. status: op.length > 0 ? 'diff' : 'noop',
  216. method: alreadyLoaded ? 'flush' : 'evict',
  217. path: source,
  218. })
  219. const applyUpdateIfNeeded = cb => {
  220. if (op.length === 0) {
  221. // Do not notify the frontend about a noop update.
  222. // We still want to execute the callback code below
  223. // to evict the doc if we loaded it into redis for
  224. // this update, otherwise the doc would never be
  225. // removed from redis.
  226. return cb(null)
  227. }
  228. UpdateManager.applyUpdate(projectId, docId, update, cb)
  229. }
  230. applyUpdateIfNeeded(error => {
  231. if (error) {
  232. return callback(error)
  233. }
  234. // If the document was loaded already, then someone has it open
  235. // in a project, and the usual flushing mechanism will happen.
  236. // Otherwise we should remove it immediately since nothing else
  237. // is using it.
  238. if (alreadyLoaded) {
  239. DocumentManager.flushDocIfLoaded(
  240. projectId,
  241. docId,
  242. (error, result) => {
  243. if (error) {
  244. return callback(error)
  245. }
  246. callback(null, result)
  247. }
  248. )
  249. } else {
  250. DocumentManager.flushAndDeleteDoc(
  251. projectId,
  252. docId,
  253. {},
  254. (error, result) => {
  255. // There is no harm in flushing project history if the previous
  256. // call failed and sometimes it is required
  257. HistoryManager.flushProjectChangesAsync(projectId)
  258. if (error) {
  259. return callback(error)
  260. }
  261. callback(null, result)
  262. }
  263. )
  264. }
  265. })
  266. })
  267. }
  268. )
  269. },
  270. flushDocIfLoaded(projectId, docId, _callback) {
  271. const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
  272. const callback = (...args) => {
  273. timer.done()
  274. _callback(...args)
  275. }
  276. RedisManager.getDoc(
  277. projectId,
  278. docId,
  279. (
  280. error,
  281. lines,
  282. version,
  283. ranges,
  284. pathname,
  285. projectHistoryId,
  286. unflushedTime,
  287. lastUpdatedAt,
  288. lastUpdatedBy
  289. ) => {
  290. if (error) {
  291. return callback(error)
  292. }
  293. if (lines == null || version == null) {
  294. Metrics.inc('flush-doc-if-loaded', 1, { status: 'not-loaded' })
  295. logger.debug(
  296. { projectId, docId },
  297. 'doc is not loaded so not flushing'
  298. )
  299. // TODO: return a flag to bail out, as we go on to remove doc from memory?
  300. callback(null)
  301. } else if (unflushedTime == null) {
  302. Metrics.inc('flush-doc-if-loaded', 1, { status: 'unmodified' })
  303. logger.debug(
  304. { projectId, docId },
  305. 'doc is not modified so not flushing'
  306. )
  307. callback(null)
  308. } else {
  309. logger.debug({ projectId, docId, version }, 'flushing doc')
  310. Metrics.inc('flush-doc-if-loaded', 1, { status: 'modified' })
  311. PersistenceManager.setDoc(
  312. projectId,
  313. docId,
  314. lines,
  315. version,
  316. ranges,
  317. lastUpdatedAt,
  318. lastUpdatedBy,
  319. (error, result) => {
  320. if (error) {
  321. return callback(error)
  322. }
  323. RedisManager.clearUnflushedTime(docId, err => {
  324. if (err) {
  325. return callback(err)
  326. }
  327. callback(null, result)
  328. })
  329. }
  330. )
  331. }
  332. }
  333. )
  334. },
  335. flushAndDeleteDoc(projectId, docId, options, _callback) {
  336. const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
  337. const callback = (...args) => {
  338. timer.done()
  339. _callback(...args)
  340. }
  341. DocumentManager.flushDocIfLoaded(projectId, docId, (error, result) => {
  342. if (error) {
  343. if (options.ignoreFlushErrors) {
  344. logger.warn(
  345. { projectId, docId, err: error },
  346. 'ignoring flush error while deleting document'
  347. )
  348. } else {
  349. return callback(error)
  350. }
  351. }
  352. RedisManager.removeDocFromMemory(projectId, docId, error => {
  353. if (error) {
  354. return callback(error)
  355. }
  356. callback(null, result)
  357. })
  358. })
  359. },
  360. acceptChanges(projectId, docId, changeIds, _callback) {
  361. if (changeIds == null) {
  362. changeIds = []
  363. }
  364. const timer = new Metrics.Timer('docManager.acceptChanges')
  365. const callback = (...args) => {
  366. timer.done()
  367. _callback(...args)
  368. }
  369. DocumentManager.getDoc(
  370. projectId,
  371. docId,
  372. (error, lines, version, ranges) => {
  373. if (error) {
  374. return callback(error)
  375. }
  376. if (lines == null || version == null) {
  377. return callback(
  378. new Errors.NotFoundError(`document not found: ${docId}`)
  379. )
  380. }
  381. let newRanges
  382. try {
  383. newRanges = RangesManager.acceptChanges(changeIds, ranges)
  384. } catch (err) {
  385. return callback(err)
  386. }
  387. RedisManager.updateDocument(
  388. projectId,
  389. docId,
  390. lines,
  391. version,
  392. [],
  393. newRanges,
  394. {},
  395. error => {
  396. if (error) {
  397. return callback(error)
  398. }
  399. callback()
  400. }
  401. )
  402. }
  403. )
  404. },
  405. deleteComment(projectId, docId, commentId, userId, _callback) {
  406. const timer = new Metrics.Timer('docManager.deleteComment')
  407. const callback = (...args) => {
  408. timer.done()
  409. _callback(...args)
  410. }
  411. DocumentManager.getDoc(
  412. projectId,
  413. docId,
  414. (
  415. error,
  416. lines,
  417. version,
  418. ranges,
  419. pathname,
  420. projectHistoryId,
  421. unflushedTime,
  422. alreadyLoaded,
  423. historyRangesSupport
  424. ) => {
  425. if (error) {
  426. return callback(error)
  427. }
  428. if (lines == null || version == null) {
  429. return callback(
  430. new Errors.NotFoundError(`document not found: ${docId}`)
  431. )
  432. }
  433. let newRanges
  434. try {
  435. newRanges = RangesManager.deleteComment(commentId, ranges)
  436. } catch (err) {
  437. return callback(err)
  438. }
  439. RedisManager.updateDocument(
  440. projectId,
  441. docId,
  442. lines,
  443. version,
  444. [],
  445. newRanges,
  446. {},
  447. error => {
  448. if (error) {
  449. return callback(error)
  450. }
  451. if (historyRangesSupport) {
  452. ProjectHistoryRedisManager.queueOps(
  453. projectId,
  454. JSON.stringify({
  455. pathname,
  456. deleteComment: commentId,
  457. meta: {
  458. ts: new Date(),
  459. user_id: userId,
  460. },
  461. }),
  462. error => {
  463. if (error) {
  464. return callback(error)
  465. }
  466. callback()
  467. }
  468. )
  469. } else {
  470. callback()
  471. }
  472. }
  473. )
  474. }
  475. )
  476. },
  477. renameDoc(projectId, docId, userId, update, projectHistoryId, _callback) {
  478. const timer = new Metrics.Timer('docManager.updateProject')
  479. const callback = (...args) => {
  480. timer.done()
  481. _callback(...args)
  482. }
  483. RedisManager.renameDoc(
  484. projectId,
  485. docId,
  486. userId,
  487. update,
  488. projectHistoryId,
  489. callback
  490. )
  491. },
  492. getDocAndFlushIfOld(projectId, docId, callback) {
  493. DocumentManager.getDoc(
  494. projectId,
  495. docId,
  496. (
  497. error,
  498. lines,
  499. version,
  500. ranges,
  501. pathname,
  502. projectHistoryId,
  503. unflushedTime,
  504. alreadyLoaded
  505. ) => {
  506. if (error) {
  507. return callback(error)
  508. }
  509. // if doc was already loaded see if it needs to be flushed
  510. if (
  511. alreadyLoaded &&
  512. unflushedTime != null &&
  513. Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
  514. ) {
  515. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  516. if (error) {
  517. return callback(error)
  518. }
  519. callback(null, lines, version)
  520. })
  521. } else {
  522. callback(null, lines, version)
  523. }
  524. }
  525. )
  526. },
  527. resyncDocContents(projectId, docId, path, callback) {
  528. logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
  529. RedisManager.getDoc(
  530. projectId,
  531. docId,
  532. (error, lines, version, ranges, pathname, projectHistoryId) => {
  533. if (error) {
  534. return callback(error)
  535. }
  536. // To avoid issues where the same docId appears with different paths,
  537. // we use the path from the resyncProjectStructure update. If we used
  538. // the path from the getDoc call to web then the two occurences of the
  539. // docId would map to the same path, and this would be rejected by
  540. // project-history as an unexpected resyncDocContent update.
  541. if (lines == null || version == null) {
  542. logger.debug(
  543. { projectId, docId },
  544. 'resyncing doc contents - not found in redis - retrieving from web'
  545. )
  546. PersistenceManager.getDoc(
  547. projectId,
  548. docId,
  549. { peek: true },
  550. (error, lines, version, ranges, pathname, projectHistoryId) => {
  551. if (error) {
  552. logger.error(
  553. { projectId, docId, getDocError: error },
  554. 'resyncing doc contents - error retrieving from web'
  555. )
  556. return callback(error)
  557. }
  558. ProjectHistoryRedisManager.queueResyncDocContent(
  559. projectId,
  560. projectHistoryId,
  561. docId,
  562. lines,
  563. version,
  564. path, // use the path from the resyncProjectStructure update
  565. callback
  566. )
  567. }
  568. )
  569. } else {
  570. logger.debug(
  571. { projectId, docId },
  572. 'resyncing doc contents - doc in redis - will queue in redis'
  573. )
  574. ProjectHistoryRedisManager.queueResyncDocContent(
  575. projectId,
  576. projectHistoryId,
  577. docId,
  578. lines,
  579. version,
  580. path, // use the path from the resyncProjectStructure update
  581. callback
  582. )
  583. }
  584. }
  585. )
  586. },
  587. getDocWithLock(projectId, docId, callback) {
  588. const UpdateManager = require('./UpdateManager')
  589. UpdateManager.lockUpdatesAndDo(
  590. DocumentManager.getDoc,
  591. projectId,
  592. docId,
  593. callback
  594. )
  595. },
  596. getDocAndRecentOpsWithLock(projectId, docId, fromVersion, callback) {
  597. const UpdateManager = require('./UpdateManager')
  598. UpdateManager.lockUpdatesAndDo(
  599. DocumentManager.getDocAndRecentOps,
  600. projectId,
  601. docId,
  602. fromVersion,
  603. callback
  604. )
  605. },
  606. getDocAndFlushIfOldWithLock(projectId, docId, callback) {
  607. const UpdateManager = require('./UpdateManager')
  608. UpdateManager.lockUpdatesAndDo(
  609. DocumentManager.getDocAndFlushIfOld,
  610. projectId,
  611. docId,
  612. callback
  613. )
  614. },
  615. setDocWithLock(projectId, docId, lines, source, userId, undoing, callback) {
  616. const UpdateManager = require('./UpdateManager')
  617. UpdateManager.lockUpdatesAndDo(
  618. DocumentManager.setDoc,
  619. projectId,
  620. docId,
  621. lines,
  622. source,
  623. userId,
  624. undoing,
  625. callback
  626. )
  627. },
  628. flushDocIfLoadedWithLock(projectId, docId, callback) {
  629. const UpdateManager = require('./UpdateManager')
  630. UpdateManager.lockUpdatesAndDo(
  631. DocumentManager.flushDocIfLoaded,
  632. projectId,
  633. docId,
  634. callback
  635. )
  636. },
  637. flushAndDeleteDocWithLock(projectId, docId, options, callback) {
  638. const UpdateManager = require('./UpdateManager')
  639. UpdateManager.lockUpdatesAndDo(
  640. DocumentManager.flushAndDeleteDoc,
  641. projectId,
  642. docId,
  643. options,
  644. callback
  645. )
  646. },
  647. acceptChangesWithLock(projectId, docId, changeIds, callback) {
  648. const UpdateManager = require('./UpdateManager')
  649. UpdateManager.lockUpdatesAndDo(
  650. DocumentManager.acceptChanges,
  651. projectId,
  652. docId,
  653. changeIds,
  654. callback
  655. )
  656. },
  657. deleteCommentWithLock(projectId, docId, threadId, userId, callback) {
  658. const UpdateManager = require('./UpdateManager')
  659. UpdateManager.lockUpdatesAndDo(
  660. DocumentManager.deleteComment,
  661. projectId,
  662. docId,
  663. threadId,
  664. userId,
  665. callback
  666. )
  667. },
  668. renameDocWithLock(
  669. projectId,
  670. docId,
  671. userId,
  672. update,
  673. projectHistoryId,
  674. callback
  675. ) {
  676. const UpdateManager = require('./UpdateManager')
  677. UpdateManager.lockUpdatesAndDo(
  678. DocumentManager.renameDoc,
  679. projectId,
  680. docId,
  681. userId,
  682. update,
  683. projectHistoryId,
  684. callback
  685. )
  686. },
  687. resyncDocContentsWithLock(projectId, docId, path, callback) {
  688. const UpdateManager = require('./UpdateManager')
  689. UpdateManager.lockUpdatesAndDo(
  690. DocumentManager.resyncDocContents,
  691. projectId,
  692. docId,
  693. path,
  694. callback
  695. )
  696. },
  697. }
  698. module.exports = DocumentManager
  699. module.exports.promises = promisifyAll(DocumentManager, {
  700. multiResult: {
  701. getDoc: [
  702. 'lines',
  703. 'version',
  704. 'ranges',
  705. 'pathname',
  706. 'projectHistoryId',
  707. 'unflushedTime',
  708. 'alreadyLoaded',
  709. 'historyRangesSupport',
  710. ],
  711. getDocWithLock: [
  712. 'lines',
  713. 'version',
  714. 'ranges',
  715. 'pathname',
  716. 'projectHistoryId',
  717. 'unflushedTime',
  718. 'alreadyLoaded',
  719. 'historyRangesSupport',
  720. ],
  721. getDocAndFlushIfOld: ['lines', 'version'],
  722. getDocAndFlushIfOldWithLock: ['lines', 'version'],
  723. getDocAndRecentOps: [
  724. 'lines',
  725. 'version',
  726. 'ops',
  727. 'ranges',
  728. 'pathname',
  729. 'projectHistoryId',
  730. ],
  731. getDocAndRecentOpsWithLock: [
  732. 'lines',
  733. 'version',
  734. 'ops',
  735. 'ranges',
  736. 'pathname',
  737. 'projectHistoryId',
  738. ],
  739. },
  740. })