2023-01-13 07:42:29 -05:00
|
|
|
#!/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(
|
2023-12-15 05:51:11 -05:00
|
|
|
{ _id: new ObjectId(projectId) },
|
2023-01-13 07:42:29 -05:00
|
|
|
{ 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)
|
|
|
|
})
|