verify_sampled_projects.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // @ts-check
  2. import commandLineArgs from 'command-line-args'
  3. import {
  4. setWriteMetrics,
  5. verifyProjectsCreatedInDateRange,
  6. verifyRandomProjectSample,
  7. verifyProjectsUpdatedInDateRange,
  8. } from '../../backupVerifier/ProjectVerifier.mjs'
  9. import knex from '../lib/knex.js'
  10. import { client } from '../lib/mongodb.js'
  11. import { setTimeout } from 'node:timers/promises'
  12. import logger from '@overleaf/logger'
  13. import { loadGlobalBlobs } from '../lib/blob_store/index.js'
  14. import { getDatesBeforeRPO } from '../../backupVerifier/utils.mjs'
  15. import { EventEmitter } from 'node:events'
  16. import { mongodb } from '../index.js'
  17. logger.logger.level('fatal')
  18. const usageMessage = [
  19. 'Usage: node verify_sampled_projects.mjs [--startDate <start>] [--endDate <end>] [--nProjects <n>] [--verbose] [--usage] [--writeMetrics] [--concurrency <n>] [--strategy <range|random>]',
  20. 'strategy: defaults to "range"; startDate and endDate are required for "range" strategy',
  21. ].join('\n')
  22. /**
  23. * Gracefully shutdown the process
  24. * @param code
  25. * @return {Promise<void>}
  26. */
  27. async function gracefulShutdown(code = process.exitCode) {
  28. await knex.destroy()
  29. await client.close()
  30. await setTimeout(1_000)
  31. process.exit(code)
  32. }
  33. const STATS = {
  34. verifiable: 0,
  35. unverifiable: 0,
  36. }
  37. /**
  38. * @typedef {Object} CLIOptions
  39. * @property {(signal: EventEmitter) => Promise<VerificationJobStatus>} projectVerifier
  40. * @property {boolean} verbose
  41. */
  42. /**
  43. * @typedef {import('../../backupVerifier/types.d.ts').VerificationJobStatus} VerificationJobStatus
  44. */
  45. /**
  46. *
  47. * @return {CLIOptions}
  48. */
  49. function getOptions() {
  50. const {
  51. startDate,
  52. endDate,
  53. concurrency,
  54. writeMetrics,
  55. verbose,
  56. nProjects,
  57. strategy,
  58. usage,
  59. } = commandLineArgs([
  60. { name: 'startDate', type: String },
  61. { name: 'endDate', type: String },
  62. { name: 'concurrency', type: Number, defaultValue: 1 },
  63. { name: 'verbose', type: Boolean, defaultValue: false },
  64. { name: 'nProjects', type: Number, defaultValue: 10 },
  65. { name: 'usage', type: Boolean, defaultValue: false },
  66. { name: 'writeMetrics', type: Boolean, defaultValue: false },
  67. { name: 'strategy', type: String, defaultValue: 'range' },
  68. ])
  69. if (usage) {
  70. console.log(usageMessage)
  71. process.exit(0)
  72. }
  73. if (!['range', 'random', 'recent'].includes(strategy)) {
  74. throw new Error(`Invalid strategy: ${strategy}`)
  75. }
  76. setWriteMetrics(writeMetrics)
  77. switch (strategy) {
  78. case 'random':
  79. console.log('Verifying random projects')
  80. return {
  81. verbose,
  82. projectVerifier: signal => verifyRandomProjectSample(nProjects, signal),
  83. }
  84. case 'recent':
  85. return {
  86. verbose,
  87. projectVerifier: async signal => {
  88. const { startDate, endDate } = getDatesBeforeRPO(3 * 3600)
  89. return await verifyProjectsUpdatedInDateRange(
  90. startDate,
  91. endDate,
  92. nProjects,
  93. signal
  94. )
  95. },
  96. }
  97. case 'range':
  98. default: {
  99. if (!startDate || !endDate) {
  100. throw new Error(usageMessage)
  101. }
  102. const start = Date.parse(startDate)
  103. const end = Date.parse(endDate)
  104. if (Number.isNaN(start)) {
  105. throw new Error(`Invalid start date: ${startDate}`)
  106. }
  107. if (Number.isNaN(end)) {
  108. throw new Error(`Invalid end date: ${endDate}`)
  109. }
  110. if (verbose) {
  111. console.log(`Verifying from ${startDate} to ${endDate}`)
  112. console.log(`Concurrency: ${concurrency}`)
  113. }
  114. STATS.ranges = 0
  115. return {
  116. projectVerifier: signal =>
  117. verifyProjectsCreatedInDateRange({
  118. startDate: new Date(start),
  119. endDate: new Date(end),
  120. projectsPerRange: nProjects,
  121. concurrency,
  122. signal,
  123. }),
  124. verbose,
  125. }
  126. }
  127. }
  128. }
  129. /**
  130. * @type {CLIOptions}
  131. */
  132. let options
  133. try {
  134. options = getOptions()
  135. } catch (error) {
  136. console.error(error)
  137. process.exitCode = 1
  138. await gracefulShutdown(1)
  139. process.exit() // just here so the type checker knows that the process will exit
  140. }
  141. const { projectVerifier, verbose } = options
  142. if (verbose) {
  143. logger.logger.level('debug')
  144. }
  145. /**
  146. *
  147. * @param {Array<string>} array
  148. * @param {string} matchString
  149. * @return {*}
  150. */
  151. function sumStringInstances(array, matchString) {
  152. return array.reduce((total, string) => {
  153. return string === matchString ? total + 1 : total
  154. }, 0)
  155. }
  156. /**
  157. *
  158. * @param {VerificationJobStatus} stats
  159. */
  160. function displayStats(stats) {
  161. console.log(`Verified projects: ${stats.verified}`)
  162. console.log(`Total projects sampled: ${stats.total}`)
  163. if (stats.errorTypes.length > 0) {
  164. console.log('Errors:')
  165. for (const error of new Set(stats.errorTypes)) {
  166. console.log(`${error}: ${sumStringInstances(stats.errorTypes, error)}`)
  167. }
  168. }
  169. }
  170. const shutdownEmitter = new EventEmitter()
  171. shutdownEmitter.on('shutdown', async () => {
  172. await gracefulShutdown()
  173. })
  174. process.on('SIGTERM', () => {
  175. shutdownEmitter.emit('shutdown')
  176. })
  177. process.on('SIGINT', () => {
  178. shutdownEmitter.emit('shutdown')
  179. })
  180. await loadGlobalBlobs()
  181. try {
  182. const stats = await projectVerifier(shutdownEmitter)
  183. displayStats(stats)
  184. console.log(`completed`)
  185. } catch (error) {
  186. console.error(error)
  187. console.log('completed with errors')
  188. process.exitCode = 1
  189. } finally {
  190. console.log('shutting down')
  191. await gracefulShutdown()
  192. }