Avoid duplicating a math-closing dollar sign (#11227)

GitOrigin-RevId: ef2ef77e26df59d1af3df6dc664e284d3c70102d
This commit is contained in:
Alf Eaton
2023-01-13 12:42:29 +00:00
committed by Copybot
parent 377a8aed60
commit ee85d948e2
268 changed files with 57782 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
/* eslint-env mongo */
// add a TTL index to expire entries for completed resyncs in the
// projectHistorySyncState collection. The entries should only be expired if
// resyncProjectStructure is false and resyncDocContents is a zero-length array.
const now = Date.now()
const inTheFuture = now + 24 * 3600 * 1000
db.projectHistorySyncState.ensureIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0, background: true }
)
db.projectHistorySyncState.updateMany(
{
resyncProjectStructure: false,
resyncDocContents: [],
expiresAt: { $exists: false },
},
{ $set: { expiresAt: new Date(inTheFuture) } }
)

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env node
// Clear timestamps which don't have any corresponding history ops
// usage: scripts/flush_all.js <limit>
import logger from '@overleaf/logger'
import * as RedisManager from '../app/js/RedisManager.js'
const argv = process.argv.slice(2)
const limit = parseInt(argv[0], 10) || null
// find all dangling timestamps and clear them
async function main() {
logger.info(
{ limit },
'running redis scan for project timestamps, this may take a while'
)
const projectIdsWithFirstOpTimestamps =
await RedisManager.promises.getProjectIdsWithFirstOpTimestamps(limit)
const totalTimestamps = projectIdsWithFirstOpTimestamps.length
logger.info(
{ totalTimestamps },
'scan completed, now clearing dangling timestamps'
)
let clearedTimestamps = 0
let processed = 0
for (const projectId of projectIdsWithFirstOpTimestamps) {
const result = await RedisManager.promises.clearDanglingFirstOpTimestamp(
projectId
)
processed++
clearedTimestamps += result
if (processed % 1000 === 0) {
logger.info(
{ processed, totalTimestamps, clearedTimestamps },
'clearing timestamps'
)
}
}
logger.info({ processed, totalTimestamps, clearedTimestamps }, 'completed')
process.exit(0)
}
main()

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env node
import async from 'async'
import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import redis from '@overleaf/redis-wrapper'
import { db, ObjectId } from '../app/js/mongodb.js'
logger.logger.level('fatal')
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 delay = 0
function checkAndClear(project, callback) {
const projectId = project.project_id
function checkDeleted(cb) {
db.projects.findOne(
{ _id: ObjectId(projectId) },
{ projection: { _id: 1 } },
(err, result) => {
if (err) {
cb(err)
} else if (!result) {
// project not found, but we still need to look at deletedProjects
cb()
} else {
console.log(`Project ${projectId} found in projects`)
cb(new Error('error: project still exists'))
}
}
)
}
function checkRecoverable(cb) {
db.deletedProjects.findOne(
{
// this condition makes use of the index
'deleterData.deletedProjectId': ObjectId(projectId),
// this condition checks if the deleted project has expired
'project._id': ObjectId(projectId),
},
{ projection: { _id: 1 } },
(err, result) => {
if (err) {
cb(err)
} else if (!result) {
console.log(
`project ${projectId} has been deleted - safe to clear queue`
)
cb()
} else {
console.log(`Project ${projectId} found in deletedProjects`)
cb(new Error('error: project still exists'))
}
}
)
}
function clearRedisQueue(cb) {
const key = Keys.projectHistoryOps({ project_id: projectId })
delay++
if (force) {
console.log('setting redis key', key, 'to expire in', delay, 'seconds')
// use expire to allow redis to delete the key in the background
rclient.expire(key, delay, err => {
cb(err)
})
} else {
console.log(
'dry run, would set key',
key,
'to expire in',
delay,
'seconds'
)
cb()
}
}
function clearMongoEntry(cb) {
if (force) {
console.log('deleting key in mongo projectHistoryFailures', projectId)
db.projectHistoryFailures.deleteOne({ project_id: projectId }, cb)
} else {
console.log('would delete failure record for', projectId, 'from mongo')
cb()
}
}
// do the checks and deletions
async.waterfall(
[checkDeleted, checkRecoverable, clearRedisQueue, clearMongoEntry],
err => {
if (!err || err.message === 'error: project still exists') {
callback()
} else {
console.log('error:', err)
callback(err)
}
}
)
}
// find all the broken projects from the failure records
async function main() {
const results = await db.projectHistoryFailures.find({}).toArray()
processFailures(results)
}
main().catch(error => {
console.error(error)
process.exit(1)
})
function processFailures(results) {
if (argv.length === 0) {
console.log(`
Usage: node clear_deleted.js [QUEUES] [FORCE]
where
QUEUES is the number of queues to process
FORCE is the string "force" when we're ready to delete the queues. Without it, this script does a dry-run
`)
}
console.log('number of stuck projects', results.length)
// now check if the project is truly deleted in mongo
async.eachSeries(results.slice(0, limit), checkAndClear, err => {
console.log('DONE', err)
process.exit()
})
}

