recover_docs_from_redis.js 5.0 KB

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