DocumentManager.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  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. const op = DiffCodec.diffAsShareJsOp(oldLines, newLines)
  194. if (undoing) {
  195. for (const o of op || []) {
  196. o.u = true
  197. } // Turn on undo flag for each op for track changes
  198. }
  199. const update = {
  200. doc: docId,
  201. op,
  202. v: version,
  203. meta: {
  204. type: 'external',
  205. source,
  206. user_id: userId,
  207. },
  208. }
  209. // Keep track of external updates, whether they are for live documents
  210. // (flush) or unloaded documents (evict), and whether the update is a no-op.
  211. Metrics.inc('external-update', 1, {
  212. status: op.length > 0 ? 'diff' : 'noop',
  213. method: alreadyLoaded ? 'flush' : 'evict',
  214. path: source,
  215. })
  216. const applyUpdateIfNeeded = cb => {
  217. if (op.length === 0) {
  218. // Do not notify the frontend about a noop update.
  219. // We still want to execute the callback code below
  220. // to evict the doc if we loaded it into redis for
  221. // this update, otherwise the doc would never be
  222. // removed from redis.
  223. return cb(null)
  224. }
  225. UpdateManager.applyUpdate(projectId, docId, update, cb)
  226. }
  227. applyUpdateIfNeeded(error => {
  228. if (error) {
  229. return callback(error)
  230. }
  231. // If the document was loaded already, then someone has it open
  232. // in a project, and the usual flushing mechanism will happen.
  233. // Otherwise we should remove it immediately since nothing else
  234. // is using it.
  235. if (alreadyLoaded) {
  236. DocumentManager.flushDocIfLoaded(
  237. projectId,
  238. docId,
  239. (error, result) => {
  240. if (error) {
  241. return callback(error)
  242. }
  243. callback(null, result)
  244. }
  245. )
  246. } else {
  247. DocumentManager.flushAndDeleteDoc(
  248. projectId,
  249. docId,
  250. {},
  251. (error, result) => {
  252. // There is no harm in flushing project history if the previous
  253. // call failed and sometimes it is required
  254. HistoryManager.flushProjectChangesAsync(projectId)
  255. if (error) {
  256. return callback(error)
  257. }
  258. callback(null, result)
  259. }
  260. )
  261. }
  262. })
  263. }
  264. )
  265. },
  266. flushDocIfLoaded(projectId, docId, _callback) {
  267. const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
  268. const callback = (...args) => {
  269. timer.done()
  270. _callback(...args)
  271. }
  272. RedisManager.getDoc(
  273. projectId,
  274. docId,
  275. (
  276. error,
  277. lines,
  278. version,
  279. ranges,
  280. pathname,
  281. projectHistoryId,
  282. unflushedTime,
  283. lastUpdatedAt,
  284. lastUpdatedBy
  285. ) => {
  286. if (error) {
  287. return callback(error)
  288. }
  289. if (lines == null || version == null) {
  290. Metrics.inc('flush-doc-if-loaded', 1, { status: 'not-loaded' })
  291. logger.debug(
  292. { projectId, docId },
  293. 'doc is not loaded so not flushing'
  294. )
  295. // TODO: return a flag to bail out, as we go on to remove doc from memory?
  296. callback(null)
  297. } else if (unflushedTime == null) {
  298. Metrics.inc('flush-doc-if-loaded', 1, { status: 'unmodified' })
  299. logger.debug(
  300. { projectId, docId },
  301. 'doc is not modified so not flushing'
  302. )
  303. callback(null)
  304. } else {
  305. logger.debug({ projectId, docId, version }, 'flushing doc')
  306. Metrics.inc('flush-doc-if-loaded', 1, { status: 'modified' })
  307. PersistenceManager.setDoc(
  308. projectId,
  309. docId,
  310. lines,
  311. version,
  312. ranges,
  313. lastUpdatedAt,
  314. lastUpdatedBy,
  315. (error, result) => {
  316. if (error) {
  317. return callback(error)
  318. }
  319. RedisManager.clearUnflushedTime(docId, err => {
  320. if (err) {
  321. return callback(err)
  322. }
  323. callback(null, result)
  324. })
  325. }
  326. )
  327. }
  328. }
  329. )
  330. },
  331. flushAndDeleteDoc(projectId, docId, options, _callback) {
  332. const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
  333. const callback = (...args) => {
  334. timer.done()
  335. _callback(...args)
  336. }
  337. DocumentManager.flushDocIfLoaded(projectId, docId, (error, result) => {
  338. if (error) {
  339. if (options.ignoreFlushErrors) {
  340. logger.warn(
  341. { projectId, docId, err: error },
  342. 'ignoring flush error while deleting document'
  343. )
  344. } else {
  345. return callback(error)
  346. }
  347. }
  348. RedisManager.removeDocFromMemory(projectId, docId, error => {
  349. if (error) {
  350. return callback(error)
  351. }
  352. callback(null, result)
  353. })
  354. })
  355. },
  356. acceptChanges(projectId, docId, changeIds, _callback) {
  357. if (changeIds == null) {
  358. changeIds = []
  359. }
  360. const timer = new Metrics.Timer('docManager.acceptChanges')
  361. const callback = (...args) => {
  362. timer.done()
  363. _callback(...args)
  364. }
  365. DocumentManager.getDoc(
  366. projectId,
  367. docId,
  368. (error, lines, version, ranges) => {
  369. if (error) {
  370. return callback(error)
  371. }
  372. if (lines == null || version == null) {
  373. return callback(
  374. new Errors.NotFoundError(`document not found: ${docId}`)
  375. )
  376. }
  377. let newRanges
  378. try {
  379. newRanges = RangesManager.acceptChanges(changeIds, ranges)
  380. } catch (err) {
  381. return callback(err)
  382. }
  383. RedisManager.updateDocument(
  384. projectId,
  385. docId,
  386. lines,
  387. version,
  388. [],
  389. newRanges,
  390. {},
  391. error => {
  392. if (error) {
  393. return callback(error)
  394. }
  395. callback()
  396. }
  397. )
  398. }
  399. )
  400. },
  401. deleteComment(projectId, docId, commentId, userId, _callback) {
  402. const timer = new Metrics.Timer('docManager.deleteComment')
  403. const callback = (...args) => {
  404. timer.done()
  405. _callback(...args)
  406. }
  407. DocumentManager.getDoc(
  408. projectId,
  409. docId,
  410. (
  411. error,
  412. lines,
  413. version,
  414. ranges,
  415. pathname,
  416. projectHistoryId,
  417. unflushedTime,
  418. alreadyLoaded,
  419. historyRangesSupport
  420. ) => {
  421. if (error) {
  422. return callback(error)
  423. }
  424. if (lines == null || version == null) {
  425. return callback(
  426. new Errors.NotFoundError(`document not found: ${docId}`)
  427. )
  428. }
  429. let newRanges
  430. try {
  431. newRanges = RangesManager.deleteComment(commentId, ranges)
  432. } catch (err) {
  433. return callback(err)
  434. }
  435. RedisManager.updateDocument(
  436. projectId,
  437. docId,
  438. lines,
  439. version,
  440. [],
  441. newRanges,
  442. {},
  443. error => {
  444. if (error) {
  445. return callback(error)
  446. }
  447. if (historyRangesSupport) {
  448. ProjectHistoryRedisManager.queueOps(
  449. projectId,
  450. JSON.stringify({
  451. pathname,
  452. deleteComment: commentId,
  453. meta: {
  454. ts: new Date(),
  455. user_id: userId,
  456. },
  457. }),
  458. error => {
  459. if (error) {
  460. return callback(error)
  461. }
  462. callback()
  463. }
  464. )
  465. } else {
  466. callback()
  467. }
  468. }
  469. )
  470. }
  471. )
  472. },
  473. renameDoc(projectId, docId, userId, update, projectHistoryId, _callback) {
  474. const timer = new Metrics.Timer('docManager.updateProject')
  475. const callback = (...args) => {
  476. timer.done()
  477. _callback(...args)
  478. }
  479. RedisManager.renameDoc(
  480. projectId,
  481. docId,
  482. userId,
  483. update,
  484. projectHistoryId,
  485. callback
  486. )
  487. },
  488. getDocAndFlushIfOld(projectId, docId, callback) {
  489. DocumentManager.getDoc(
  490. projectId,
  491. docId,
  492. (
  493. error,
  494. lines,
  495. version,
  496. ranges,
  497. pathname,
  498. projectHistoryId,
  499. unflushedTime,
  500. alreadyLoaded
  501. ) => {
  502. if (error) {
  503. return callback(error)
  504. }
  505. // if doc was already loaded see if it needs to be flushed
  506. if (
  507. alreadyLoaded &&
  508. unflushedTime != null &&
  509. Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
  510. ) {
  511. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  512. if (error) {
  513. return callback(error)
  514. }
  515. callback(null, lines, version)
  516. })
  517. } else {
  518. callback(null, lines, version)
  519. }
  520. }
  521. )
  522. },
  523. resyncDocContents(projectId, docId, path, callback) {
  524. logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
  525. RedisManager.getDoc(
  526. projectId,
  527. docId,
  528. (error, lines, version, ranges, pathname, projectHistoryId) => {
  529. if (error) {
  530. return callback(error)
  531. }
  532. // To avoid issues where the same docId appears with different paths,
  533. // we use the path from the resyncProjectStructure update. If we used
  534. // the path from the getDoc call to web then the two occurences of the
  535. // docId would map to the same path, and this would be rejected by
  536. // project-history as an unexpected resyncDocContent update.
  537. if (lines == null || version == null) {
  538. logger.debug(
  539. { projectId, docId },
  540. 'resyncing doc contents - not found in redis - retrieving from web'
  541. )
  542. PersistenceManager.getDoc(
  543. projectId,
  544. docId,
  545. { peek: true },
  546. (error, lines, version, ranges, pathname, projectHistoryId) => {
  547. if (error) {
  548. logger.error(
  549. { projectId, docId, getDocError: error },
  550. 'resyncing doc contents - error retrieving from web'
  551. )
  552. return callback(error)
  553. }
  554. ProjectHistoryRedisManager.queueResyncDocContent(
  555. projectId,
  556. projectHistoryId,
  557. docId,
  558. lines,
  559. version,
  560. path, // use the path from the resyncProjectStructure update
  561. callback
  562. )
  563. }
  564. )
  565. } else {
  566. logger.debug(
  567. { projectId, docId },
  568. 'resyncing doc contents - doc in redis - will queue in redis'
  569. )
  570. ProjectHistoryRedisManager.queueResyncDocContent(
  571. projectId,
  572. projectHistoryId,
  573. docId,
  574. lines,
  575. version,
  576. path, // use the path from the resyncProjectStructure update
  577. callback
  578. )
  579. }
  580. }
  581. )
  582. },
  583. getDocWithLock(projectId, docId, callback) {
  584. const UpdateManager = require('./UpdateManager')
  585. UpdateManager.lockUpdatesAndDo(
  586. DocumentManager.getDoc,
  587. projectId,
  588. docId,
  589. callback
  590. )
  591. },
  592. getDocAndRecentOpsWithLock(projectId, docId, fromVersion, callback) {
  593. const UpdateManager = require('./UpdateManager')
  594. UpdateManager.lockUpdatesAndDo(
  595. DocumentManager.getDocAndRecentOps,
  596. projectId,
  597. docId,
  598. fromVersion,
  599. callback
  600. )
  601. },
  602. getDocAndFlushIfOldWithLock(projectId, docId, callback) {
  603. const UpdateManager = require('./UpdateManager')
  604. UpdateManager.lockUpdatesAndDo(
  605. DocumentManager.getDocAndFlushIfOld,
  606. projectId,
  607. docId,
  608. callback
  609. )
  610. },
  611. setDocWithLock(projectId, docId, lines, source, userId, undoing, callback) {
  612. const UpdateManager = require('./UpdateManager')
  613. UpdateManager.lockUpdatesAndDo(
  614. DocumentManager.setDoc,
  615. projectId,
  616. docId,
  617. lines,
  618. source,
  619. userId,
  620. undoing,
  621. callback
  622. )
  623. },
  624. flushDocIfLoadedWithLock(projectId, docId, callback) {
  625. const UpdateManager = require('./UpdateManager')
  626. UpdateManager.lockUpdatesAndDo(
  627. DocumentManager.flushDocIfLoaded,
  628. projectId,
  629. docId,
  630. callback
  631. )
  632. },
  633. flushAndDeleteDocWithLock(projectId, docId, options, callback) {
  634. const UpdateManager = require('./UpdateManager')
  635. UpdateManager.lockUpdatesAndDo(
  636. DocumentManager.flushAndDeleteDoc,
  637. projectId,
  638. docId,
  639. options,
  640. callback
  641. )
  642. },
  643. acceptChangesWithLock(projectId, docId, changeIds, callback) {
  644. const UpdateManager = require('./UpdateManager')
  645. UpdateManager.lockUpdatesAndDo(
  646. DocumentManager.acceptChanges,
  647. projectId,
  648. docId,
  649. changeIds,
  650. callback
  651. )
  652. },
  653. deleteCommentWithLock(projectId, docId, threadId, userId, callback) {
  654. const UpdateManager = require('./UpdateManager')
  655. UpdateManager.lockUpdatesAndDo(
  656. DocumentManager.deleteComment,
  657. projectId,
  658. docId,
  659. threadId,
  660. userId,
  661. callback
  662. )
  663. },
  664. renameDocWithLock(
  665. projectId,
  666. docId,
  667. userId,
  668. update,
  669. projectHistoryId,
  670. callback
  671. ) {
  672. const UpdateManager = require('./UpdateManager')
  673. UpdateManager.lockUpdatesAndDo(
  674. DocumentManager.renameDoc,
  675. projectId,
  676. docId,
  677. userId,
  678. update,
  679. projectHistoryId,
  680. callback
  681. )
  682. },
  683. resyncDocContentsWithLock(projectId, docId, path, callback) {
  684. const UpdateManager = require('./UpdateManager')
  685. UpdateManager.lockUpdatesAndDo(
  686. DocumentManager.resyncDocContents,
  687. projectId,
  688. docId,
  689. path,
  690. callback
  691. )
  692. },
  693. }
  694. module.exports = DocumentManager
  695. module.exports.promises = promisifyAll(DocumentManager, {
  696. multiResult: {
  697. getDoc: [
  698. 'lines',
  699. 'version',
  700. 'ranges',
  701. 'pathname',
  702. 'projectHistoryId',
  703. 'unflushedTime',
  704. 'alreadyLoaded',
  705. 'historyRangesSupport',
  706. ],
  707. getDocWithLock: [
  708. 'lines',
  709. 'version',
  710. 'ranges',
  711. 'pathname',
  712. 'projectHistoryId',
  713. 'unflushedTime',
  714. 'alreadyLoaded',
  715. 'historyRangesSupport',
  716. ],
  717. getDocAndFlushIfOld: ['lines', 'version'],
  718. getDocAndFlushIfOldWithLock: ['lines', 'version'],
  719. getDocAndRecentOps: [
  720. 'lines',
  721. 'version',
  722. 'ops',
  723. 'ranges',
  724. 'pathname',
  725. 'projectHistoryId',
  726. ],
  727. getDocAndRecentOpsWithLock: [
  728. 'lines',
  729. 'version',
  730. 'ops',
  731. 'ranges',
  732. 'pathname',
  733. 'projectHistoryId',
  734. ],
  735. },
  736. })