MongoManager.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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. async function upsertIntoDocCollection(projectId, docId, previousRev, updates) {
  85. if (previousRev) {
  86. const update = {
  87. $set: updates,
  88. $unset: { inS3: true },
  89. }
  90. if (updates.lines || updates.ranges) {
  91. update.$inc = { rev: 1 }
  92. }
  93. const payloadSize = BSON.calculateObjectSize(update)
  94. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'update' })
  95. const result = await db.docs.updateOne(
  96. {
  97. _id: new ObjectId(docId),
  98. project_id: new ObjectId(projectId),
  99. rev: previousRev,
  100. },
  101. update
  102. )
  103. if (result.matchedCount !== 1) {
  104. throw new Errors.DocRevValueError()
  105. }
  106. } else {
  107. const payloadSize = BSON.calculateObjectSize(updates)
  108. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'insert' })
  109. try {
  110. await db.docs.insertOne({
  111. _id: new ObjectId(docId),
  112. project_id: new ObjectId(projectId),
  113. rev: 1,
  114. ...updates,
  115. })
  116. } catch (err) {
  117. if (err.code === 11000) {
  118. // duplicate doc _id
  119. throw new Errors.DocRevValueError()
  120. } else {
  121. throw err
  122. }
  123. }
  124. }
  125. }
  126. async function patchDoc(projectId, docId, meta) {
  127. const payloadSize = BSON.calculateObjectSize(meta)
  128. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'patch' })
  129. await db.docs.updateOne(
  130. {
  131. _id: new ObjectId(docId),
  132. project_id: new ObjectId(projectId),
  133. },
  134. { $set: meta }
  135. )
  136. }
  137. /**
  138. * Fetch a doc and lock it for archiving
  139. *
  140. * This will return null if the doc is not found, if it's already archived or
  141. * if the lock can't be acquired.
  142. */
  143. async function getDocForArchiving(projectId, docId) {
  144. const archivingUntil = new Date(Date.now() + ARCHIVING_LOCK_DURATION_MS)
  145. const result = await db.docs.findOneAndUpdate(
  146. {
  147. _id: new ObjectId(docId),
  148. project_id: new ObjectId(projectId),
  149. inS3: { $ne: true },
  150. $or: [{ archivingUntil: null }, { archivingUntil: { $lt: new Date() } }],
  151. },
  152. { $set: { archivingUntil } },
  153. {
  154. projection: { lines: 1, ranges: 1, rev: 1 },
  155. includeResultMetadata: true,
  156. }
  157. )
  158. return result.value
  159. }
  160. /**
  161. * Clear the doc contents from Mongo and release the archiving lock
  162. */
  163. async function markDocAsArchived(projectId, docId, rev) {
  164. await db.docs.updateOne(
  165. { _id: new ObjectId(docId), rev },
  166. {
  167. $set: { inS3: true },
  168. $unset: { lines: 1, ranges: 1, archivingUntil: 1 },
  169. }
  170. )
  171. }
  172. /**
  173. * Restore an archived doc
  174. *
  175. * This checks that the archived doc's rev matches.
  176. */
  177. async function restoreArchivedDoc(projectId, docId, archivedDoc) {
  178. const query = {
  179. _id: new ObjectId(docId),
  180. project_id: new ObjectId(projectId),
  181. rev: archivedDoc.rev,
  182. }
  183. const update = {
  184. $set: {
  185. lines: archivedDoc.lines,
  186. ranges: archivedDoc.ranges || {},
  187. },
  188. $unset: {
  189. inS3: true,
  190. },
  191. }
  192. const payloadSize = BSON.calculateObjectSize(update)
  193. Metrics.count('mongo_docs_write', payloadSize, 1, { method: 'restore' })
  194. const result = await db.docs.updateOne(query, update)
  195. if (result.matchedCount === 0) {
  196. throw new Errors.DocRevValueError('failed to unarchive doc', {
  197. docId,
  198. rev: archivedDoc.rev,
  199. })
  200. }
  201. }
  202. async function getDocRev(docId) {
  203. const doc = await db.docs.findOne(
  204. { _id: new ObjectId(docId.toString()) },
  205. { projection: { rev: 1 } }
  206. )
  207. return doc && doc.rev
  208. }
  209. /**
  210. * Helper method to support optimistic locking.
  211. *
  212. * Check that the rev of an existing doc is unchanged. If the rev has
  213. * changed, return a DocModifiedError.
  214. */
  215. async function checkRevUnchanged(doc) {
  216. const currentRev = await getDocRev(doc._id)
  217. if (isNaN(currentRev) || isNaN(doc.rev)) {
  218. throw new Errors.DocRevValueError('doc rev is NaN', {
  219. doc_id: doc._id,
  220. rev: doc.rev,
  221. currentRev,
  222. })
  223. }
  224. if (doc.rev !== currentRev) {
  225. throw new Errors.DocModifiedError('doc rev has changed', {
  226. doc_id: doc._id,
  227. rev: doc.rev,
  228. currentRev,
  229. })
  230. }
  231. }
  232. async function destroyProject(projectId) {
  233. await db.docs.deleteMany({ project_id: new ObjectId(projectId) })
  234. }
  235. export default {
  236. findDoc,
  237. getProjectsDeletedDocs,
  238. getProjectsDocs,
  239. getArchivedProjectDocs,
  240. getNonArchivedProjectDocIds,
  241. getNonDeletedArchivedProjectDocs,
  242. upsertIntoDocCollection,
  243. restoreArchivedDoc,
  244. patchDoc,
  245. getDocForArchiving,
  246. markDocAsArchived,
  247. checkRevUnchanged,
  248. destroyProject,
  249. }