MongoManager.js 7.3 KB

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