| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- // @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: [],
- }
- )
- }
|