View File

@@ -0,0 +1,175 @@
#!/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 logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import redis from '@overleaf/redis-wrapper'
import { db, ObjectId } from '../app/js/mongodb.js'
logger.logger.level('fatal')
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 projectWithHistoryIdErrors = 0
const projectsWithHistoryId = []
function checkAndClear(project, callback) {
const projectId = project.project_id
console.log('checking project', projectId)
function checkDeleted(cb) {
db.projects.findOne(
{ _id: 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 &&
result.overleaf.history &&
!result.overleaf.history.id &&
result.overleaf.history.deleted_id
) {
console.log(
' - project is not imported from v1 and has a deleted_id - ok to clear'
)
return cb()
} else if (result && result.overleaf && result.overleaf.id) {
console.log(' - project is imported from v1')
return cb(
new Error('project is imported from v1 - will not clear it')
)
} else if (
result &&
result.overleaf &&
result.overleaf.history &&
result.overleaf.history.id
) {
console.log(' - project has a history id')
return cb(new Error('project has a history id - will not clear it'))
} else {
console.log(' - project state not recognised')
return cb(new Error('project state not recognised'))
}
}
)
}
function clearRedisQueue(cb) {
const key = Keys.projectHistoryOps({ project_id: projectId })
if (force) {
console.log('deleting redis key', key)
rclient.del(key, err => {
cb(err)
})
} else {
console.log('dry run, would deleted key', key)
cb()
}
}
function clearMongoEntry(cb) {
if (force) {
console.log('deleting key in mongo projectHistoryFailures', projectId)
db.projectHistoryFailures.deleteOne(
{ project_id: projectId },
(err, result) => {
console.log('got result from remove', err, result)
cb(err)
}
)
} else {
console.log('would delete failure record for', projectId, 'from mongo')
cb()
}
}
// do the checks and deletions
async.waterfall([checkDeleted, clearRedisQueue, clearMongoEntry], err => {
if (!err) {
if (force) {
return setTimeout(callback, 100)
} // include a 1 second delay
return callback()
} else if (err.message === 'project not found in mongo') {
projectNotFoundErrors++
projectsNotFound.push(projectId)
return callback()
} else if (err.message === 'project has a history id - will not clear it') {
projectWithHistoryIdErrors++
projectsWithHistoryId.push(projectId)
return callback()
} else if (
err.message === 'project is imported from v1 - will not clear it'
) {
projectImportedFromV1Errors++
projectsImportedFromV1.push(projectId)
return callback()
} else {
console.log('error:', err)
return callback(err)
}
})
}
// find all the broken projects from the failure records
async function main() {
const results = await db.projectHistoryFailures
.find({ error: 'Error: history store a non-success status code: 422' })
.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 not found errors',
projectNotFoundErrors,
projectsNotFound
)
console.log(
'Project with history id errors',
projectWithHistoryIdErrors,
projectsWithHistoryId
)
console.log(
'Project imported from V1 errors',
projectImportedFromV1Errors,
projectsImportedFromV1
)
process.exit()
})
}
main().catch(error => {
console.error(error)
process.exit(1)
})

