DocumentManager.js 19 KB

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