DocumentManager.js 18 KB

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