View File

@@ -0,0 +1,204 @@
#!/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 logger from '@overleaf/logger'
import request from 'request'
import Settings from '@overleaf/settings'
import redis from '@overleaf/redis-wrapper'
import { db, ObjectId } from '../app/js/mongodb.js'
logger.logger.level('fatal')
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 = []
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: 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) {
console.log(' - project is not imported from v1 - ok to clear')
cb()
} else {
cb(new Error('project is imported from v1 - will not clear it'))
}
}
)
}
function clearProjectHistoryInMongo(cb) {
if (force) {
console.log('2. deleting overleaf.history.id in mongo project', projectId)
// Accessing mongo projects collection directly - BE CAREFUL!
db.projects.updateOne(
{ _id: ObjectId(projectId) },
{ $unset: { 'overleaf.history.id': '' } },
(err, result) => {
console.log(' - got result from remove', err, result)
if (err) {
return err
}
if (
result &&
(result.modifiedCount === 1 || result.modifiedCount === 0)
) {
return cb()
} else {
return cb(
new Error('error: problem trying to unset overleaf.history.id')
)
}
}
)
} else {
console.log(
'2. would delete overleaf.history.id for',
projectId,
'from mongo'
)
cb()
}
}
function clearDocUpdaterCache(cb) {
const url = Settings.apis.documentupdater.url + '/project/' + projectId
if (force) {
console.log('3. making request to clear docupdater', url)
request.delete(url, (err, response, body) => {
console.log(
' - result of request',
err,
response && response.statusCode,
body
)
cb(err)
})
} else {
console.log('3. dry run, would request DELETE on url', url)
cb()
}
}
function clearRedisQueue(cb) {
const key = Keys.projectHistoryOps({ project_id: projectId })
if (force) {
console.log('4. deleting redis queue key', key)
rclient.del(key, err => {
cb(err)
})
} else {
console.log('4. dry run, would delete redis key', key)
cb()
}
}
function clearMongoEntry(cb) {
if (force) {
console.log('5. deleting key in mongo projectHistoryFailures', projectId)
db.projectHistoryFailures.deleteOne(
{ project_id: projectId },
(err, result) => {
console.log(' - got result from remove', err, result)
cb(err)
}
)
} else {
console.log('5. would delete failure record for', projectId, 'from mongo')
cb()
}
}
// do the checks and deletions
async.waterfall(
[
checkNotV1Project,
clearProjectHistoryInMongo,
clearDocUpdaterCache,
clearRedisQueue,
clearMongoEntry,
],
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 === 'project is imported from v1 - will not clear it'
) {
projectImportedFromV1Errors++
projectsImportedFromV1.push(projectId)
return callback()
} else {
console.log('error:', err)
return callback(err)
}
}
)
}
// find all the broken projects from the failure records
async function main() {
const results = await db.projectHistoryFailures
.find({ error: 'Error: bad response from filestore: 404' })
.toArray()
console.log('number of queues without filestore 404 =', 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 not found errors',
projectNotFoundErrors,
projectsNotFound
)
console.log(
'Project imported from V1 errors',
projectImportedFromV1Errors,
projectsImportedFromV1
)
process.exit()
})
}
main().catch(error => {
console.error(error)
process.exit(1)
})

View File

