DocumentManager.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  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. UpdateManager.applyUpdate(projectId, docId, update, error => {
  223. if (error) {
  224. return callback(error)
  225. }
  226. // If the document was loaded already, then someone has it open
  227. // in a project, and the usual flushing mechanism will happen.
  228. // Otherwise we should remove it immediately since nothing else
  229. // is using it.
  230. if (alreadyLoaded) {
  231. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  232. if (error) {
  233. return callback(error)
  234. }
  235. callback(null)
  236. })
  237. } else {
  238. DocumentManager.flushAndDeleteDoc(projectId, docId, {}, error => {
  239. // There is no harm in flushing project history if the previous
  240. // call failed and sometimes it is required
  241. HistoryManager.flushProjectChangesAsync(projectId)
  242. if (error) {
  243. return callback(error)
  244. }
  245. callback(null)
  246. })
  247. }
  248. })
  249. })
  250. }
  251. )
  252. },
  253. flushDocIfLoaded(projectId, docId, _callback) {
  254. const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
  255. const callback = (...args) => {
  256. timer.done()
  257. _callback(...args)
  258. }
  259. RedisManager.getDoc(
  260. projectId,
  261. docId,
  262. (
  263. error,
  264. lines,
  265. version,
  266. ranges,
  267. pathname,
  268. projectHistoryId,
  269. unflushedTime,
  270. lastUpdatedAt,
  271. lastUpdatedBy
  272. ) => {
  273. if (error) {
  274. return callback(error)
  275. }
  276. if (lines == null || version == null) {
  277. logger.debug(
  278. { projectId, docId },
  279. 'doc is not loaded so not flushing'
  280. )
  281. // TODO: return a flag to bail out, as we go on to remove doc from memory?
  282. callback(null)
  283. } else {
  284. logger.debug({ projectId, docId, version }, 'flushing doc')
  285. PersistenceManager.setDoc(
  286. projectId,
  287. docId,
  288. lines,
  289. version,
  290. ranges,
  291. lastUpdatedAt,
  292. lastUpdatedBy,
  293. error => {
  294. if (error) {
  295. return callback(error)
  296. }
  297. RedisManager.clearUnflushedTime(docId, callback)
  298. }
  299. )
  300. }
  301. }
  302. )
  303. },
  304. flushAndDeleteDoc(projectId, docId, options, _callback) {
  305. const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
  306. const callback = (...args) => {
  307. timer.done()
  308. _callback(...args)
  309. }
  310. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  311. if (error) {
  312. if (options.ignoreFlushErrors) {
  313. logger.warn(
  314. { projectId, docId, err: error },
  315. 'ignoring flush error while deleting document'
  316. )
  317. } else {
  318. return callback(error)
  319. }
  320. }
  321. // Flush in the background since it requires a http request
  322. HistoryManager.flushDocChangesAsync(projectId, docId)
  323. RedisManager.removeDocFromMemory(projectId, docId, error => {
  324. if (error) {
  325. return callback(error)
  326. }
  327. callback(null)
  328. })
  329. })
  330. },
  331. acceptChanges(projectId, docId, changeIds, _callback) {
  332. if (changeIds == null) {
  333. changeIds = []
  334. }
  335. const timer = new Metrics.Timer('docManager.acceptChanges')
  336. const callback = (...args) => {
  337. timer.done()
  338. _callback(...args)
  339. }
  340. DocumentManager.getDoc(
  341. projectId,
  342. docId,
  343. (error, lines, version, ranges) => {
  344. if (error) {
  345. return callback(error)
  346. }
  347. if (lines == null || version == null) {
  348. return callback(
  349. new Errors.NotFoundError(`document not found: ${docId}`)
  350. )
  351. }
  352. RangesManager.acceptChanges(changeIds, ranges, (error, newRanges) => {
  353. if (error) {
  354. return callback(error)
  355. }
  356. RedisManager.updateDocument(
  357. projectId,
  358. docId,
  359. lines,
  360. version,
  361. [],
  362. newRanges,
  363. {},
  364. error => {
  365. if (error) {
  366. return callback(error)
  367. }
  368. callback()
  369. }
  370. )
  371. })
  372. }
  373. )
  374. },
  375. deleteComment(projectId, docId, commentId, _callback) {
  376. const timer = new Metrics.Timer('docManager.deleteComment')
  377. const callback = (...args) => {
  378. timer.done()
  379. _callback(...args)
  380. }
  381. DocumentManager.getDoc(
  382. projectId,
  383. docId,
  384. (error, lines, version, ranges) => {
  385. if (error) {
  386. return callback(error)
  387. }
  388. if (lines == null || version == null) {
  389. return callback(
  390. new Errors.NotFoundError(`document not found: ${docId}`)
  391. )
  392. }
  393. RangesManager.deleteComment(commentId, ranges, (error, newRanges) => {
  394. if (error) {
  395. return callback(error)
  396. }
  397. RedisManager.updateDocument(
  398. projectId,
  399. docId,
  400. lines,
  401. version,
  402. [],
  403. newRanges,
  404. {},
  405. error => {
  406. if (error) {
  407. return callback(error)
  408. }
  409. callback()
  410. }
  411. )
  412. })
  413. }
  414. )
  415. },
  416. renameDoc(projectId, docId, userId, update, projectHistoryId, _callback) {
  417. const timer = new Metrics.Timer('docManager.updateProject')
  418. const callback = (...args) => {
  419. timer.done()
  420. _callback(...args)
  421. }
  422. RedisManager.renameDoc(
  423. projectId,
  424. docId,
  425. userId,
  426. update,
  427. projectHistoryId,
  428. callback
  429. )
  430. },
  431. getDocAndFlushIfOld(projectId, docId, callback) {
  432. DocumentManager.getDoc(
  433. projectId,
  434. docId,
  435. (
  436. error,
  437. lines,
  438. version,
  439. ranges,
  440. pathname,
  441. projectHistoryId,
  442. unflushedTime,
  443. alreadyLoaded
  444. ) => {
  445. if (error) {
  446. return callback(error)
  447. }
  448. // if doc was already loaded see if it needs to be flushed
  449. if (
  450. alreadyLoaded &&
  451. unflushedTime != null &&
  452. Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
  453. ) {
  454. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  455. if (error) {
  456. return callback(error)
  457. }
  458. callback(null, lines, version)
  459. })
  460. } else {
  461. callback(null, lines, version)
  462. }
  463. }
  464. )
  465. },
  466. resyncDocContents(projectId, docId, path, callback) {
  467. logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
  468. RedisManager.getDoc(
  469. projectId,
  470. docId,
  471. (error, lines, version, ranges, pathname, projectHistoryId) => {
  472. if (error) {
  473. return callback(error)
  474. }
  475. // To avoid issues where the same docId appears with different paths,
  476. // we use the path from the resyncProjectStructure update. If we used
  477. // the path from the getDoc call to web then the two occurences of the
  478. // docId would map to the same path, and this would be rejected by
  479. // project-history as an unexpected resyncDocContent update.
  480. if (lines == null || version == null) {
  481. logger.debug(
  482. { projectId, docId },
  483. 'resyncing doc contents - not found in redis - retrieving from web'
  484. )
  485. PersistenceManager.getDoc(
  486. projectId,
  487. docId,
  488. { peek: true },
  489. (error, lines, version, ranges, pathname, projectHistoryId) => {
  490. if (error) {
  491. logger.error(
  492. { projectId, docId, getDocError: error },
  493. 'resyncing doc contents - error retrieving from web'
  494. )
  495. return callback(error)
  496. }
  497. ProjectHistoryRedisManager.queueResyncDocContent(
  498. projectId,
  499. projectHistoryId,
  500. docId,
  501. lines,
  502. version,
  503. path, // use the path from the resyncProjectStructure update
  504. callback
  505. )
  506. }
  507. )
  508. } else {
  509. logger.debug(
  510. { projectId, docId },
  511. 'resyncing doc contents - doc in redis - will queue in redis'
  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. )
  525. },
  526. getDocWithLock(projectId, docId, callback) {
  527. const UpdateManager = require('./UpdateManager')
  528. UpdateManager.lockUpdatesAndDo(
  529. DocumentManager.getDoc,
  530. projectId,
  531. docId,
  532. callback
  533. )
  534. },
  535. getDocAndRecentOpsWithLock(projectId, docId, fromVersion, callback) {
  536. const UpdateManager = require('./UpdateManager')
  537. UpdateManager.lockUpdatesAndDo(
  538. DocumentManager.getDocAndRecentOps,
  539. projectId,
  540. docId,
  541. fromVersion,
  542. callback
  543. )
  544. },
  545. getDocAndFlushIfOldWithLock(projectId, docId, callback) {
  546. const UpdateManager = require('./UpdateManager')
  547. UpdateManager.lockUpdatesAndDo(
  548. DocumentManager.getDocAndFlushIfOld,
  549. projectId,
  550. docId,
  551. callback
  552. )
  553. },
  554. setDocWithLock(projectId, docId, lines, source, userId, undoing, callback) {
  555. const UpdateManager = require('./UpdateManager')
  556. UpdateManager.lockUpdatesAndDo(
  557. DocumentManager.setDoc,
  558. projectId,
  559. docId,
  560. lines,
  561. source,
  562. userId,
  563. undoing,
  564. callback
  565. )
  566. },
  567. flushDocIfLoadedWithLock(projectId, docId, callback) {
  568. const UpdateManager = require('./UpdateManager')
  569. UpdateManager.lockUpdatesAndDo(
  570. DocumentManager.flushDocIfLoaded,
  571. projectId,
  572. docId,
  573. callback
  574. )
  575. },
  576. flushAndDeleteDocWithLock(projectId, docId, options, callback) {
  577. const UpdateManager = require('./UpdateManager')
  578. UpdateManager.lockUpdatesAndDo(
  579. DocumentManager.flushAndDeleteDoc,
  580. projectId,
  581. docId,
  582. options,
  583. callback
  584. )
  585. },
  586. acceptChangesWithLock(projectId, docId, changeIds, callback) {
  587. const UpdateManager = require('./UpdateManager')
  588. UpdateManager.lockUpdatesAndDo(
  589. DocumentManager.acceptChanges,
  590. projectId,
  591. docId,
  592. changeIds,
  593. callback
  594. )
  595. },
  596. deleteCommentWithLock(projectId, docId, threadId, callback) {
  597. const UpdateManager = require('./UpdateManager')
  598. UpdateManager.lockUpdatesAndDo(
  599. DocumentManager.deleteComment,
  600. projectId,
  601. docId,
  602. threadId,
  603. callback
  604. )
  605. },
  606. renameDocWithLock(
  607. projectId,
  608. docId,
  609. userId,
  610. update,
  611. projectHistoryId,
  612. callback
  613. ) {
  614. const UpdateManager = require('./UpdateManager')
  615. UpdateManager.lockUpdatesAndDo(
  616. DocumentManager.renameDoc,
  617. projectId,
  618. docId,
  619. userId,
  620. update,
  621. projectHistoryId,
  622. callback
  623. )
  624. },
  625. resyncDocContentsWithLock(projectId, docId, path, callback) {
  626. const UpdateManager = require('./UpdateManager')
  627. UpdateManager.lockUpdatesAndDo(
  628. DocumentManager.resyncDocContents,
  629. projectId,
  630. docId,
  631. path,
  632. callback
  633. )
  634. },
  635. }