delete_orphaned_chat_threads.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. const READ_CONCURRENCY_SECONDARY =
  2. parseInt(process.env.READ_CONCURRENCY_SECONDARY, 10) || 1000
  3. const READ_CONCURRENCY_PRIMARY =
  4. parseInt(process.env.READ_CONCURRENCY_PRIMARY, 10) || 500
  5. const WRITE_CONCURRENCY = parseInt(process.env.WRITE_CONCURRENCY, 10) || 10
  6. const BATCH_SIZE = parseInt(process.env.BATCH_SIZE, 10) || 100
  7. const DRY_RUN = process.env.DRY_RUN !== 'false'
  8. const MAX_CHATS_TO_DESTROY =
  9. parseInt(process.env.MAX_CHATS_TO_DESTROY, 10) || false
  10. // persist fallback in order to keep batchedUpdate in-sync
  11. process.env.BATCH_SIZE = BATCH_SIZE
  12. // raise mongo timeout to 10mins if otherwise unspecified
  13. process.env.MONGO_SOCKET_TIMEOUT =
  14. parseInt(process.env.MONGO_SOCKET_TIMEOUT, 10) || 600000
  15. const { ObjectId, ReadPreference } = require('mongodb')
  16. const { db } = require('../app/src/infrastructure/mongodb')
  17. const { promiseMapWithLimit } = require('../app/src/util/promises')
  18. const { batchedUpdate } = require('./helpers/batchedUpdate')
  19. const ChatApiHandler = require('../app/src/Features/Chat/ChatApiHandler')
  20. console.log({
  21. DRY_RUN,
  22. WRITE_CONCURRENCY,
  23. BATCH_SIZE,
  24. MAX_CHATS_TO_DESTROY,
  25. })
  26. const RESULT = {
  27. DRY_RUN,
  28. projectChatsDestroyed: 0,
  29. continueFrom: null,
  30. }
  31. async function processBatch(_, rooms) {
  32. if (rooms.length && rooms[0]._id) {
  33. RESULT.continueFrom = rooms[0]._id
  34. }
  35. // Logic taken from delete_orphaned_docs_online_check.js
  36. // gets projectIds from rooms,
  37. // then checks 'expired' status of project
  38. const projectIds = Array.from(
  39. new Set(rooms.map(room => room.project_id.toString()))
  40. ).map(ObjectId)
  41. console.log(
  42. `Checking projects (${projectIds.length})`,
  43. JSON.stringify(projectIds)
  44. )
  45. const doubleCheckProjectIdsOnPrimary = []
  46. async function checkProjectOnSecondary(projectId) {
  47. if (await checkProjectExistsOnSecondary(projectId)) {
  48. // Finding a project with secondary confidence is sufficient.
  49. return
  50. }
  51. // At this point, the secondaries deem this project as having orphaned chat.
  52. doubleCheckProjectIdsOnPrimary.push(projectId)
  53. }
  54. const projectsWithOrphanedChat = []
  55. async function checkProjectOnPrimary(projectId) {
  56. if (await checkProjectExistsOnPrimary(projectId)) {
  57. // The project is actually live.
  58. return
  59. }
  60. projectsWithOrphanedChat.push(projectId)
  61. }
  62. await promiseMapWithLimit(
  63. READ_CONCURRENCY_SECONDARY,
  64. projectIds,
  65. checkProjectOnSecondary
  66. )
  67. await promiseMapWithLimit(
  68. READ_CONCURRENCY_PRIMARY,
  69. doubleCheckProjectIdsOnPrimary,
  70. checkProjectOnPrimary
  71. )
  72. console.log(
  73. `Destroying chat for projects (${projectsWithOrphanedChat.length})`,
  74. JSON.stringify(projectsWithOrphanedChat)
  75. )
  76. if (!DRY_RUN) {
  77. await promiseMapWithLimit(
  78. WRITE_CONCURRENCY,
  79. projectsWithOrphanedChat,
  80. ChatApiHandler.promises.destroyProject
  81. )
  82. }
  83. RESULT.projectChatsDestroyed += projectsWithOrphanedChat.length
  84. console.log(RESULT)
  85. if (
  86. MAX_CHATS_TO_DESTROY &&
  87. RESULT.projectChatsDestroyed >= MAX_CHATS_TO_DESTROY
  88. ) {
  89. console.log(
  90. `MAX_CHATS_TO_DELETE limit (${MAX_CHATS_TO_DESTROY}) reached. Stopping.`
  91. )
  92. process.exit(0)
  93. }
  94. }
  95. async function getDeletedProject(projectId, readPreference) {
  96. return await db.deletedProjects.findOne(
  97. { 'deleterData.deletedProjectId': projectId },
  98. {
  99. // There is no index on .project. Pull down something small.
  100. projection: { 'project._id': 1 },
  101. readPreference,
  102. }
  103. )
  104. }
  105. async function getProject(projectId, readPreference) {
  106. return await db.projects.findOne(
  107. { _id: projectId },
  108. {
  109. // Pulling down an empty object is fine for differentiating with null.
  110. projection: { _id: 0 },
  111. readPreference,
  112. }
  113. )
  114. }
  115. async function checkProjectExistsWithReadPreference(projectId, readPreference) {
  116. // NOTE: Possible race conditions!
  117. // There are two processes which are racing with our queries:
  118. // 1. project deletion
  119. // 2. project restoring
  120. // For 1. we check the projects collection before deletedProjects.
  121. // If a project were to be delete in this very moment, we should see the
  122. // soft-deleted entry which is created before deleting the projects entry.
  123. // For 2. we check the projects collection after deletedProjects again.
  124. // If a project were to be restored in this very moment, it is very likely
  125. // to see the projects entry again.
  126. // Unlikely edge case: Restore+Deletion in rapid succession.
  127. // We could add locking to the ProjectDeleter for ruling ^ out.
  128. if (await getProject(projectId, readPreference)) {
  129. // The project is live.
  130. return true
  131. }
  132. const deletedProject = await getDeletedProject(projectId, readPreference)
  133. if (deletedProject && deletedProject.project) {
  134. // The project is registered for hard-deletion.
  135. return true
  136. }
  137. if (await getProject(projectId, readPreference)) {
  138. // The project was just restored.
  139. return true
  140. }
  141. // The project does not exist.
  142. return false
  143. }
  144. async function checkProjectExistsOnPrimary(projectId) {
  145. return await checkProjectExistsWithReadPreference(
  146. projectId,
  147. ReadPreference.PRIMARY
  148. )
  149. }
  150. async function checkProjectExistsOnSecondary(projectId) {
  151. return await checkProjectExistsWithReadPreference(
  152. projectId,
  153. ReadPreference.SECONDARY
  154. )
  155. }
  156. async function main() {
  157. const projection = {
  158. _id: 1,
  159. project_id: 1,
  160. }
  161. await batchedUpdate('rooms', {}, processBatch, projection)
  162. console.log('Final')
  163. console.log(RESULT)
  164. }
  165. main()
  166. .then(() => {
  167. console.log('Done.')
  168. process.exit(0)
  169. })
  170. .catch(error => {
  171. console.error({ error })
  172. process.exit(1)
  173. })