delete_orphaned_data_helper.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. const {
  2. db,
  3. READ_PREFERENCE_PRIMARY,
  4. READ_PREFERENCE_SECONDARY,
  5. } = require('../app/src/infrastructure/mongodb')
  6. const { promiseMapWithLimit } = require('@overleaf/promise-utils')
  7. async function getDeletedProject(projectId, readPreference) {
  8. return await db.deletedProjects.findOne(
  9. { 'deleterData.deletedProjectId': projectId },
  10. {
  11. // There is no index on .project. Pull down something small.
  12. projection: { 'project._id': 1 },
  13. readPreference,
  14. }
  15. )
  16. }
  17. async function getProject(projectId, readPreference) {
  18. return await db.projects.findOne(
  19. { _id: projectId },
  20. {
  21. // Pulling down an empty object is fine for differentiating with null.
  22. projection: { _id: 0 },
  23. readPreference,
  24. }
  25. )
  26. }
  27. async function checkProjectExistsWithReadPreference(projectId, readPreference) {
  28. // NOTE: Possible race conditions!
  29. // There are two processes which are racing with our queries:
  30. // 1. project deletion
  31. // 2. project restoring
  32. // For 1. we check the projects collection before deletedProjects.
  33. // If a project were to be delete in this very moment, we should see the
  34. // soft-deleted entry which is created before deleting the projects entry.
  35. // For 2. we check the projects collection after deletedProjects again.
  36. // If a project were to be restored in this very moment, it is very likely
  37. // to see the projects entry again.
  38. // Unlikely edge case: Restore+Deletion in rapid succession.
  39. // We could add locking to the ProjectDeleter for ruling ^ out.
  40. if (await getProject(projectId, readPreference)) {
  41. // The project is live.
  42. return true
  43. }
  44. const deletedProject = await getDeletedProject(projectId, readPreference)
  45. if (deletedProject && deletedProject.project) {
  46. // The project is registered for hard-deletion.
  47. return true
  48. }
  49. if (await getProject(projectId, readPreference)) {
  50. // The project was just restored.
  51. return true
  52. }
  53. // The project does not exist.
  54. return false
  55. }
  56. async function checkProjectExistsOnPrimary(projectId) {
  57. return await checkProjectExistsWithReadPreference(
  58. projectId,
  59. READ_PREFERENCE_PRIMARY
  60. )
  61. }
  62. async function checkProjectExistsOnSecondary(projectId) {
  63. return await checkProjectExistsWithReadPreference(
  64. projectId,
  65. READ_PREFERENCE_SECONDARY
  66. )
  67. }
  68. async function getHardDeletedProjectIds({
  69. projectIds,
  70. READ_CONCURRENCY_PRIMARY,
  71. READ_CONCURRENCY_SECONDARY,
  72. }) {
  73. const doubleCheckProjectIdsOnPrimary = []
  74. async function checkProjectOnSecondary(projectId) {
  75. if (await checkProjectExistsOnSecondary(projectId)) {
  76. // Finding a project with secondary confidence is sufficient.
  77. return
  78. }
  79. // At this point, the secondaries deem this project as having orphaned docs.
  80. doubleCheckProjectIdsOnPrimary.push(projectId)
  81. }
  82. const hardDeletedProjectIds = []
  83. async function checkProjectOnPrimary(projectId) {
  84. if (await checkProjectExistsOnPrimary(projectId)) {
  85. // The project is actually live.
  86. return
  87. }
  88. hardDeletedProjectIds.push(projectId)
  89. }
  90. await promiseMapWithLimit(
  91. READ_CONCURRENCY_SECONDARY,
  92. projectIds,
  93. checkProjectOnSecondary
  94. )
  95. await promiseMapWithLimit(
  96. READ_CONCURRENCY_PRIMARY,
  97. doubleCheckProjectIdsOnPrimary,
  98. checkProjectOnPrimary
  99. )
  100. return hardDeletedProjectIds
  101. }
  102. module.exports = {
  103. getHardDeletedProjectIds,
  104. }