backup-verifier-app.mjs 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. // @ts-check
  2. // Metrics must be initialized before importing anything else
  3. import '@overleaf/metrics/initialize.js'
  4. import http from 'node:http'
  5. import { fileURLToPath } from 'node:url'
  6. import { promisify } from 'node:util'
  7. import { setTimeout } from 'node:timers/promises'
  8. import express from 'express'
  9. import logger from '@overleaf/logger'
  10. import Metrics from '@overleaf/metrics'
  11. import { healthCheck } from './backupVerifier/healthCheck.mjs'
  12. import {
  13. BackupCorruptedError,
  14. verifyBlob,
  15. } from './storage/lib/backupVerifier.mjs'
  16. import { mongodb } from './storage/index.js'
  17. import { expressify } from '@overleaf/promise-utils'
  18. import { Blob } from 'overleaf-editor-core'
  19. import { loadGlobalBlobs } from './storage/lib/blob_store/index.js'
  20. import { EventEmitter } from 'node:events'
  21. import {
  22. loopRandomProjects,
  23. setWriteMetrics,
  24. } from './backupVerifier/ProjectVerifier.mjs'
  25. const app = express()
  26. logger.initialize('history-v1-backup-verifier')
  27. Metrics.open_sockets.monitor()
  28. Metrics.injectMetricsRoute(app)
  29. app.use(Metrics.http.monitor(logger))
  30. Metrics.leaked_sockets.monitor(logger)
  31. Metrics.event_loop.monitor(logger)
  32. Metrics.memory.monitor(logger)
  33. app.get(
  34. '/history/:historyId/blob/:hash/verify',
  35. expressify(async (req, res) => {
  36. const { historyId, hash } = req.params
  37. try {
  38. await verifyBlob(historyId, hash)
  39. res.sendStatus(200)
  40. } catch (err) {
  41. logger.warn({ err, historyId, hash }, 'manual verify blob failed')
  42. if (err instanceof Blob.NotFoundError) {
  43. res.status(404).send(err.message)
  44. } else if (err instanceof BackupCorruptedError) {
  45. res.status(422).send(err.message)
  46. } else {
  47. throw err
  48. }
  49. }
  50. })
  51. )
  52. app.get('/status', (req, res) => {
  53. res.send('history-v1-backup-verifier is up')
  54. })
  55. app.get(
  56. '/health_check',
  57. expressify(async (req, res) => {
  58. await healthCheck()
  59. res.sendStatus(200)
  60. })
  61. )
  62. app.use((err, req, res, next) => {
  63. req.logger.addFields({ err })
  64. req.logger.setLevel('error')
  65. next(err)
  66. })
  67. const shutdownEmitter = new EventEmitter()
  68. shutdownEmitter.once('shutdown', async code => {
  69. logger.info({ code }, 'shutting down')
  70. await mongodb.client.close()
  71. await setTimeout(100)
  72. process.exit(code)
  73. })
  74. process.on('SIGTERM', () => {
  75. shutdownEmitter.emit('shutdown', 0)
  76. })
  77. process.on('SIGINT', () => {
  78. shutdownEmitter.emit('shutdown', 0)
  79. })
  80. /**
  81. * @param {number} port
  82. * @param {boolean} enableVerificationLoop
  83. * @return {Promise<http.Server>}
  84. */
  85. export async function startApp(port, enableVerificationLoop = true) {
  86. await mongodb.client.connect()
  87. await loadGlobalBlobs()
  88. await healthCheck()
  89. const server = http.createServer(app)
  90. await promisify(server.listen.bind(server, port))()
  91. enableVerificationLoop && loopRandomProjects(shutdownEmitter)
  92. return server
  93. }
  94. setWriteMetrics(true)
  95. // Run this if we're called directly
  96. if (process.argv[1] === fileURLToPath(import.meta.url)) {
  97. const PORT = parseInt(process.env.PORT || '3102', 10)
  98. try {
  99. await startApp(PORT)
  100. } catch (error) {
  101. shutdownEmitter.emit('shutdown', 1)
  102. logger.error({ error }, 'error starting app')
  103. }
  104. }