delete_orphaned_docs_online_check.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js'
  2. import { promisify } from 'node:util'
  3. import mongodb from 'mongodb-legacy'
  4. import {
  5. db,
  6. READ_PREFERENCE_PRIMARY,
  7. READ_PREFERENCE_SECONDARY,
  8. } from '../app/src/infrastructure/mongodb.js'
  9. import { promiseMapWithLimit } from '@overleaf/promise-utils'
  10. import DeleteOrphanedDataHelper from './delete_orphaned_data_helper.mjs'
  11. import { scriptRunner } from './lib/ScriptRunner.mjs'
  12. const { ObjectId } = mongodb
  13. const sleep = promisify(setTimeout)
  14. const { getHardDeletedProjectIds } = DeleteOrphanedDataHelper
  15. const NOW_IN_S = Date.now() / 1000
  16. const ONE_WEEK_IN_S = 60 * 60 * 24 * 7
  17. const TEN_SECONDS = 10 * 1000
  18. const DRY_RUN = process.env.DRY_RUN === 'true'
  19. if (!process.env.BATCH_LAST_ID) {
  20. console.error('Set BATCH_LAST_ID and re-run.')
  21. process.exit(1)
  22. }
  23. const BATCH_LAST_ID = new ObjectId(process.env.BATCH_LAST_ID)
  24. const INCREMENT_BY_S = parseInt(process.env.INCREMENT_BY_S, 10) || ONE_WEEK_IN_S
  25. const BATCH_SIZE = parseInt(process.env.BATCH_SIZE, 10) || 1000
  26. const READ_CONCURRENCY_SECONDARY =
  27. parseInt(process.env.READ_CONCURRENCY_SECONDARY, 10) || 1000
  28. const READ_CONCURRENCY_PRIMARY =
  29. parseInt(process.env.READ_CONCURRENCY_PRIMARY, 10) || 500
  30. const STOP_AT_S = parseInt(process.env.STOP_AT_S, 10) || NOW_IN_S
  31. const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY, 10) || 10
  32. const LET_USER_DOUBLE_CHECK_INPUTS_FOR =
  33. parseInt(process.env.LET_USER_DOUBLE_CHECK_INPUTS_FOR, 10) || TEN_SECONDS
  34. function getSecondsFromObjectId(id) {
  35. return id.getTimestamp().getTime() / 1000
  36. }
  37. async function main() {
  38. await letUserDoubleCheckInputs()
  39. let lowerProjectId = BATCH_LAST_ID
  40. let nProjectsProcessedTotal = 0
  41. let nProjectsWithOrphanedDocsTotal = 0
  42. let nDeletedDocsTotal = 0
  43. while (getSecondsFromObjectId(lowerProjectId) <= STOP_AT_S) {
  44. const upperTime = getSecondsFromObjectId(lowerProjectId) + INCREMENT_BY_S
  45. let upperProjectId = ObjectId.createFromTime(upperTime)
  46. const query = {
  47. project_id: {
  48. // exclude edge
  49. $gt: lowerProjectId,
  50. // include edge
  51. $lte: upperProjectId,
  52. },
  53. }
  54. const docs = await db.docs
  55. .find(query, { readPreference: READ_PREFERENCE_SECONDARY })
  56. .project({ project_id: 1 })
  57. .sort({ project_id: 1 })
  58. .limit(BATCH_SIZE)
  59. .toArray()
  60. if (docs.length) {
  61. const projectIds = Array.from(
  62. new Set(docs.map(doc => doc.project_id.toString()))
  63. ).map(id => new ObjectId(id))
  64. console.log('Checking projects', JSON.stringify(projectIds))
  65. const { nProjectsWithOrphanedDocs, nDeletedDocs } =
  66. await processBatch(projectIds)
  67. nProjectsProcessedTotal += projectIds.length
  68. nProjectsWithOrphanedDocsTotal += nProjectsWithOrphanedDocs
  69. nDeletedDocsTotal += nDeletedDocs
  70. if (docs.length === BATCH_SIZE) {
  71. // This project may have more than BATCH_SIZE docs.
  72. const lastDoc = docs[docs.length - 1]
  73. // Resume from after this projectId.
  74. upperProjectId = lastDoc.project_id
  75. }
  76. }
  77. console.error(
  78. 'Processed %d projects ' +
  79. '(%d projects with orphaned docs/%d docs deleted) ' +
  80. 'until %s',
  81. nProjectsProcessedTotal,
  82. nProjectsWithOrphanedDocsTotal,
  83. nDeletedDocsTotal,
  84. upperProjectId
  85. )
  86. lowerProjectId = upperProjectId
  87. }
  88. }
  89. async function getProjectDocs(projectId) {
  90. return await db.docs
  91. .find(
  92. { project_id: projectId },
  93. {
  94. projection: { _id: 1 },
  95. readPreference: READ_PREFERENCE_PRIMARY,
  96. }
  97. )
  98. .toArray()
  99. }
  100. async function processBatch(projectIds) {
  101. const projectsWithOrphanedDocs = await getHardDeletedProjectIds({
  102. projectIds,
  103. READ_CONCURRENCY_PRIMARY,
  104. READ_CONCURRENCY_SECONDARY,
  105. })
  106. let nDeletedDocs = 0
  107. async function countOrphanedDocs(projectId) {
  108. const docs = await getProjectDocs(projectId)
  109. nDeletedDocs += docs.length
  110. console.log(
  111. 'Deleted project %s has %s orphaned docs: %s',
  112. projectId,
  113. docs.length,
  114. JSON.stringify(docs.map(doc => doc._id))
  115. )
  116. }
  117. await promiseMapWithLimit(
  118. READ_CONCURRENCY_PRIMARY,
  119. projectsWithOrphanedDocs,
  120. countOrphanedDocs
  121. )
  122. if (!DRY_RUN) {
  123. await promiseMapWithLimit(
  124. WRITE_CONCURRENCY,
  125. projectsWithOrphanedDocs,
  126. DocstoreManager.promises.destroyProject
  127. )
  128. }
  129. const nProjectsWithOrphanedDocs = projectsWithOrphanedDocs.length
  130. return { nProjectsWithOrphanedDocs, nDeletedDocs }
  131. }
  132. async function letUserDoubleCheckInputs() {
  133. console.error(
  134. 'Options:',
  135. JSON.stringify(
  136. {
  137. BATCH_LAST_ID,
  138. BATCH_SIZE,
  139. DRY_RUN,
  140. INCREMENT_BY_S,
  141. STOP_AT_S,
  142. READ_CONCURRENCY_SECONDARY,
  143. READ_CONCURRENCY_PRIMARY,
  144. WRITE_CONCURRENCY,
  145. LET_USER_DOUBLE_CHECK_INPUTS_FOR,
  146. },
  147. null,
  148. 2
  149. )
  150. )
  151. console.error(
  152. 'Waiting for you to double check inputs for',
  153. LET_USER_DOUBLE_CHECK_INPUTS_FOR,
  154. 'ms'
  155. )
  156. await sleep(LET_USER_DOUBLE_CHECK_INPUTS_FOR)
  157. }
  158. try {
  159. await scriptRunner(main)
  160. console.error('Done.')
  161. process.exit(0)
  162. } catch (error) {
  163. console.error({ error })
  164. process.exit(1)
  165. }