#!/usr/bin/env node // To run in dev: // // docker-compose run --rm project-history scripts/clear_deleted.js // // In production: // // docker run --rm $(docker ps -lq) scripts/clear_deleted.js import async from 'async' import Settings from '@overleaf/settings' import redis from '@overleaf/redis-wrapper' import { db, ObjectId } from '../app/js/mongodb.js' import * as SyncManager from '../app/js/SyncManager.js' import * as UpdatesProcessor from '../app/js/UpdatesProcessor.js' const rclient = redis.createClient(Settings.redis.project_history) const Keys = Settings.redis.project_history.key_schema const argv = process.argv.slice(2) const limit = parseInt(argv[0], 10) || null const force = argv[1] === 'force' || false let projectNotFoundErrors = 0 let projectImportedFromV1Errors = 0 const projectsNotFound = [] const projectsImportedFromV1 = [] let projectNoHistoryIdErrors = 0 let projectsFailedErrors = 0 const projectsFailed = [] let projectsBrokenSyncErrors = 0 const projectsBrokenSync = [] function checkAndClear(project, callback) { const projectId = project.project_id console.log('checking project', projectId) // These can probably also be reset and their overleaf.history.id unset // (unless they are v1 projects). function checkNotV1Project(cb) { db.projects.findOne( { _id: new ObjectId(projectId) }, { projection: { overleaf: true } }, (err, result) => { console.log( '1. looking in mongo projects collection: err', err, 'result', JSON.stringify(result) ) if (err) { return cb(err) } if (!result) { return cb(new Error('project not found in mongo')) } if (result && result.overleaf && !result.overleaf.id) { if (result.overleaf.history.id) { console.log( ' - project is not imported from v1 and has a history id - ok to resync' ) return cb() } else { console.log( ' - project is not imported from v1 but does not have a history id' ) return cb(new Error('no history id')) } } else { cb(new Error('project is imported from v1 - will not resync it')) } } ) } function startResync(cb) { if (force) { console.log('2. starting resync for', projectId) SyncManager.startResync(projectId, err => { if (err) { console.log('ERR', JSON.stringify(err.message)) return cb(err) } setTimeout(cb, 3000) // include a delay to allow the request to be processed }) } else { console.log('2. dry run, would start resync for', projectId) cb() } } function forceFlush(cb) { if (force) { console.log('3. forcing a flush for', projectId) UpdatesProcessor.processUpdatesForProject(projectId, err => { console.log('err', err) return cb(err) }) } else { console.log('3. dry run, would force a flush for', projectId) cb() } } function watchRedisQueue(cb) { const key = Keys.projectHistoryOps({ project_id: projectId }) function checkQueueEmpty(_callback) { rclient.llen(key, (err, result) => { console.log('LLEN', projectId, err, result) if (err) { _callback(err) } if (result === 0) { _callback() } else { _callback(new Error('queue not empty')) } }) } if (force) { console.log('4. checking redis queue key', key) async.retry({ times: 30, interval: 1000 }, checkQueueEmpty, err => { cb(err) }) } else { console.log('4. dry run, would check redis key', key) cb() } } function checkMongoFailureEntry(cb) { if (force) { console.log('5. checking key in mongo projectHistoryFailures', projectId) db.projectHistoryFailures.findOne( { project_id: projectId }, { projection: { _id: 1 } }, (err, result) => { console.log('got result', err, result) if (err) { return cb(err) } if (result) { return cb(new Error('failure record still exists')) } return cb() } ) } else { console.log('5. would check failure record for', projectId, 'in mongo') cb() } } // do the checks and deletions async.waterfall( [ checkNotV1Project, startResync, forceFlush, watchRedisQueue, checkMongoFailureEntry, ], err => { if (!err) { return setTimeout(callback, 1000) // include a 1 second delay } else if (err.message === 'project not found in mongo') { projectNotFoundErrors++ projectsNotFound.push(projectId) return callback() } else if (err.message === 'no history id') { projectNoHistoryIdErrors++ return callback() } else if ( err.message === 'project is imported from v1 - will not resync it' ) { projectImportedFromV1Errors++ projectsImportedFromV1.push(projectId) return callback() } else if ( err.message === 'history store a non-success status code: 422' ) { projectsFailedErrors++ projectsFailed.push(projectId) return callback() } else if (err.message === 'sync ongoing') { projectsBrokenSyncErrors++ projectsBrokenSync.push(projectId) return callback() } else { console.log('error:', err) return callback() } } ) } // find all the broken projects from the failure records const errorsToResync = [ 'Error: history store a non-success status code: 422', 'OpsOutOfOrderError: project structure version out of order', ] async function main() { const results = await db.projectHistoryFailures .find({ error: { $in: errorsToResync } }) .toArray() console.log('number of queues without history store 442 =', results.length) // now check if the project is truly deleted in mongo async.eachSeries(results.slice(0, limit), checkAndClear, err => { console.log('Final error status', err) console.log( 'Project flush failed again errors', projectsFailedErrors, projectsFailed ) console.log( 'Project flush ongoing errors', projectsBrokenSyncErrors, projectsBrokenSync ) console.log( 'Project not found errors', projectNotFoundErrors, projectsNotFound ) console.log('Project without history_id errors', projectNoHistoryIdErrors) console.log( 'Project imported from V1 errors', projectImportedFromV1Errors, projectsImportedFromV1 ) process.exit() }) } main().catch(error => { console.error(error) process.exit(1) })