mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Avoid duplicating a math-closing dollar sign (#11227)
GitOrigin-RevId: ef2ef77e26df59d1af3df6dc664e284d3c70102d
This commit is contained in:
21
services/project-history/scripts/add_index_for_sync_state.js
Normal file
21
services/project-history/scripts/add_index_for_sync_state.js
Normal 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) } }
|
||||
)
|
||||
@@ -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()
|
||||
136
services/project-history/scripts/clear_deleted.js
Executable file
136
services/project-history/scripts/clear_deleted.js
Executable 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()
|
||||
})
|
||||
}
|
||||
175
services/project-history/scripts/clear_deleted_history.js
Executable file
175
services/project-history/scripts/clear_deleted_history.js
Executable 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)
|
||||
})
|
||||
204
services/project-history/scripts/clear_filestore_404.js
Executable file
204
services/project-history/scripts/clear_filestore_404.js
Executable 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)
|
||||
})
|
||||
260
services/project-history/scripts/clear_project_version_out_of_order.js
Executable file
260
services/project-history/scripts/clear_project_version_out_of_order.js
Executable 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)
|
||||
})
|
||||
76
services/project-history/scripts/debug_translate_updates.js
Executable file
76
services/project-history/scripts/debug_translate_updates.js
Executable 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)
|
||||
}
|
||||
93
services/project-history/scripts/flush_all.js
Executable file
93
services/project-history/scripts/flush_all.js
Executable 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
241
services/project-history/scripts/force_resync.js
Executable file
241
services/project-history/scripts/force_resync.js
Executable 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)
|
||||
})
|
||||
Reference in New Issue
Block a user