DocManager.js 7.8 KB

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