| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- // @ts-check
- import { verifyProjectWithErrorContext } from '../storage/lib/backupVerifier.mjs'
- import { promiseMapSettledWithLimit } from '@overleaf/promise-utils'
- import logger from '@overleaf/logger'
- import metrics from '@overleaf/metrics'
- import {
- getSampleProjectsCursor,
- getProjectsCreatedInDateRangeCursor,
- getProjectsUpdatedInDateRangeCursor,
- } from './ProjectSampler.mjs'
- import OError from '@overleaf/o-error'
- import { setTimeout } from 'node:timers/promises'
- const MS_PER_30_DAYS = 30 * 24 * 60 * 60 * 1000
- const failureCounter = new metrics.prom.Counter({
- name: 'backup_project_verification_failed',
- help: 'Number of projects that failed verification',
- labelNames: ['name'],
- })
- const successCounter = new metrics.prom.Counter({
- name: 'backup_project_verification_succeeded',
- help: 'Number of projects that succeeded verification',
- })
- let WRITE_METRICS = false
- /**
- * @typedef {import('node:events').EventEmitter} EventEmitter
- */
- /**
- * 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 && failureCounter.inc({ 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 {AsyncGenerator<string>} historyIdCursor
- * @param {EventEmitter} [eventEmitter]
- * @param {number} [delay] - Allows a delay between each verification
- * @return {Promise<{verified: number, total: number, errorTypes: *[], hasFailure: boolean}>}
- */
- async function verifyProjectsFromCursor(
- historyIdCursor,
- eventEmitter,
- delay = 0
- ) {
- const errorTypes = []
- let verified = 0
- let total = 0
- let receivedShutdownSignal = false
- if (eventEmitter) {
- eventEmitter.once('shutdown', () => {
- receivedShutdownSignal = true
- })
- }
- for await (const historyId of historyIdCursor) {
- if (receivedShutdownSignal) {
- break
- }
- total++
- try {
- await verifyProjectWithErrorContext(historyId)
- logger.debug({ historyId }, 'verified project backup successfully')
- WRITE_METRICS && successCounter.inc()
- verified++
- } catch (error) {
- const errorType = handleVerificationError(error, historyId)
- errorTypes.push(errorType)
- }
- if (delay > 0) {
- await setTimeout(delay)
- }
- }
- return {
- verified,
- total,
- errorTypes,
- hasFailure: errorTypes.length > 0,
- }
- }
- /**
- *
- * @param {number} nProjectsToSample
- * @param {EventEmitter} [signal]
- * @param {number} [delay]
- * @return {Promise<VerificationJobStatus>}
- */
- export async function verifyRandomProjectSample(
- nProjectsToSample,
- signal,
- delay = 0
- ) {
- const historyIds = await getSampleProjectsCursor(nProjectsToSample)
- return await verifyProjectsFromCursor(historyIds, signal, delay)
- }
- /**
- * Samples projects with history IDs between the specified dates and verifies them.
- *
- * @param {Date} startDate
- * @param {Date} endDate
- * @param {number} projectsPerRange
- * @param {EventEmitter} [signal]
- * @return {Promise<VerificationJobStatus>}
- */
- async function verifyRange(startDate, endDate, projectsPerRange, signal) {
- logger.info({ startDate, endDate }, 'verifying range')
- const results = await verifyProjectsFromCursor(
- getProjectsCreatedInDateRangeCursor(startDate, endDate, projectsPerRange),
- signal
- )
- if (results.total === 0) {
- logger.debug(
- { start: startDate, end: endDate },
- 'No projects found in range'
- )
- }
- const jobStatus = {
- ...results,
- startDate,
- endDate,
- }
- logger.debug(
- { ...jobStatus, errorTypes: Array.from(new Set(jobStatus.errorTypes)) },
- '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]
- * @property {EventEmitter} [signal]
- */
- /**
- *
- * @param {VerifyDateRangeOptions} options
- * @return {Promise<VerificationJobStatus>}
- */
- export async function verifyProjectsCreatedInDateRange({
- concurrency = 0,
- projectsPerRange = 10,
- startDate,
- endDate,
- interval = MS_PER_30_DAYS,
- signal,
- }) {
- 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, signal)
- )
- 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: [],
- }
- )
- }
- /**
- * Verifies that projects that have recently gone out of RPO have been updated.
- *
- * @param {Date} startDate
- * @param {Date} endDate
- * @param {number} nProjects
- * @param {EventEmitter} [signal]
- * @return {Promise<VerificationJobStatus>}
- */
- export async function verifyProjectsUpdatedInDateRange(
- startDate,
- endDate,
- nProjects,
- signal
- ) {
- logger.debug(
- { startDate, endDate, nProjects },
- 'Sampling projects updated in date range'
- )
- const results = await verifyProjectsFromCursor(
- getProjectsUpdatedInDateRangeCursor(startDate, endDate, nProjects),
- signal
- )
- if (results.total === 0) {
- logger.debug(
- { start: startDate, end: endDate },
- 'No projects updated recently'
- )
- }
- const jobStatus = {
- ...results,
- startDate,
- endDate,
- }
- logger.debug(
- { ...jobStatus, errorTypes: Array.from(new Set(jobStatus.errorTypes)) },
- 'Verified recently updated projects'
- )
- return jobStatus
- }
- /**
- *
- * @param {EventEmitter} signal
- * @return {void}
- */
- export function loopRandomProjects(signal) {
- let shutdown = false
- signal.on('shutdown', function () {
- shutdown = true
- })
- async function loop() {
- do {
- try {
- const result = await verifyRandomProjectSample(100, signal, 2_000)
- logger.debug({ result }, 'verified random project sample')
- } catch (error) {
- logger.error({ error }, 'error verifying random project sample')
- }
- // eslint-disable-next-line no-unmodified-loop-condition
- } while (!shutdown)
- }
- loop()
- }
|