delete_orphaned_project_archives.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. const Settings = require('@overleaf/settings')
  2. const OError = require('@overleaf/o-error')
  3. const { waitForDb } = require('../app/src/infrastructure/mongodb')
  4. const { promiseMapWithLimit } = require('../app/src/util/promises')
  5. const { getHardDeletedProjectIds } = require('./delete_orphaned_data_helper')
  6. const TpdsUpdateSender = require('../app/src/Features/ThirdPartyDataStore/TpdsUpdateSender')
  7. const { promisify } = require('util')
  8. const { ObjectId } = require('mongodb')
  9. const fetch = require('node-fetch')
  10. const sleep = promisify(setTimeout)
  11. const START_OFFSET = process.env.START_OFFSET
  12. const BATCH_SIZE = parseInt(process.env.BATCH_SIZE, 10) || 1000
  13. const DRY_RUN = process.env.DRY_RUN !== 'false'
  14. const READ_CONCURRENCY_SECONDARY =
  15. parseInt(process.env.READ_CONCURRENCY_SECONDARY, 10) || 1000
  16. const READ_CONCURRENCY_PRIMARY =
  17. parseInt(process.env.READ_CONCURRENCY_PRIMARY, 10) || 500
  18. const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY, 10) || 10
  19. const LET_USER_DOUBLE_CHECK_INPUTS_FOR =
  20. parseInt(process.env.LET_USER_DOUBLE_CHECK_INPUTS_FOR, 10) || 10 * 1000
  21. async function main() {
  22. await letUserDoubleCheckInputs()
  23. await waitForDb()
  24. let processed = 0
  25. let hardDeleted = 0
  26. let pageToken = ''
  27. let startOffset = START_OFFSET
  28. while (pageToken !== undefined) {
  29. const url = new URL(`${Settings.apis.project_archiver.url}/project/list`)
  30. url.searchParams.append('pageToken', pageToken)
  31. url.searchParams.append('startOffset', startOffset)
  32. const response = await fetch(url, {
  33. headers: { Accept: 'application/json' },
  34. })
  35. if (!response.ok) {
  36. throw new OError('Failed to get list of projects from project archiver', {
  37. status: response.status,
  38. })
  39. }
  40. const { nextPageToken, entries } = await response.json()
  41. pageToken = nextPageToken
  42. startOffset = undefined
  43. hardDeleted += await processBatch(entries)
  44. processed += entries.length
  45. console.log(
  46. 'processed:',
  47. processed.toString().padStart(10, '0'),
  48. 'hard deleted:',
  49. hardDeleted.toString().padStart(10, '0'),
  50. 'nextPageToken:',
  51. nextPageToken,
  52. 'START_OFFSET:',
  53. entries.pop()?.prefix
  54. )
  55. }
  56. }
  57. async function processBatch(entries) {
  58. const projectIdToPrefix = new Map()
  59. for (const { prefix, projectId } of entries) {
  60. const prefixes = projectIdToPrefix.get(projectId) || []
  61. prefixes.push(prefix)
  62. projectIdToPrefix.set(projectId, prefixes)
  63. }
  64. const projectIds = Array.from(projectIdToPrefix.keys()).map(id =>
  65. ObjectId(id)
  66. )
  67. const projectsWithOrphanedArchive = await getHardDeletedProjectIds({
  68. projectIds,
  69. READ_CONCURRENCY_PRIMARY,
  70. READ_CONCURRENCY_SECONDARY,
  71. })
  72. await promiseMapWithLimit(
  73. WRITE_CONCURRENCY,
  74. projectsWithOrphanedArchive.flatMap(id =>
  75. projectIdToPrefix.get(id.toString())
  76. ),
  77. hardDeleteProjectArchiverData
  78. )
  79. return projectsWithOrphanedArchive.length
  80. }
  81. async function hardDeleteProjectArchiverData(prefix) {
  82. console.log(`Destroying hard deleted project archive at '${prefix}/'`)
  83. if (DRY_RUN) return
  84. for (let i = 0; i < 10; i++) {
  85. await sleep(1000 * i)
  86. try {
  87. const ok = await TpdsUpdateSender.promises.deleteProject({
  88. projectId: encodeURIComponent(prefix),
  89. })
  90. if (ok) {
  91. return
  92. }
  93. } catch (e) {
  94. console.error(`deletion failed for '${prefix}/'`, e)
  95. }
  96. }
  97. throw new Error(`deletion failed for '${prefix}/', check logs`)
  98. }
  99. async function letUserDoubleCheckInputs() {
  100. console.error(
  101. 'Options:',
  102. JSON.stringify(
  103. {
  104. BATCH_SIZE,
  105. DRY_RUN,
  106. LET_USER_DOUBLE_CHECK_INPUTS_FOR,
  107. READ_CONCURRENCY_SECONDARY,
  108. READ_CONCURRENCY_PRIMARY,
  109. START_OFFSET,
  110. WRITE_CONCURRENCY,
  111. },
  112. null,
  113. 2
  114. )
  115. )
  116. console.error(
  117. 'Waiting for you to double check inputs for',
  118. LET_USER_DOUBLE_CHECK_INPUTS_FOR,
  119. 'ms'
  120. )
  121. await sleep(LET_USER_DOUBLE_CHECK_INPUTS_FOR)
  122. }
  123. main()
  124. .then(() => {
  125. console.log('Done.')
  126. process.exit(0)
  127. })
  128. .catch(error => {
  129. console.error({ error })
  130. process.exit(1)
  131. })