recover_docs_from_redis.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. // recover docs from redis where there is no doc in mongo but the project exists
  2. const minimist = require('minimist')
  3. const { db, waitForDb, ObjectId } = require('../app/src/infrastructure/mongodb')
  4. const ProjectEntityUpdateHandler = require('../app/src/Features/Project/ProjectEntityUpdateHandler')
  5. const ProjectEntityRestoreHandler = require('../app/src/Features/Project/ProjectEntityRestoreHandler')
  6. const RedisWrapper = require('@overleaf/redis-wrapper')
  7. const Settings = require('@overleaf/settings')
  8. const logger = require('@overleaf/logger')
  9. const opts = parseArgs()
  10. const redis = RedisWrapper.createClient(Settings.redis.web)
  11. function parseArgs() {
  12. const args = minimist(process.argv.slice(2), {
  13. boolean: ['commit'],
  14. })
  15. const commit = args.commit
  16. return { commit, maxDocSize: 2 * 1024 * 1024 }
  17. }
  18. function extractObjectId(s) {
  19. const m = s.match(/:\{?([0-9a-f]{24})\}?/)
  20. return m[1]
  21. }
  22. async function main() {
  23. await waitForDb()
  24. logger.info({ opts }, 'removing deleted docs')
  25. let cursor = 0
  26. do {
  27. const [newCursor, doclinesKeys] = await redis.scan(
  28. cursor,
  29. 'MATCH',
  30. 'doclines:{*}',
  31. 'COUNT',
  32. 1000
  33. )
  34. const docIds = doclinesKeys.map(extractObjectId)
  35. for (const docId of docIds) {
  36. await processDoc(docId)
  37. }
  38. cursor = newCursor
  39. } while (cursor !== '0')
  40. if (!opts.commit) {
  41. console.log('This was a dry run. Re-run with --commit to apply changes')
  42. }
  43. }
  44. async function processDoc(docId) {
  45. // check if the doc is in mongo.. if so ignore it
  46. const docCount = await db.docs.find({ _id: ObjectId(docId) }).count()
  47. if (docCount > 0) {
  48. logger.debug({ docId }, 'doc is present in mongo - no recovery needed')
  49. return
  50. }
  51. // get the doc from redis and check if it has a project id
  52. const doc = await getDoc(docId)
  53. const projectId = doc.projectId
  54. if (!projectId) {
  55. logger.warn(
  56. { docId, doc },
  57. 'projectId not available in redis, cannot restore - skipping'
  58. )
  59. // we could delete the document in redis here since we have no way to recover it
  60. return
  61. }
  62. // check that the project is in mongo, if not delete the doc
  63. const project = await db.projects.findOne({ _id: ObjectId(projectId) })
  64. if (!project) {
  65. logger.warn(
  66. { docId },
  67. 'project not present in mongo - could remove doc from redis'
  68. )
  69. return
  70. }
  71. // if the doc is too big we will need to convert it to a file, skip it for now
  72. const size = doc.lines.reduce((sum, line) => sum + line.length + 1, 0)
  73. if (size > opts.maxDocSize) {
  74. logger.warn(
  75. { docId, projectId, size },
  76. 'doc that exceeds max size, cannot restore'
  77. )
  78. return
  79. }
  80. // now we have a doc content from redis, in a project where the doc has been deleted
  81. const restoredName = ProjectEntityRestoreHandler.generateRestoredName(
  82. doc.name || 'unknown',
  83. 'recovered'
  84. )
  85. logger.info(
  86. { docId, projectId, restoredName, commit: opts.commit },
  87. 'recovering doc from redis to mongo'
  88. )
  89. if (opts.commit) {
  90. const folderId = project.rootFolder[0]._id
  91. try {
  92. await ProjectEntityUpdateHandler.promises.addDocWithRanges(
  93. projectId,
  94. folderId,
  95. restoredName,
  96. doc.lines,
  97. doc.ranges,
  98. null
  99. )
  100. await deleteDocFromRedis(projectId, docId)
  101. } catch (err) {
  102. logger.error(
  103. { docId, projectId, restoreErr: err },
  104. 'error restoring doc from redis to mongo'
  105. )
  106. }
  107. }
  108. }
  109. async function getDoc(docId) {
  110. const [projectId, lines, ranges, pathname] = await redis.mget(
  111. `ProjectId:{${docId}}`,
  112. `doclines:{${docId}}`,
  113. `Ranges:{${docId}}`,
  114. `Pathname:{${docId}}`
  115. )
  116. const name = pathname?.split('/').pop()
  117. return {
  118. projectId,
  119. id: docId,
  120. lines: JSON.parse(lines),
  121. ranges: ranges ? JSON.parse(ranges) : {},
  122. name: name || 'unnamed',
  123. }
  124. }
  125. async function deleteDocFromRedis(projectId, docId) {
  126. await redis.del(
  127. `Blocking:{${docId}}`,
  128. `doclines:{${docId}}`,
  129. `DocOps:{${docId}}`,
  130. `DocVersion:{${docId}}`,
  131. `DocHash:{${docId}}`,
  132. `ProjectId:{${docId}}`,
  133. `Ranges:{${docId}}`,
  134. `UnflushedTime:{${docId}}`,
  135. `Pathname:{${docId}}`,
  136. `ProjectHistoryId:{${docId}}`,
  137. `ProjectHistoryType:{${docId}}`,
  138. `PendingUpdates:{${docId}}`,
  139. `lastUpdatedAt:{${docId}}`,
  140. `lastUpdatedBy:{${docId}}`
  141. )
  142. await redis.srem(`DocsIn:{${projectId}}`, projectId)
  143. }
  144. main()
  145. .then(() => {
  146. process.exit(0)
  147. })
  148. .catch(err => {
  149. console.error(err)
  150. process.exit(1)
  151. })