@@ -0,0 +1,260 @@
#!/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 logger from '@overleaf/logger'
import request from 'request'
import Settings from '@overleaf/settings'
import redis from '@overleaf/redis-wrapper'
import { db, ObjectId } from '../app/js/mongodb.js'
logger.logger.level('fatal')
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 projectHasV2HistoryErrors = 0
const projectsV2HistoryInUse = []
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: 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'))
}
const isV1Project = result && result.overleaf && result.overleaf.id
const hasHistoryId =
result &&
result.overleaf &&
result.overleaf.history &&
result.overleaf.history.id
const hasV2HistoryInUse =
result &&
result.overleaf &&
result.overleaf.history &&
result.overleaf.history.display
const hasExistingDeletedHistory =
result &&
result.overleaf.history &&
result.overleaf.history.deleted_id
if (
hasHistoryId &&
!(isV1Project || hasV2HistoryInUse || hasExistingDeletedHistory)
) {
console.log(
' - project is not imported from v1 and v2 history is not in use - ok to clear'
)
return cb()
} else if (hasHistoryId && hasExistingDeletedHistory) {
console.log(' - project already has deleted_id')
return cb(
new Error('project already has deleted_id - will not clear it')
)
} else if (hasHistoryId && isV1Project) {
console.log(' - project is imported from v1')
return cb(
new Error('project is imported from v1 - will not clear it')
)
} else if (hasHistoryId && hasV2HistoryInUse) {
console.log(' - project is displaying v2 history')
return cb(
new Error('project is displaying v2 history - will not clear it')
)
} else {
console.log(' - project state not recognised')
return cb(new Error('project state not recognised'))
}
}
)
}
function clearProjectHistoryInMongo(cb) {
if (force) {
console.log('2. deleting overleaf.history.id in mongo project', projectId)
// Accessing mongo projects collection directly - BE CAREFUL!
db.projects.updateOne(
{ _id: ObjectId(projectId) },
{ $rename: { 'overleaf.history.id': 'overleaf.history.deleted_id' } },
(err, result) => {
console.log(' - got result from remove', err, result)
if (err) {
return err
}
if (
result &&
(result.modifiedCount === 1 || result.modifiedCount === 0)
) {
return cb()
} else {
return cb(
new Error('error: problem trying to unset overleaf.history.id')
)
}
}
)
} else {
console.log(
'2. would delete overleaf.history.id for',
projectId,
'from mongo'
)
cb()
}
}
function clearDocUpdaterCache(cb) {
const url = Settings.apis.documentupdater.url + '/project/' + projectId
if (force) {
console.log('3. making request to clear docupdater', url)
request.delete(url, (err, response, body) => {
console.log(
' - result of request',
err,
response && response.statusCode,
body
)
cb(err)
})
} else {
console.log('3. dry run, would request DELETE on url', url)
cb()
}
}
function clearRedisQueue(cb) {
const key = Keys.projectHistoryOps({ project_id: projectId })
if (force) {
console.log('4. deleting redis queue key', key)
rclient.del(key, err => {
cb(err)
})
} else {
console.log('4. dry run, would delete redis key', key)
cb()
}
}
function clearMongoEntry(cb) {
if (force) {
console.log('5. deleting key in mongo projectHistoryFailures', projectId)
db.projectHistoryFailures.deleteOne(
{ project_id: projectId },
(err, result) => {
console.log(' - got result from remove', err, result)
cb(err)
}
)
} else {
console.log('5. would delete failure record for', projectId, 'from mongo')
cb()
}
}
// do the checks and deletions
async.waterfall(
[
checkNotV1Project,
clearProjectHistoryInMongo,
clearDocUpdaterCache,
clearRedisQueue,
clearMongoEntry,
],
err => {
if (!err) {
return setTimeout(callback, 100) // include a delay
} else if (err.message === 'project not found in mongo') {
projectNotFoundErrors++
projectsNotFound.push(projectId)
return callback()
} else if (
err.message === 'project is imported from v1 - will not clear it'
) {
projectImportedFromV1Errors++
projectsImportedFromV1.push(projectId)
return callback()
} else if (
err.message === 'project is displaying v2 history - will not clear it'
) {
projectHasV2HistoryErrors++
projectsV2HistoryInUse.push(projectId)
return callback()
} else {
console.log('error:', err)
return callback(err)
}
}
)
}
// find all the broken projects from the failure records
async function main() {
const results = await db.projectHistoryFailures
.find({
error:
'OpsOutOfOrderError: project structure version out of order on incoming updates',
})
.toArray()
console.log(
'number of queues with project structure version out of order on incoming updates=',
results.length
)
// now clear the sharelatex projects
async.eachSeries(results.slice(0, limit), checkAndClear, err => {
console.log('Final error status', err)
console.log(
'Project not found errors',
projectNotFoundErrors,
projectsNotFound
)
console.log(
'Project imported from V1 errors',
projectImportedFromV1Errors,
projectsImportedFromV1
)
console.log(
'Project has V2 history in use',
projectHasV2HistoryErrors,
projectsV2HistoryInUse
)
process.exit()
})
}
main().catch(error => {
console.error(error)
process.exit(1)
})

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* This script takes a dump file, obtained via the /project/:project_id/dump
* endpoint and feeds it to the update translator to how updates are transfomed
* into changes sent to v1 history.
*/
import fs from 'fs'
import * as UpdateTranslator from '../app/js/UpdateTranslator.js'
import * as SyncManager from '../app/js/SyncManager.js'
import * as HistoryStoreManager from '../app/js/HistoryStoreManager.js'
const { filename } = parseArgs()
const { projectId, updates, chunk } = parseDumpFile(filename)
function expandResyncProjectStructure(chunk, update) {
HistoryStoreManager._mocks.getMostRecentChunk = function (
projectId,
projectHistoryId,
callback
) {
callback(null, chunk)
}
SyncManager.expandSyncUpdates(
projectId,
99999, // dummy history id
[update],
cb => cb(), // extend lock
(err, result) => {
console.log('err', err, 'result', JSON.stringify(result, null, 2))
process.exit()
}
)
}
function expandUpdates(updates) {
const wrappedUpdates = updates.map(update => ({ update }))
UpdateTranslator.convertToChanges(
projectId,
wrappedUpdates,
(err, changes) => {
if (err != null) {
error(err)
}
console.log(JSON.stringify(changes, null, 2))
}
)
}
if (updates[0].resyncProjectStructure) {
expandResyncProjectStructure(chunk, updates[0])
} else {
expandUpdates(updates)
}
function parseArgs() {
const args = process.argv.slice(2)
if (args.length !== 1) {
console.log('Usage: debug_translate_updates.js DUMP_FILE')
process.exit(1)
}
const filename = args[0]
return { filename }
}
function parseDumpFile(filename) {
const json = fs.readFileSync(filename)
const { project_id: projectId, updates, chunk } = JSON.parse(json)
return { projectId, updates, chunk }
}
function error(err) {
console.error(err)
process.exit(1)
}

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env node
// To run in dev:
//
// docker-compose run --rm project-history scripts/flush_all.js <limit>
//
// In production:
//
// docker run --rm $(docker ps -lq) scripts/flush_all.js <limit>
import _ from 'lodash'
import async from 'async'
import logger from '@overleaf/logger'
import * as RedisManager from '../app/js/RedisManager.js'
import * as UpdatesProcessor from '../app/js/UpdatesProcessor.js'
logger.logger.level('fatal')
const argv = process.argv.slice(2)
const limit = parseInt(argv[0], 10) || null
const parallelism = Math.min(parseInt(argv[1], 10) || 1, 10)
// flush all outstanding changes
RedisManager.getProjectIdsWithHistoryOps(limit, flushProjects)
function flushProjects(error, projectIds) {
if (error) {
throw error
}
let ts = new Date()
console.log(
'found projects',
JSON.stringify({ project_ids: projectIds.length, limit, ts })
)
projectIds = _.shuffle(projectIds) // randomise to avoid hitting same projects each time
if (limit > 0) {
projectIds = projectIds.slice(0, limit)
}
let succeededProjects = 0
let failedProjects = 0
let attempts = 0
async.eachLimit(
projectIds,
parallelism,
function (projectId, cb) {
attempts++
UpdatesProcessor.processUpdatesForProject(
projectId,
function (err, queueSize) {
const progress = attempts + '/' + projectIds.length
ts = new Date()
if (err) {
failedProjects++
console.log(
'failed',
progress,
JSON.stringify({
projectId,
queueSize,
ts,
err: err.toString(),
})
)
} else {
succeededProjects++
console.log(
'succeeded',
progress,
JSON.stringify({
projectId,
queueSize,
ts,
})
)
}
return cb()
}
)
},
function () {
console.log(
'total',
JSON.stringify({
succeededProjects,
failedProjects,
})
)
process.exit(0)
}
)
}

View File

@@ -0,0 +1,241 @@
#!/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: 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)
})