| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- const { setTimeout } = require('node:timers/promises')
- const Settings = require('@overleaf/settings')
- const { rclient } = require('./RedisManager')
- const ProjectManager = require('./ProjectManager')
- const logger = require('@overleaf/logger')
- const { promiseMapSettledWithLimit } = require('@overleaf/promise-utils')
- const docUpdaterKeys = Settings.redis.documentupdater.key_schema
- // iterate over keys asynchronously using redis scan (non-blocking)
- // handle all the cluster nodes or single redis server
- async function _getKeys(pattern, limit) {
- const nodes = (typeof rclient.nodes === 'function'
- ? rclient.nodes('master')
- : undefined) || [rclient]
- let keys = []
- for (const node of nodes) {
- keys = keys.concat(await _getKeysFromNode(node, pattern, limit))
- }
- return keys
- }
- async function _getKeysFromNode(node, pattern, limit = 1000) {
- let cursor = 0 // redis iterator
- const keySet = new Set() // use hash to avoid duplicate results
- const batchSize = Math.min(limit, 1000)
- while (true) {
- // scan over all keys looking for pattern
- const reply = await node.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize)
- cursor = reply[0]
- for (const key of reply[1]) {
- keySet.add(key)
- }
- const noResults = cursor === '0' // redis returns string results not numeric
- const limitReached = keySet.size >= limit
- if (noResults || limitReached) {
- return Array.from(keySet)
- } else {
- // avoid hitting redis too hard
- await setTimeout(10)
- }
- }
- }
- // extract ids from keys like DocsWithHistoryOps:57fd0b1f53a8396d22b2c24b
- // or docsInProject:{57fd0b1f53a8396d22b2c24b} (for redis cluster)
- function _extractIds(keyList) {
- const result = []
- for (const key of Array.from(keyList)) {
- const m = key.match(/:\{?([0-9a-f]{24})\}?/) // extract object id
- result.push(m[1])
- }
- return result
- }
- async function flushAllProjects(options) {
- logger.debug({ options }, 'listing all projects with docs')
- const projectKeys = await _getKeys(
- docUpdaterKeys.docsInProject({ project_id: '*' }),
- options.limit
- )
- const projectIds = _extractIds(projectKeys)
- if (options.dryRun) {
- return projectIds
- }
- const total = projectIds.length
- logger.info({ total, options }, 'flushing all projects')
- let flushed = 0
- const results = await promiseMapSettledWithLimit(
- options.concurrency,
- projectIds,
- async projectId => {
- await ProjectManager.promises.flushAndDeleteProjectWithLocks(projectId, {
- background: true,
- })
- flushed++
- if (options.logProgress && flushed % options.logProgress === 0) {
- logger.info({ flushed, total }, 'Flush all projects progress')
- }
- }
- )
- const success = []
- const failure = []
- for (let i = 0; i < results.length; i++) {
- if (results[i].status === 'rejected') {
- failure.push(projectIds[i])
- } else {
- success.push(projectIds[i])
- }
- }
- logger.info(
- { successCount: success.length, failureCount: failure.length },
- 'finished flushing all projects'
- )
- return { success, failure }
- }
- module.exports = {
- _extractIds,
- promises: {
- flushAllProjects,
- },
- }
|