DocManager.js 7.9 KB

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