clear_deleted.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #!/usr/bin/env node
  2. import async from 'async'
  3. import logger from '@overleaf/logger'
  4. import Settings from '@overleaf/settings'
  5. import redis from '@overleaf/redis-wrapper'
  6. import path from 'node:path'
  7. import { db, ObjectId } from '../app/js/mongodb.js'
  8. logger.logger.level('fatal')
  9. const rclient = redis.createClient(Settings.redis.project_history)
  10. const Keys = Settings.redis.project_history.key_schema
  11. const argv = process.argv.slice(2)
  12. const limit = parseInt(argv[0], 10) || null
  13. const force = argv[1] === 'force' || false
  14. let delay = 0
  15. function checkAndClear(project, callback) {
  16. const projectId = project.project_id
  17. function checkDeleted(cb) {
  18. db.projects.findOne(
  19. { _id: new ObjectId(projectId) },
  20. { projection: { _id: 1 } },
  21. (err, result) => {
  22. if (err) {
  23. cb(err)
  24. } else if (!result) {
  25. // project not found, but we still need to look at deletedProjects
  26. cb()
  27. } else {
  28. console.log(`Project ${projectId} found in projects`)
  29. cb(new Error('error: project still exists'))
  30. }
  31. }
  32. )
  33. }
  34. function checkRecoverable(cb) {
  35. db.deletedProjects.findOne(
  36. {
  37. // this condition makes use of the index
  38. 'deleterData.deletedProjectId': new ObjectId(projectId),
  39. // this condition checks if the deleted project has expired
  40. 'project._id': new ObjectId(projectId),
  41. },
  42. { projection: { _id: 1 } },
  43. (err, result) => {
  44. if (err) {
  45. cb(err)
  46. } else if (!result) {
  47. console.log(
  48. `project ${projectId} has been deleted - safe to clear queue`
  49. )
  50. cb()
  51. } else {
  52. console.log(`Project ${projectId} found in deletedProjects`)
  53. cb(new Error('error: project still exists'))
  54. }
  55. }
  56. )
  57. }
  58. function clearRedisQueue(cb) {
  59. const key = Keys.projectHistoryOps({ project_id: projectId })
  60. delay++
  61. if (force) {
  62. console.log('setting redis key', key, 'to expire in', delay, 'seconds')
  63. // use expire to allow redis to delete the key in the background
  64. rclient.expire(key, delay, err => {
  65. cb(err)
  66. })
  67. } else {
  68. console.log(
  69. 'dry run, would set key',
  70. key,
  71. 'to expire in',
  72. delay,
  73. 'seconds'
  74. )
  75. cb()
  76. }
  77. }
  78. function clearMongoEntry(cb) {
  79. if (force) {
  80. console.log('deleting key in mongo projectHistoryFailures', projectId)
  81. db.projectHistoryFailures.deleteOne({ project_id: projectId }, cb)
  82. } else {
  83. console.log('would delete failure record for', projectId, 'from mongo')
  84. cb()
  85. }
  86. }
  87. // do the checks and deletions
  88. async.waterfall(
  89. [checkDeleted, checkRecoverable, clearRedisQueue, clearMongoEntry],
  90. err => {
  91. if (!err || err.message === 'error: project still exists') {
  92. callback()
  93. } else {
  94. console.log('error:', err)
  95. callback(err)
  96. }
  97. }
  98. )
  99. }
  100. // find all the broken projects from the failure records
  101. async function main() {
  102. const results = await db.projectHistoryFailures.find({}).toArray()
  103. processFailures(results)
  104. }
  105. main().catch(error => {
  106. console.error(error)
  107. process.exit(1)
  108. })
  109. function processFailures(results) {
  110. if (limit === null) {
  111. console.log(`
  112. Usage: node clear_deleted.js [QUEUES] [FORCE]
  113. where
  114. QUEUES is the number of queues to process
  115. FORCE is the string "force" when we're ready to delete the queues. Without it, this script does a dry-run
  116. `)
  117. process.exit(0)
  118. }
  119. console.log('number of stuck projects', results.length)
  120. console.log('force mode', force ? 'enabled' : 'disabled (dry run)')
  121. const projectsToProcess = results.slice(0, limit)
  122. const unprocessedProjects = results.length - projectsToProcess.length
  123. const scriptFileName = path.basename(process.argv[1])
  124. const limitOrPlaceholder = limit ?? 100
  125. const forceExampleCommand = `node scripts/${scriptFileName} ${limitOrPlaceholder} force`
  126. // now check if the project is truly deleted in mongo
  127. async.eachSeries(projectsToProcess, checkAndClear, err => {
  128. console.log('DONE', err)
  129. if (unprocessedProjects > 0) {
  130. console.warn(
  131. `WARNING: ${unprocessedProjects} project(s) were not processed in this run`
  132. )
  133. }
  134. if (!force) {
  135. console.warn(
  136. `Dry run only. Rerun with force to apply changes, for example: ${forceExampleCommand}`
  137. )
  138. }
  139. process.exit()
  140. })
  141. }