ProjectVerifier.mjs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. // @ts-check
  2. import {
  3. BackupCorruptedError,
  4. BackupCorruptedInvalidBlobError,
  5. BackupCorruptedMissingBlobError,
  6. BackupRPOViolationChunkNotBackedUpError,
  7. BackupRPOViolationError,
  8. verifyProjectWithErrorContext,
  9. } from '../storage/lib/backupVerifier.mjs'
  10. import { promiseMapSettledWithLimit } from '@overleaf/promise-utils'
  11. import logger from '@overleaf/logger'
  12. import metrics from '@overleaf/metrics'
  13. import {
  14. getSampleProjectsCursor,
  15. selectProjectsInDateRange,
  16. } from './ProjectSampler.mjs'
  17. import OError from '@overleaf/o-error'
  18. const MS_PER_30_DAYS = 30 * 24 * 60 * 60 * 1000
  19. const METRICS = {
  20. backup_project_verification_failed: 'backup_project_verification_failed',
  21. backup_project_verification_succeeded:
  22. 'backup_project_verification_succeeded',
  23. }
  24. let WRITE_METRICS = false
  25. /**
  26. * Allows writing metrics to be enabled or disabled.
  27. * @param {Boolean} writeMetrics
  28. */
  29. export function setWriteMetrics(writeMetrics) {
  30. WRITE_METRICS = writeMetrics
  31. }
  32. /**
  33. *
  34. * @param {Error|unknown} error
  35. * @param {string} historyId
  36. */
  37. function handleVerificationError(error, historyId) {
  38. const name = error instanceof Error ? error.name : 'UnknownError'
  39. logger.error({ historyId, error, name }, 'error verifying project backup')
  40. WRITE_METRICS &&
  41. metrics.inc(METRICS.backup_project_verification_failed, 1, { name })
  42. return name
  43. }
  44. /**
  45. *
  46. * @param {Date} startDate
  47. * @param {Date} endDate
  48. * @param {number} interval
  49. * @returns {Array<VerificationJobSpecification>}
  50. */
  51. function splitJobs(startDate, endDate, interval) {
  52. /** @type {Array<VerificationJobSpecification>} */
  53. const jobs = []
  54. while (startDate < endDate) {
  55. const nextStart = new Date(
  56. Math.min(startDate.getTime() + interval, endDate.getTime())
  57. )
  58. jobs.push({ startDate, endDate: nextStart })
  59. startDate = nextStart
  60. }
  61. return jobs
  62. }
  63. /**
  64. *
  65. * @param {Array<string>} historyIds
  66. * @return {Promise<VerificationJobStatus>}
  67. */
  68. async function verifyProjects(historyIds) {
  69. let verified = 0
  70. const errorTypes = []
  71. for (const historyId of historyIds) {
  72. try {
  73. await verifyProjectWithErrorContext(historyId)
  74. logger.debug({ historyId }, 'verified project backup successfully')
  75. WRITE_METRICS &&
  76. metrics.inc(METRICS.backup_project_verification_succeeded)
  77. verified++
  78. } catch (error) {
  79. errorTypes.push(handleVerificationError(error, historyId))
  80. }
  81. }
  82. return {
  83. verified,
  84. errorTypes,
  85. hasFailure: errorTypes.length > 0,
  86. total: historyIds.length,
  87. }
  88. }
  89. /**
  90. *
  91. * @param {number} nProjectsToSample
  92. * @return {Promise<VerificationJobStatus>}
  93. */
  94. export async function verifyRandomProjectSample(nProjectsToSample) {
  95. const historyIds = await getSampleProjectsCursor(nProjectsToSample)
  96. const errorTypes = []
  97. let verified = 0
  98. let total = 0
  99. for await (const historyId of historyIds) {
  100. total++
  101. try {
  102. await verifyProjectWithErrorContext(historyId)
  103. logger.debug({ historyId }, 'verified project backup successfully')
  104. WRITE_METRICS &&
  105. metrics.inc(METRICS.backup_project_verification_succeeded)
  106. verified++
  107. } catch (error) {
  108. errorTypes.push(handleVerificationError(error, historyId))
  109. }
  110. }
  111. return {
  112. verified,
  113. total,
  114. errorTypes,
  115. hasFailure: errorTypes.length > 0,
  116. }
  117. }
  118. /**
  119. * Samples projects with history IDs between the specified dates and verifies them.
  120. *
  121. * @param {Date} startDate
  122. * @param {Date} endDate
  123. * @param {number} projectsPerRange
  124. * @return {Promise<VerificationJobStatus>}
  125. */
  126. async function verifyRange(startDate, endDate, projectsPerRange) {
  127. logger.info({ startDate, endDate }, 'verifying range')
  128. const historyIds = await selectProjectsInDateRange(
  129. startDate,
  130. endDate,
  131. projectsPerRange
  132. )
  133. if (historyIds.length === 0) {
  134. logger.debug(
  135. { start: startDate, end: endDate },
  136. 'No projects found in range'
  137. )
  138. return {
  139. startDate,
  140. endDate,
  141. verified: 0,
  142. total: 0,
  143. hasFailure: false,
  144. errorTypes: [],
  145. }
  146. }
  147. logger.debug(
  148. { startDate, endDate, total: historyIds.length },
  149. 'Verifying projects in range'
  150. )
  151. const { errorTypes, hasFailure, verified } = await verifyProjects(historyIds)
  152. const jobStatus = {
  153. verified,
  154. total: historyIds.length,
  155. hasFailure,
  156. startDate,
  157. endDate,
  158. errorTypes,
  159. }
  160. logger.debug(jobStatus, 'verified range')
  161. return jobStatus
  162. }
  163. /**
  164. * @typedef {Object} VerificationJobSpecification
  165. * @property {Date} startDate
  166. * @property {Date} endDate
  167. */
  168. /**
  169. * @typedef {import('./types.d.ts').VerificationJobStatus} VerificationJobStatus
  170. */
  171. /**
  172. * @typedef {Object} VerifyDateRangeOptions
  173. * @property {Date} startDate
  174. * @property {Date} endDate
  175. * @property {number} [interval]
  176. * @property {number} [projectsPerRange]
  177. * @property {number} [concurrency]
  178. */
  179. /**
  180. *
  181. * @param {VerifyDateRangeOptions} options
  182. * @return {Promise<VerificationJobStatus>}
  183. */
  184. export async function verifyProjectsInDateRange({
  185. concurrency = 0,
  186. projectsPerRange = 10,
  187. startDate,
  188. endDate,
  189. interval = MS_PER_30_DAYS,
  190. }) {
  191. const jobs = splitJobs(startDate, endDate, interval)
  192. if (jobs.length === 0) {
  193. throw new OError('Time range could not be split into jobs', {
  194. start: startDate,
  195. end: endDate,
  196. interval,
  197. })
  198. }
  199. const settlements = await promiseMapSettledWithLimit(
  200. concurrency,
  201. jobs,
  202. ({ startDate, endDate }) =>
  203. verifyRange(startDate, endDate, projectsPerRange)
  204. )
  205. return settlements.reduce(
  206. /**
  207. *
  208. * @param {VerificationJobStatus} acc
  209. * @param settlement
  210. * @return {VerificationJobStatus}
  211. */
  212. (acc, settlement) => {
  213. if (settlement.status !== 'rejected') {
  214. if (settlement.value.hasFailure) {
  215. acc.hasFailure = true
  216. }
  217. acc.total += settlement.value.total
  218. acc.verified += settlement.value.verified
  219. acc.errorTypes = acc.errorTypes.concat(settlement.value.errorTypes)
  220. } else {
  221. logger.error({ ...settlement.reason }, 'Error processing range')
  222. }
  223. return acc
  224. },
  225. /** @type {VerificationJobStatus} */
  226. {
  227. startDate,
  228. endDate,
  229. verified: 0,
  230. total: 0,
  231. hasFailure: false,
  232. errorTypes: [],
  233. }
  234. )
  235. }