find_dangling_comments.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. // @ts-check
  2. import minimist from 'minimist'
  3. import {
  4. db,
  5. ObjectId,
  6. READ_PREFERENCE_SECONDARY,
  7. } from '../app/src/infrastructure/mongodb.js'
  8. import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js'
  9. import { NotFoundError } from '../app/src/Features/Errors/Errors.js'
  10. const OPTS = parseArgs()
  11. function parseArgs() {
  12. const args = minimist(process.argv.slice(2), {
  13. string: ['min-project-id', 'max-project-id'],
  14. boolean: ['help'],
  15. })
  16. if (args.help) {
  17. usage()
  18. process.exit(0)
  19. }
  20. return {
  21. minProjectId: args['min-project-id'] ?? null,
  22. maxProjectId: args['max-project-id'] ?? null,
  23. }
  24. }
  25. function usage() {
  26. console.log(`Usage: find_dangling_comments.mjs [OPTS]
  27. Options:
  28. --min-project-id Start scanning at this project id
  29. --max-project-id Stop scanning at this project id`)
  30. }
  31. async function main() {
  32. let projectsProcessed = 0
  33. let projectsFound = 0
  34. for await (const { projectId, threadIds } of fetchThreadIdsByProject()) {
  35. projectsProcessed += 1
  36. const danglingThreadIds = await findDanglingThreadIds(projectId, threadIds)
  37. if (danglingThreadIds.length > 0) {
  38. console.log(
  39. `Project ${projectId} has dangling threads: ${danglingThreadIds.join(', ')}`
  40. )
  41. projectsFound += 1
  42. }
  43. if (projectsProcessed % 10000 === 0) {
  44. console.log(
  45. `${projectsProcessed} projects processed - Last project: ${projectId}`
  46. )
  47. }
  48. }
  49. console.log(`${projectsFound} projects with dangling comments found`)
  50. }
  51. async function* fetchThreadIdsByProject() {
  52. const clauses = []
  53. clauses.push({
  54. deleted: { $ne: true },
  55. $or: [{ 'ranges.comments.0': { $exists: true } }, { inS3: true }],
  56. })
  57. if (OPTS.minProjectId != null) {
  58. clauses.push({ project_id: { $gte: new ObjectId(OPTS.minProjectId) } })
  59. }
  60. if (OPTS.maxProjectId != null) {
  61. clauses.push({ project_id: { $lte: new ObjectId(OPTS.maxProjectId) } })
  62. }
  63. const docs = db.docs.find(
  64. { $and: clauses },
  65. {
  66. sort: { project_id: 1 },
  67. projection: { project_id: 1, 'ranges.comments': 1, inS3: 1 },
  68. readPreference: READ_PREFERENCE_SECONDARY,
  69. }
  70. )
  71. let projectId
  72. let threadIds = new Set()
  73. for await (const doc of docs) {
  74. if (projectId !== doc.project_id) {
  75. yield { projectId, threadIds }
  76. projectId = doc.project_id
  77. threadIds = new Set()
  78. }
  79. projectId = doc.project_id
  80. let comments = []
  81. if (doc.inS3) {
  82. try {
  83. const archivedDoc = await DocstoreManager.promises.getDoc(
  84. projectId,
  85. doc._id,
  86. { peek: true }
  87. )
  88. comments = archivedDoc.ranges?.comments ?? []
  89. } catch (err) {
  90. if (err instanceof NotFoundError) {
  91. console.warn(`Doc ${doc._id} in project ${projectId} not found`)
  92. } else {
  93. throw err
  94. }
  95. }
  96. } else {
  97. comments = doc.ranges?.comments
  98. }
  99. for (const comment of comments) {
  100. threadIds.add(comment.op.t.toString())
  101. }
  102. }
  103. yield { projectId, threadIds }
  104. } /**
  105. * @param {string} projectId
  106. * @param {Set<string>} threadIds
  107. */
  108. async function findDanglingThreadIds(projectId, threadIds) {
  109. const rooms = await db.rooms.find(
  110. { project_id: projectId, thread_id: { $exists: true } },
  111. { readPreference: READ_PREFERENCE_SECONDARY }
  112. )
  113. const existingThreadIds = new Set()
  114. for await (const room of rooms) {
  115. existingThreadIds.add(room.thread_id.toString())
  116. }
  117. const danglingThreadIds = []
  118. for (const threadId of threadIds) {
  119. if (!existingThreadIds.has(threadId)) {
  120. danglingThreadIds.push(threadId)
  121. }
  122. }
  123. return danglingThreadIds
  124. }
  125. try {
  126. await main()
  127. process.exit(0)
  128. } catch (err) {
  129. console.error(err)
  130. process.exit(1)
  131. }