Files
Verso/services/history-v1/backupVerifier/ProjectVerifier.mjs
T
Andrew RumbleandCopybot 0b387c5116 Simplify error identification
GitOrigin-RevId: 6dae1ed99269deb26e15ec921ba173f85f5e6fc9
2025-03-12 09:05:48 +00:00

255 lines
6.1 KiB
JavaScript

// @ts-check
import {
BackupCorruptedError,
BackupCorruptedInvalidBlobError,
BackupCorruptedMissingBlobError,
BackupRPOViolationChunkNotBackedUpError,
BackupRPOViolationError,
verifyProjectWithErrorContext,
} from '../storage/lib/backupVerifier.mjs'
import { promiseMapSettledWithLimit } from '@overleaf/promise-utils'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import {
getSampleProjectsCursor,
selectProjectsInDateRange,
} from './ProjectSampler.mjs'
import OError from '@overleaf/o-error'
const MS_PER_30_DAYS = 30 * 24 * 60 * 60 * 1000
const METRICS = {
backup_project_verification_failed: 'backup_project_verification_failed',
backup_project_verification_succeeded:
'backup_project_verification_succeeded',
}
let WRITE_METRICS = false
/**
* Allows writing metrics to be enabled or disabled.
* @param {Boolean} writeMetrics
*/
export function setWriteMetrics(writeMetrics) {
WRITE_METRICS = writeMetrics
}
/**
*
* @param {Error|unknown} error
* @param {string} historyId
*/
function handleVerificationError(error, historyId) {
const name = error instanceof Error ? error.name : 'UnknownError'
logger.error({ historyId, error, name }, 'error verifying project backup')
WRITE_METRICS &&
metrics.inc(METRICS.backup_project_verification_failed, 1, { name })
return name
}
/**
*
* @param {Date} startDate
* @param {Date} endDate
* @param {number} interval
* @returns {Array<VerificationJobSpecification>}
*/
function splitJobs(startDate, endDate, interval) {
/** @type {Array<VerificationJobSpecification>} */
const jobs = []
while (startDate < endDate) {
const nextStart = new Date(
Math.min(startDate.getTime() + interval, endDate.getTime())
)
jobs.push({ startDate, endDate: nextStart })
startDate = nextStart
}
return jobs
}
/**
*
* @param {Array<string>} historyIds
* @return {Promise<VerificationJobStatus>}
*/
async function verifyProjects(historyIds) {
let verified = 0
const errorTypes = []
for (const historyId of historyIds) {
try {
await verifyProjectWithErrorContext(historyId)
logger.debug({ historyId }, 'verified project backup successfully')
WRITE_METRICS &&
metrics.inc(METRICS.backup_project_verification_succeeded)
verified++
} catch (error) {
errorTypes.push(handleVerificationError(error, historyId))
}
}
return {
verified,
errorTypes,
hasFailure: errorTypes.length > 0,
total: historyIds.length,
}
}
/**
*
* @param {number} nProjectsToSample
* @return {Promise<VerificationJobStatus>}
*/
export async function verifyRandomProjectSample(nProjectsToSample) {
const historyIds = await getSampleProjectsCursor(nProjectsToSample)
const errorTypes = []
let verified = 0
let total = 0
for await (const historyId of historyIds) {
total++
try {
await verifyProjectWithErrorContext(historyId)
logger.debug({ historyId }, 'verified project backup successfully')
WRITE_METRICS &&
metrics.inc(METRICS.backup_project_verification_succeeded)
verified++
} catch (error) {
errorTypes.push(handleVerificationError(error, historyId))
}
}
return {
verified,
total,
errorTypes,
hasFailure: errorTypes.length > 0,
}
}
/**
* Samples projects with history IDs between the specified dates and verifies them.
*
* @param {Date} startDate
* @param {Date} endDate
* @param {number} projectsPerRange
* @return {Promise<VerificationJobStatus>}
*/
async function verifyRange(startDate, endDate, projectsPerRange) {
logger.info({ startDate, endDate }, 'verifying range')
const historyIds = await selectProjectsInDateRange(
startDate,
endDate,
projectsPerRange
)
if (historyIds.length === 0) {
logger.debug(
{ start: startDate, end: endDate },
'No projects found in range'
)
return {
startDate,
endDate,
verified: 0,
total: 0,
hasFailure: false,
errorTypes: [],
}
}
logger.debug(
{ startDate, endDate, total: historyIds.length },
'Verifying projects in range'
)
const { errorTypes, hasFailure, verified } = await verifyProjects(historyIds)
const jobStatus = {
verified,
total: historyIds.length,
hasFailure,
startDate,
endDate,
errorTypes,
}
logger.debug(jobStatus, 'verified range')
return jobStatus
}
/**
* @typedef {Object} VerificationJobSpecification
* @property {Date} startDate
* @property {Date} endDate
*/
/**
* @typedef {import('./types.d.ts').VerificationJobStatus} VerificationJobStatus
*/
/**
* @typedef {Object} VerifyDateRangeOptions
* @property {Date} startDate
* @property {Date} endDate
* @property {number} [interval]
* @property {number} [projectsPerRange]
* @property {number} [concurrency]
*/
/**
*
* @param {VerifyDateRangeOptions} options
* @return {Promise<VerificationJobStatus>}
*/
export async function verifyProjectsInDateRange({
concurrency = 0,
projectsPerRange = 10,
startDate,
endDate,
interval = MS_PER_30_DAYS,
}) {
const jobs = splitJobs(startDate, endDate, interval)
if (jobs.length === 0) {
throw new OError('Time range could not be split into jobs', {
start: startDate,
end: endDate,
interval,
})
}
const settlements = await promiseMapSettledWithLimit(
concurrency,
jobs,
({ startDate, endDate }) =>
verifyRange(startDate, endDate, projectsPerRange)
)
return settlements.reduce(
/**
*
* @param {VerificationJobStatus} acc
* @param settlement
* @return {VerificationJobStatus}
*/
(acc, settlement) => {
if (settlement.status !== 'rejected') {
if (settlement.value.hasFailure) {
acc.hasFailure = true
}
acc.total += settlement.value.total
acc.verified += settlement.value.verified
acc.errorTypes = acc.errorTypes.concat(settlement.value.errorTypes)
} else {
logger.error({ ...settlement.reason }, 'Error processing range')
}
return acc
},
/** @type {VerificationJobStatus} */
{
startDate,
endDate,
verified: 0,
total: 0,
hasFailure: false,
errorTypes: [],
}
)
}