backup_scheduler.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import Queue from 'bull'
  2. import config from 'config'
  3. import commandLineArgs from 'command-line-args'
  4. import logger from '@overleaf/logger'
  5. logger.initialize('backup-queue')
  6. // Use the same redis config as backup_worker
  7. const redisOptions = config.get('redis.queue')
  8. // Create a Bull queue named 'backup'
  9. const backupQueue = new Queue('backup', {
  10. redis: redisOptions,
  11. defaultJobOptions: {
  12. removeOnComplete: true,
  13. removeOnFail: true,
  14. },
  15. })
  16. // Define command-line options
  17. const optionDefinitions = [
  18. { name: 'clean', type: Boolean },
  19. { name: 'status', type: Boolean },
  20. {
  21. name: 'add',
  22. type: String,
  23. multiple: true,
  24. description: 'Project IDs or date range in YYYY-MM-DD:YYYY-MM-DD format',
  25. },
  26. { name: 'monitor', type: Boolean },
  27. ]
  28. // Parse command line arguments
  29. const options = commandLineArgs(optionDefinitions)
  30. // Helper to validate date format
  31. function isValidDateFormat(dateStr) {
  32. return /^\d{4}-\d{2}-\d{2}$/.test(dateStr)
  33. }
  34. // Setup queue event listeners
  35. function setupMonitoring() {
  36. console.log('Starting queue monitoring. Press Ctrl+C to exit.')
  37. backupQueue.on('global:error', error => {
  38. logger.info({ error }, 'Queue error')
  39. })
  40. backupQueue.on('global:waiting', jobId => {
  41. logger.info({ jobId }, 'job is waiting')
  42. })
  43. backupQueue.on('global:active', jobId => {
  44. logger.info({ jobId }, 'job is now active')
  45. })
  46. backupQueue.on('global:stalled', jobId => {
  47. logger.info({ jobId }, 'job has stalled')
  48. })
  49. backupQueue.on('global:progress', (jobId, progress) => {
  50. logger.info({ jobId, progress }, 'job progress')
  51. })
  52. backupQueue.on('global:completed', (jobId, result) => {
  53. logger.info({ jobId, result }, 'job completed')
  54. })
  55. backupQueue.on('global:failed', (jobId, err) => {
  56. logger.info({ jobId, err }, 'job failed')
  57. })
  58. backupQueue.on('global:paused', () => {
  59. logger.info({}, 'Queue paused')
  60. })
  61. backupQueue.on('global:resumed', () => {
  62. logger.info({}, 'Queue resumed')
  63. })
  64. backupQueue.on('global:cleaned', (jobs, type) => {
  65. logger.info({ jobsCount: jobs.length, type }, 'Jobs cleaned')
  66. })
  67. backupQueue.on('global:drained', () => {
  68. logger.info({}, 'Queue drained')
  69. })
  70. backupQueue.on('global:removed', jobId => {
  71. logger.info({ jobId }, 'Job removed')
  72. })
  73. }
  74. async function addDateRangeJob(input) {
  75. const [startDate, endDate] = input.split(':')
  76. if (!isValidDateFormat(startDate) || !isValidDateFormat(endDate)) {
  77. console.error(
  78. `Invalid date format for "${input}". Use YYYY-MM-DD:YYYY-MM-DD`
  79. )
  80. return
  81. }
  82. const job = await backupQueue.add(
  83. { startDate, endDate },
  84. { jobId: `backup-${startDate}-to-${endDate}` }
  85. )
  86. console.log(
  87. `Added date range backup job: ${startDate} to ${endDate}, job ID: ${job.id}`
  88. )
  89. }
  90. // Main execution block
  91. async function run() {
  92. const optionCount = [
  93. options.clean,
  94. options.status,
  95. options.add,
  96. options.monitor,
  97. ].filter(Boolean).length
  98. if (optionCount > 1) {
  99. console.error('Only one option can be specified')
  100. process.exit(1)
  101. }
  102. if (options.clean) {
  103. const beforeCounts = await backupQueue.getJobCounts()
  104. console.log('Current queue state:', JSON.stringify(beforeCounts))
  105. console.log('Cleaning completed and failed jobs...')
  106. await backupQueue.clean(1, 'completed')
  107. await backupQueue.clean(1, 'failed')
  108. const afterCounts = await backupQueue.getJobCounts()
  109. console.log('Current queue state:', JSON.stringify(afterCounts))
  110. console.log('Queue cleaned successfully')
  111. } else if (options.status) {
  112. const counts = await backupQueue.getJobCounts()
  113. console.log('Current queue state:', JSON.stringify(counts))
  114. } else if (options.add) {
  115. const inputs = Array.isArray(options.add) ? options.add : [options.add]
  116. for (const input of inputs) {
  117. if (input.includes(':')) {
  118. // Handle date range format
  119. await addDateRangeJob(input)
  120. } else {
  121. // Handle project ID format
  122. const job = await backupQueue.add(
  123. { projectId: input },
  124. { jobId: input }
  125. )
  126. console.log(`Added job for project: ${input}, job ID: ${job.id}`)
  127. }
  128. }
  129. } else if (options.monitor) {
  130. setupMonitoring()
  131. } else {
  132. console.log('Usage:')
  133. console.log(' --clean Clean up completed and failed jobs')
  134. console.log(' --status Show current job counts')
  135. console.log(' --add [projectId] Add a job for the specified projectId')
  136. console.log(
  137. ' --add [YYYY-MM-DD:YYYY-MM-DD] Add a job for the specified date range'
  138. )
  139. console.log(' --monitor Monitor queue events')
  140. }
  141. }
  142. // Run and handle errors
  143. run()
  144. .catch(err => {
  145. console.error('Error:', err)
  146. process.exit(1)
  147. })
  148. .then(result => {
  149. // Only exit if not in monitor mode
  150. if (!options.monitor) {
  151. process.exit(0)
  152. }
  153. })