DocManager.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. const MongoManager = require('./MongoManager')
  2. const Errors = require('./Errors')
  3. const logger = require('@overleaf/logger')
  4. const _ = require('lodash')
  5. const DocArchive = require('./DocArchiveManager')
  6. const RangeManager = require('./RangeManager')
  7. const Settings = require('@overleaf/settings')
  8. const { callbackifyAll } = require('@overleaf/promise-utils')
  9. const { setTimeout } = require('timers/promises')
  10. const DocManager = {
  11. async _getDoc(projectId, docId, filter) {
  12. if (filter == null) {
  13. filter = {}
  14. }
  15. if (filter.inS3 !== true) {
  16. throw new Error('must include inS3 when getting doc')
  17. }
  18. const doc = await MongoManager.promises.findDoc(projectId, docId, filter)
  19. if (doc == null) {
  20. throw new Errors.NotFoundError(
  21. `No such doc: ${docId} in project ${projectId}`
  22. )
  23. }
  24. if (doc.inS3) {
  25. await DocArchive.promises.unarchiveDoc(projectId, docId)
  26. return await DocManager._getDoc(projectId, docId, filter)
  27. }
  28. return doc
  29. },
  30. async isDocDeleted(projectId, docId) {
  31. const doc = await MongoManager.promises.findDoc(projectId, docId, {
  32. deleted: true,
  33. })
  34. if (!doc) {
  35. throw new Errors.NotFoundError(
  36. `No such project/doc: ${projectId}/${docId}`
  37. )
  38. }
  39. // `doc.deleted` is `undefined` for non deleted docs
  40. return Boolean(doc.deleted)
  41. },
  42. async getFullDoc(projectId, docId) {
  43. const doc = await DocManager._getDoc(projectId, docId, {
  44. lines: true,
  45. rev: true,
  46. deleted: true,
  47. version: true,
  48. ranges: true,
  49. inS3: true,
  50. })
  51. return doc
  52. },
  53. // returns the doc without any version information
  54. async _peekRawDoc(projectId, docId) {
  55. const doc = await MongoManager.promises.findDoc(projectId, docId, {
  56. lines: true,
  57. rev: true,
  58. deleted: true,
  59. version: true,
  60. ranges: true,
  61. inS3: true,
  62. })
  63. if (doc == null) {
  64. throw new Errors.NotFoundError(
  65. `No such doc: ${docId} in project ${projectId}`
  66. )
  67. }
  68. if (doc.inS3) {
  69. // skip the unarchiving to mongo when getting a doc
  70. const archivedDoc = await DocArchive.promises.getDoc(projectId, docId)
  71. Object.assign(doc, archivedDoc)
  72. }
  73. return doc
  74. },
  75. // get the doc from mongo if possible, or from the persistent store otherwise,
  76. // without unarchiving it (avoids unnecessary writes to mongo)
  77. async peekDoc(projectId, docId) {
  78. const doc = await DocManager._peekRawDoc(projectId, docId)
  79. await MongoManager.promises.checkRevUnchanged(doc)
  80. return doc
  81. },
  82. async getDocLines(projectId, docId) {
  83. const doc = await DocManager._getDoc(projectId, docId, {
  84. lines: true,
  85. inS3: true,
  86. })
  87. return doc
  88. },
  89. async getAllDeletedDocs(projectId, filter) {
  90. return await MongoManager.promises.getProjectsDeletedDocs(projectId, filter)
  91. },
  92. async getAllNonDeletedDocs(projectId, filter) {
  93. await DocArchive.promises.unArchiveAllDocs(projectId)
  94. const docs = await MongoManager.promises.getProjectsDocs(
  95. projectId,
  96. { include_deleted: false },
  97. filter
  98. )
  99. if (docs == null) {
  100. throw new Errors.NotFoundError(`No docs for project ${projectId}`)
  101. }
  102. return docs
  103. },
  104. async updateDoc(projectId, docId, lines, version, ranges) {
  105. const MAX_ATTEMPTS = 2
  106. for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
  107. try {
  108. const { modified, rev } = await DocManager._tryUpdateDoc(
  109. projectId,
  110. docId,
  111. lines,
  112. version,
  113. ranges
  114. )
  115. return { modified, rev }
  116. } catch (err) {
  117. if (err instanceof Errors.DocRevValueError && attempt < MAX_ATTEMPTS) {
  118. // Another updateDoc call was racing with ours.
  119. // Retry once in a bit.
  120. logger.warn(
  121. { projectId, docId, err },
  122. 'detected concurrent updateDoc call'
  123. )
  124. await setTimeout(100 + Math.random() * 100)
  125. continue
  126. } else {
  127. throw err
  128. }
  129. }
  130. }
  131. },
  132. async _tryUpdateDoc(projectId, docId, lines, version, ranges) {
  133. if (lines == null || version == null || ranges == null) {
  134. throw new Error('no lines, version or ranges provided')
  135. }
  136. let doc
  137. try {
  138. doc = await DocManager._getDoc(projectId, docId, {
  139. version: true,
  140. rev: true,
  141. lines: true,
  142. ranges: true,
  143. inS3: true,
  144. })
  145. } catch (err) {
  146. if (err instanceof Errors.NotFoundError) {
  147. doc = null
  148. } else {
  149. throw err
  150. }
  151. }
  152. ranges = RangeManager.jsonRangesToMongo(ranges)
  153. let updateLines, updateRanges, updateVersion
  154. if (doc == null) {
  155. // If the document doesn't exist, we'll make sure to create/update all parts of it.
  156. updateLines = true
  157. updateVersion = true
  158. updateRanges = true
  159. } else {
  160. if (doc.version > version) {
  161. // Reject update when the version was decremented.
  162. // Potential reasons: racing flush, broken history.
  163. throw new Errors.DocVersionDecrementedError('rejecting stale update', {
  164. updateVersion: version,
  165. flushedVersion: doc.version,
  166. })
  167. }
  168. updateLines = !_.isEqual(doc.lines, lines)
  169. updateVersion = doc.version !== version
  170. updateRanges = RangeManager.shouldUpdateRanges(doc.ranges, ranges)
  171. }
  172. let modified = false
  173. let rev = doc?.rev || 0
  174. if (updateLines || updateRanges || updateVersion) {
  175. const update = {}
  176. if (updateLines) {
  177. update.lines = lines
  178. }
  179. if (updateRanges) {
  180. update.ranges = ranges
  181. }
  182. if (updateVersion) {
  183. update.version = version
  184. }
  185. logger.debug(
  186. { projectId, docId, oldVersion: doc?.version, newVersion: version },
  187. 'updating doc'
  188. )
  189. if (updateLines || updateRanges) {
  190. rev += 1 // rev will be incremented in mongo by MongoManager.upsertIntoDocCollection
  191. }
  192. modified = true
  193. await MongoManager.promises.upsertIntoDocCollection(
  194. projectId,
  195. docId,
  196. doc?.rev,
  197. update
  198. )
  199. } else {
  200. logger.debug({ projectId, docId }, 'doc has not changed - not updating')
  201. }
  202. return { modified, rev }
  203. },
  204. async patchDoc(projectId, docId, meta) {
  205. const projection = { _id: 1, deleted: true }
  206. const doc = await MongoManager.promises.findDoc(
  207. projectId,
  208. docId,
  209. projection
  210. )
  211. if (!doc) {
  212. throw new Errors.NotFoundError(
  213. `No such project/doc to delete: ${projectId}/${docId}`
  214. )
  215. }
  216. if (meta.deleted && Settings.docstore.archiveOnSoftDelete) {
  217. // The user will not read this doc anytime soon. Flush it out of mongo.
  218. DocArchive.promises.archiveDoc(projectId, docId).catch(err => {
  219. logger.warn(
  220. { projectId, docId, err },
  221. 'archiving a single doc in the background failed'
  222. )
  223. })
  224. }
  225. await MongoManager.promises.patchDoc(projectId, docId, meta)
  226. },
  227. }
  228. module.exports = {
  229. ...callbackifyAll(DocManager, {
  230. multiResult: {
  231. updateDoc: ['modified', 'rev'],
  232. },
  233. }),
  234. promises: DocManager,
  235. }