delete_orphaned_docs_online_check.mjs 5.1 KB

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