delete_orphaned_data_helper.js 3.3 KB

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