DocManager.js 7.3 KB

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