verify_sampled_projects.mjs 5.3 KB

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