| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- // @ts-check
- const Settings = require('@overleaf/settings')
- const logger = require('@overleaf/logger')
- const RedisManager = require('../app/js/RedisManager')
- const minimist = require('minimist')
- const { db, ObjectId } = require('../app/js/mongodb')
- const ProjectManager = require('../app/js/ProjectManager')
- const OError = require('@overleaf/o-error')
- const docUpdaterKeys = Settings.redis.documentupdater.key_schema
- const rclient = RedisManager.rclient
- const { verbose, commit, ...args } = minimist(process.argv.slice(2), {
- boolean: ['verbose', 'commit'],
- string: ['batchSize'],
- default: {
- batchSize: '1000',
- },
- })
- logger.logger.level(verbose ? 'debug' : 'warn')
- const batchSize = parseInt(args.batchSize, 10)
- /**
- * @typedef {import('ioredis').Redis} Redis
- */
- /**
- *
- * @param {string} key
- * @return {string|void}
- */
- function extractDocId(key) {
- const matches = key.match(/DocVersion:\{(.*?)\}/)
- if (matches) {
- return matches[1]
- }
- }
- /**
- *
- * @param {string} docId
- * @return {Promise<{projectId: string, historyId: string}>}
- */
- async function getHistoryId(docId) {
- const doc = await db.docs.findOne(
- { _id: new ObjectId(docId) },
- { projection: { project_id: 1 }, readPreference: 'secondaryPreferred' }
- )
- if (!doc) {
- throw new OError('Doc not present in mongo', { docId })
- }
- const project = await db.projects.findOne(
- { _id: doc.project_id },
- {
- projection: { 'overleaf.history': 1 },
- readPreference: 'secondaryPreferred',
- }
- )
- if (!project?.overleaf?.history?.id) {
- throw new OError('Project not present in mongo (or has no history id)', {
- docId,
- project,
- doc,
- })
- }
- return {
- historyId: project?.overleaf?.history?.id,
- projectId: doc.project_id.toString(),
- }
- }
- /**
- * @typedef {Object} UpdateableDoc
- * @property {string} docId
- * @property {string} projectId
- * @property {string} historyId
- */
- /**
- *
- * @param {Redis} node
- * @param {Array<string>} docIds
- * @return {Promise<Array<UpdateableDoc>>}
- */
- async function findDocsWithMissingHistoryIds(node, docIds) {
- const fromRedis = await node.mget(
- docIds
- .map(docId => [
- docUpdaterKeys.docVersion({ doc_id: docId }),
- docUpdaterKeys.projectHistoryId({ doc_id: docId }),
- docUpdaterKeys.lastUpdatedAt({ doc_id: docId }),
- docUpdaterKeys.unflushedTime({ doc_id: docId }),
- ])
- .flat()
- )
- const results = []
- const nKeysPerDoc = 4
- for (const [index, docId] of docIds.entries()) {
- const [docVersion, historyId, lastUpdatedAt, unflushedTime] =
- fromRedis.slice(index * nKeysPerDoc, index * nKeysPerDoc + nKeysPerDoc)
- if (!docVersion) {
- // Already removed from redis.
- continue
- }
- if (!historyId) {
- try {
- const { projectId, historyId } = await getHistoryId(docId)
- results.push({
- projectId,
- historyId,
- docId,
- docVersion,
- lastUpdatedAt,
- unflushedTime,
- })
- } catch (error) {
- logger.warn(
- { error },
- 'Error gathering data for doc with missing history id'
- )
- }
- }
- }
- return results
- }
- /**
- *
- * @param {Array<UpdateableDoc>} updates
- * @return {Promise<void>}
- */
- async function fixAndFlushProjects(updates) {
- for (const update of updates) {
- if (commit) {
- try {
- await rclient.set(
- docUpdaterKeys.projectHistoryId({ doc_id: update.docId }),
- update.historyId
- )
- logger.debug({ ...update }, 'Set history id in redis')
- await ProjectManager.promises.flushAndDeleteProjectWithLocks(
- update.projectId,
- {}
- )
- logger.debug({ ...update }, 'Flushed project')
- } catch (err) {
- logger.error({ err, ...update }, 'Error fixing and flushing project')
- }
- } else {
- logger.debug(
- { ...update },
- 'Would have set history id in redis and flushed'
- )
- }
- }
- }
- /**
- *
- * @param {Array<Redis>} nodes
- * @param {number} batchSize
- * @return {Promise<void>}
- */
- async function scanNodes(nodes, batchSize = 1000) {
- let scanned = 0
- for (const node of nodes) {
- const stream = node.scanStream({
- match: docUpdaterKeys.docVersion({ doc_id: '*' }),
- count: batchSize,
- })
- for await (const docKeys of stream) {
- if (docKeys.length === 0) {
- continue
- }
- stream.pause()
- scanned += docKeys.length
- const docIds = docKeys
- .map((/** @type {string} */ docKey) => extractDocId(docKey))
- .filter(Boolean)
- try {
- const updates = await findDocsWithMissingHistoryIds(node, docIds)
- if (updates.length > 0) {
- logger.info({ updates }, 'Found doc(s) with missing history ids')
- await fixAndFlushProjects(updates)
- }
- } catch (error) {
- logger.error({ docKeys }, 'Error processing batch')
- } finally {
- stream.resume()
- }
- }
- logger.info({ scanned, server: node.serverInfo.role }, 'Scanned node')
- }
- }
- async function main({ batchSize }) {
- const nodes = (typeof rclient.nodes === 'function'
- ? rclient.nodes('master')
- : undefined) || [rclient]
- await scanNodes(nodes, batchSize)
- }
- let code = 0
- main({ batchSize })
- .then(() => {
- logger.info({}, 'done')
- })
- .catch(error => {
- logger.error({ error }, 'error')
- code = 1
- })
- .finally(() => {
- rclient.quit().then(() => process.exit(code))
- })
|