find_dangling_comments.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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', 'project-modified-since'],
  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. projectModifiedSince: args['project-modified-since']
  24. ? new Date(args['project-modified-since'])
  25. : null,
  26. }
  27. }
  28. function usage() {
  29. console.log(`Usage: find_dangling_comments.mjs [OPTS]
  30. Options:
  31. --min-project-id Start scanning at this project id
  32. --max-project-id Stop scanning at this project id
  33. --project-modified-since Only consider projects that were modified after the given date
  34. Example: 2020-01-01`)
  35. }
  36. async function main() {
  37. let projectsProcessed = 0
  38. let projectsFound = 0
  39. for await (const projectId of fetchProjectIds()) {
  40. projectsProcessed += 1
  41. const threadIds = await fetchThreadIds(projectId)
  42. const danglingThreadIds = await findDanglingThreadIds(projectId, threadIds)
  43. if (danglingThreadIds.length > 0) {
  44. console.log(
  45. `Project ${projectId} has dangling threads: ${danglingThreadIds.join(', ')}`
  46. )
  47. projectsFound += 1
  48. }
  49. if (projectsProcessed % 1000 === 0) {
  50. console.log(
  51. `${projectsProcessed} projects processed - Last project: ${projectId}`
  52. )
  53. }
  54. }
  55. console.log(`${projectsFound} projects with dangling comments found`)
  56. }
  57. function fetchProjectIds() {
  58. const clauses = []
  59. if (OPTS.minProjectId != null) {
  60. clauses.push({ project_id: { $gte: new ObjectId(OPTS.minProjectId) } })
  61. }
  62. if (OPTS.maxProjectId != null) {
  63. clauses.push({ project_id: { $lte: new ObjectId(OPTS.maxProjectId) } })
  64. }
  65. if (OPTS.projectModifiedSince) {
  66. clauses.push({ lastUpdated: { $gte: OPTS.projectModifiedSince } })
  67. }
  68. const query = clauses.length > 0 ? { $and: clauses } : {}
  69. return db.projects
  70. .find(query, {
  71. projection: { _id: 1 },
  72. readPreference: READ_PREFERENCE_SECONDARY,
  73. })
  74. .map(x => x._id.toString())
  75. }
  76. async function fetchThreadIds(projectId) {
  77. const docs = db.docs.find(
  78. {
  79. project_id: new ObjectId(projectId),
  80. deleted: { $ne: true },
  81. $or: [{ 'ranges.comments.0': { $exists: true } }, { inS3: true }],
  82. },
  83. {
  84. projection: { 'ranges.comments': 1, inS3: 1 },
  85. readPreference: READ_PREFERENCE_SECONDARY,
  86. }
  87. )
  88. const threadIds = new Set()
  89. for await (const doc of docs) {
  90. let comments = []
  91. if (doc.inS3) {
  92. try {
  93. const archivedDoc = await DocstoreManager.promises.getDoc(
  94. projectId,
  95. doc._id,
  96. { peek: true }
  97. )
  98. comments = archivedDoc.ranges?.comments ?? []
  99. } catch (err) {
  100. if (err instanceof NotFoundError) {
  101. console.warn(`Doc ${doc._id} in project ${projectId} not found`)
  102. } else {
  103. throw err
  104. }
  105. }
  106. } else {
  107. comments = doc.ranges?.comments
  108. }
  109. for (const comment of comments) {
  110. threadIds.add(comment.op.t.toString())
  111. }
  112. }
  113. return threadIds
  114. }
  115. /**
  116. * @param {string} projectId
  117. * @param {Set<string>} threadIds
  118. */
  119. async function findDanglingThreadIds(projectId, threadIds) {
  120. const rooms = await db.rooms.find(
  121. { project_id: projectId, thread_id: { $exists: true } },
  122. { readPreference: READ_PREFERENCE_SECONDARY }
  123. )
  124. const existingThreadIds = new Set()
  125. for await (const room of rooms) {
  126. existingThreadIds.add(room.thread_id.toString())
  127. }
  128. const danglingThreadIds = []
  129. for (const threadId of threadIds) {
  130. if (!existingThreadIds.has(threadId)) {
  131. danglingThreadIds.push(threadId)
  132. }
  133. }
  134. return danglingThreadIds
  135. }
  136. try {
  137. await main()
  138. process.exit(0)
  139. } catch (err) {
  140. console.error(err)
  141. process.exit(1)
  142. }