MongoManager.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import mongodb from './mongodb.js'
  2. import Settings from '@overleaf/settings'
  3. import Errors from './Errors.js'
  4. import Metrics from '@overleaf/metrics'
  5. const { db, ObjectId, BSON } = mongodb
  6. const ARCHIVING_LOCK_DURATION_MS = Settings.archivingLockDurationMs
  7. async function findDoc(projectId, docId, projection) {
  8. const doc = await db.docs.findOne(
  9. {
  10. _id: new ObjectId(docId.toString()),
  11. project_id: new ObjectId(projectId.toString()),
  12. },
  13. { projection }
  14. )
  15. if (doc && projection.version && !doc.version) {
  16. doc.version = 0
  17. }
  18. return doc
  19. }
  20. async function getProjectsDeletedDocs(projectId, projection) {
  21. const docs = await db.docs
  22. .find(
  23. {
  24. project_id: new ObjectId(projectId.toString()),
  25. deleted: true,
  26. },
  27. {
  28. projection,
  29. sort: { deletedAt: -1 },
  30. limit: Settings.max_deleted_docs,
  31. }
  32. )
  33. .toArray()
  34. return docs
  35. }
  36. async function getProjectsDocs(projectId, options, projection) {
  37. const query = { project_id: new ObjectId(projectId.toString()) }
  38. if (!options.include_deleted) {
  39. query.deleted = { $ne: true }
  40. }
  41. const queryOptions = {
  42. projection,
  43. }
  44. if (options.limit) {
  45. queryOptions.limit = options.limit
  46. }
  47. const docs = await db.docs.find(query, queryOptions).toArray()
  48. return docs
  49. }
  50. async function getArchivedProjectDocs(projectId, maxResults) {
  51. const query = {
  52. project_id: new ObjectId(projectId.toString()),
  53. inS3: true,
  54. }
  55. const docs = await db.docs
  56. .find(query, { projection: { _id: 1 }, limit: maxResults })
  57. .toArray()
  58. return docs
  59. }
  60. async function getNonArchivedProjectDocIds(projectId) {
  61. const docs = await db.docs
  62. .find(
  63. {
  64. project_id: new ObjectId(projectId),
  65. inS3: { $ne: true },
  66. },
  67. { projection: { _id: 1 } }
  68. )
  69. .map(doc => doc._id)
  70. .toArray()
  71. return docs
  72. }
  73. async function getNonDeletedArchivedProjectDocs(projectId, maxResults) {
  74. const query = {
  75. project_id: new ObjectId(projectId.toString()),
  76. deleted: { $ne: true },
  77. inS3: true,
  78. }
  79. const docs = await db.docs
  80. .find(query, { projection: { _id: 1 }, limit: maxResults })
  81. .toArray()
  82. return docs
  83. }
  84. function convertUpdateToPipeline(update) {
  85. const pipeline = []
  86. for (const [operation, ops] of Object.entries(update)) {
  87. for (const [field, value] of Object.entries(ops)) {
  88. if (operation === '$unset') {
  89. // $unset uses a different schema in a pipeline
  90. pipeline.push({ [operation]: field })
  91. } else {
  92. // Avoid evaluating '$foo' strings
  93. pipeline.push({ [operation]: { [field]: { $literal: value } } })
  94. }
  95. }
  96. }
  97. return pipeline
  98. }
  99. async function upsertIntoDocCollection(projectId, docId, previousRev, updates) {
  100. if (previousRev) {
  101. const update = {
  102. $set: updates,
  103. $unset: { inS3: true },
  104. }
  105. if (updates.lines || updates.ranges) {
  106. update.$set.rev = previousRev + 1
  107. }
  108. const pipeline = convertUpdateToPipeline(update)
  109. const payloadSize = BSON.calculateObjectSize(pipeline)
  110. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'update' })
  111. const result = await db.docs.updateOne(
  112. {
  113. _id: new ObjectId(docId),
  114. project_id: new ObjectId(projectId),
  115. rev: previousRev,
  116. },
  117. pipeline
  118. )
  119. if (result.matchedCount !== 1) {
  120. throw new Errors.DocRevValueError()
  121. }
  122. } else {
  123. const payloadSize = BSON.calculateObjectSize(updates)
  124. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'insert' })
  125. try {
  126. await db.docs.insertOne({
  127. _id: new ObjectId(docId),
  128. project_id: new ObjectId(projectId),
  129. rev: 1,
  130. ...updates,
  131. })
  132. } catch (err) {
  133. if (err.code === 11000) {
  134. // duplicate doc _id
  135. throw new Errors.DocRevValueError()
  136. } else {
  137. throw err
  138. }
  139. }
  140. }
  141. }
  142. async function patchDoc(projectId, docId, meta) {
  143. const payloadSize = BSON.calculateObjectSize(meta)
  144. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'patch' })
  145. await db.docs.updateOne(
  146. {
  147. _id: new ObjectId(docId),
  148. project_id: new ObjectId(projectId),
  149. },
  150. { $set: meta }
  151. )
  152. }
  153. /**
  154. * Fetch a doc and lock it for archiving
  155. *
  156. * This will return null if the doc is not found, if it's already archived or
  157. * if the lock can't be acquired.
  158. */
  159. async function getDocForArchiving(projectId, docId) {
  160. const archivingUntil = new Date(Date.now() + ARCHIVING_LOCK_DURATION_MS)
  161. const result = await db.docs.findOneAndUpdate(
  162. {
  163. _id: new ObjectId(docId),
  164. project_id: new ObjectId(projectId),
  165. inS3: { $ne: true },
  166. $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
  167. },
  168. { $set: { archivingUntil } },
  169. {
  170. projection: { lines: 1, ranges: 1, rev: 1 },
  171. includeResultMetadata: true,
  172. }
  173. )
  174. return result.value
  175. }
  176. /**
  177. * Clear the doc contents from Mongo and release the archiving lock
  178. */
  179. async function markDocAsArchived(projectId, docId, rev) {
  180. await db.docs.updateOne(
  181. { _id: new ObjectId(docId), rev },
  182. {
  183. $set: { inS3: true },
  184. $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
  185. }
  186. )
  187. }
  188. /**
  189. * Restore an archived doc
  190. *
  191. * This checks that the archived doc's rev matches.
  192. */
  193. async function restoreArchivedDoc(projectId, docId, archivedDoc) {
  194. const query = {
  195. _id: new ObjectId(docId),
  196. project_id: new ObjectId(projectId),
  197. rev: archivedDoc.rev,
  198. }
  199. const update = {
  200. $set: {
  201. lines: archivedDoc.lines,
  202. ranges: archivedDoc.ranges || {},
  203. },
  204. $unset: {
  205. inS3: true,
  206. },
  207. }
  208. const pipeline = convertUpdateToPipeline(update)
  209. const payloadSize = BSON.calculateObjectSize(pipeline)
  210. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'restore' })
  211. const result = await db.docs.updateOne(query, pipeline)
  212. if (result.matchedCount !== 1) {
  213. throw new Errors.DocRevValueError('failed to unarchive doc', {
  214. docId,
  215. rev: archivedDoc.rev,
  216. })
  217. }
  218. }
  219. async function getDocRev(docId) {
  220. const doc = await db.docs.findOne(
  221. { _id: new ObjectId(docId.toString()) },
  222. { projection: { rev: 1 } }
  223. )
  224. return doc && doc.rev
  225. }
  226. /**
  227. * Helper method to support optimistic locking.
  228. *
  229. * Check that the rev of an existing doc is unchanged. If the rev has
  230. * changed, return a DocModifiedError.
  231. */
  232. async function checkRevUnchanged(doc) {
  233. const currentRev = await getDocRev(doc._id)
  234. if (isNaN(currentRev) || isNaN(doc.rev)) {
  235. throw new Errors.DocRevValueError('doc rev is NaN', {
  236. doc_id: doc._id,
  237. rev: doc.rev,
  238. currentRev,
  239. })
  240. }
  241. if (doc.rev !== currentRev) {
  242. throw new Errors.DocModifiedError('doc rev has changed', {
  243. doc_id: doc._id,
  244. rev: doc.rev,
  245. currentRev,
  246. })
  247. }
  248. }
  249. async function destroyProject(projectId) {
  250. await db.docs.deleteMany({ project_id: new ObjectId(projectId) })
  251. }
  252. export default {
  253. convertUpdateToPipeline,
  254. findDoc,
  255. getProjectsDeletedDocs,
  256. getProjectsDocs,
  257. getArchivedProjectDocs,
  258. getNonArchivedProjectDocIds,
  259. getNonDeletedArchivedProjectDocs,
  260. upsertIntoDocCollection,
  261. restoreArchivedDoc,
  262. patchDoc,
  263. getDocForArchiving,
  264. markDocAsArchived,
  265. checkRevUnchanged,
  266. destroyProject,
  267. }