ProjectFlusher.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. const { setTimeout } = require('node:timers/promises')
  2. const Settings = require('@overleaf/settings')
  3. const { rclient } = require('./RedisManager')
  4. const ProjectManager = require('./ProjectManager')
  5. const logger = require('@overleaf/logger')
  6. const { promiseMapSettledWithLimit } = require('@overleaf/promise-utils')
  7. const docUpdaterKeys = Settings.redis.documentupdater.key_schema
  8. // iterate over keys asynchronously using redis scan (non-blocking)
  9. // handle all the cluster nodes or single redis server
  10. async function _getKeys(pattern, limit) {
  11. const nodes = (typeof rclient.nodes === 'function'
  12. ? rclient.nodes('master')
  13. : undefined) || [rclient]
  14. let keys = []
  15. for (const node of nodes) {
  16. keys = keys.concat(await _getKeysFromNode(node, pattern, limit))
  17. }
  18. return keys
  19. }
  20. async function _getKeysFromNode(node, pattern, limit = 1000) {
  21. let cursor = 0 // redis iterator
  22. const keySet = new Set() // use hash to avoid duplicate results
  23. const batchSize = Math.min(limit, 1000)
  24. while (true) {
  25. // scan over all keys looking for pattern
  26. const reply = await node.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize)
  27. cursor = reply[0]
  28. for (const key of reply[1]) {
  29. keySet.add(key)
  30. }
  31. const noResults = cursor === '0' // redis returns string results not numeric
  32. const limitReached = keySet.size >= limit
  33. if (noResults || limitReached) {
  34. return Array.from(keySet)
  35. } else {
  36. // avoid hitting redis too hard
  37. await setTimeout(10)
  38. }
  39. }
  40. }
  41. // extract ids from keys like DocsWithHistoryOps:57fd0b1f53a8396d22b2c24b
  42. // or docsInProject:{57fd0b1f53a8396d22b2c24b} (for redis cluster)
  43. function _extractIds(keyList) {
  44. const result = []
  45. for (const key of Array.from(keyList)) {
  46. const m = key.match(/:\{?([0-9a-f]{24})\}?/) // extract object id
  47. result.push(m[1])
  48. }
  49. return result
  50. }
  51. async function flushAllProjects(options) {
  52. logger.debug({ options }, 'listing all projects with docs')
  53. const projectKeys = await _getKeys(
  54. docUpdaterKeys.docsInProject({ project_id: '*' }),
  55. options.limit
  56. )
  57. const projectIds = _extractIds(projectKeys)
  58. if (options.dryRun) {
  59. return projectIds
  60. }
  61. const total = projectIds.length
  62. logger.info({ total, options }, 'flushing all projects')
  63. let flushed = 0
  64. const results = await promiseMapSettledWithLimit(
  65. options.concurrency,
  66. projectIds,
  67. async projectId => {
  68. await ProjectManager.promises.flushAndDeleteProjectWithLocks(projectId, {
  69. background: true,
  70. })
  71. flushed++
  72. if (options.logProgress && flushed % options.logProgress === 0) {
  73. logger.info({ flushed, total }, 'Flush all projects progress')
  74. }
  75. }
  76. )
  77. const success = []
  78. const failure = []
  79. for (let i = 0; i < results.length; i++) {
  80. if (results[i].status === 'rejected') {
  81. failure.push(projectIds[i])
  82. } else {
  83. success.push(projectIds[i])
  84. }
  85. }
  86. logger.info(
  87. { successCount: success.length, failureCount: failure.length },
  88. 'finished flushing all projects'
  89. )
  90. return { success, failure }
  91. }
  92. module.exports = {
  93. _extractIds,
  94. promises: {
  95. flushAllProjects,
  96. },
  97. }