deactivate_projects.mjs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #!/usr/bin/env node
  2. import minimist from 'minimist'
  3. import PQueue from 'p-queue'
  4. import InactiveProjectManager from '../app/src/Features/InactiveData/InactiveProjectManager.mjs'
  5. import { gracefulShutdown } from '../app/src/infrastructure/GracefulShutdown.mjs'
  6. import logger from '@overleaf/logger'
  7. // Global variables for tracking job and error counts
  8. let jobCount = 0
  9. let succeededCount = 0
  10. let skippedCount = 0
  11. let failedCount = 0
  12. let currentAgeInDays = null
  13. let currentLastOpened = null
  14. let DRY_RUN = false
  15. let gracefulShutdownInitiated = false
  16. const SCRIPT_START_TIME = Date.now()
  17. const MAX_RUNTIME_DEFAULT = null
  18. let MAX_RUNTIME = MAX_RUNTIME_DEFAULT // in milliseconds
  19. // Configure signal handling
  20. process.on('SIGINT', handleSignal)
  21. process.on('SIGTERM', handleSignal)
  22. function handleSignal() {
  23. if (gracefulShutdownInitiated) return
  24. gracefulShutdownInitiated = true
  25. logger.warn(
  26. { gracefulShutdownInitiated },
  27. 'graceful shutdown initiated, draining queue'
  28. )
  29. }
  30. // Check if max runtime has been exceeded
  31. function hasMaxRuntimeExceeded() {
  32. if (MAX_RUNTIME === null) return false
  33. const elapsedTime = Date.now() - SCRIPT_START_TIME
  34. const hasExceeded = elapsedTime >= MAX_RUNTIME
  35. if (hasExceeded && !gracefulShutdownInitiated) {
  36. gracefulShutdownInitiated = true
  37. logger.warn(
  38. { elapsedTimeMs: elapsedTime, maxRuntimeMs: MAX_RUNTIME },
  39. 'maximum runtime exceeded, initiating graceful shutdown'
  40. )
  41. }
  42. return hasExceeded
  43. }
  44. // Calculates the age in days since the provided lastOpened date.
  45. function getAgeFromLastOpened(lastOpened) {
  46. const lastOpenedDate = new Date(lastOpened)
  47. const now = new Date()
  48. return Number(((now - lastOpenedDate) / (1000 * 60 * 60 * 24)).toFixed(2))
  49. }
  50. // Deactivates a single project and handles errors
  51. async function deactivateSingleProject(project) {
  52. const { _id: projectId, lastOpened } = project
  53. jobCount++
  54. if (lastOpened) {
  55. currentLastOpened = lastOpened
  56. currentAgeInDays = getAgeFromLastOpened(lastOpened)
  57. }
  58. // Periodic progress logging
  59. if (jobCount % 1000 === 0) {
  60. logger.info(
  61. { jobCount, failedCount, currentAgeInDays },
  62. 'project deactivation in progress'
  63. )
  64. }
  65. // Debug level detail logging
  66. logger.debug(
  67. { projectId, jobCount, failedCount, dryRun: DRY_RUN },
  68. 'attempting to deactivate project'
  69. )
  70. // Dry run handling
  71. if (DRY_RUN) {
  72. logger.info({ projectId }, '[DRY RUN] would deactivate project')
  73. succeededCount++
  74. }
  75. // Actual deactivation with error handling
  76. try {
  77. await InactiveProjectManager.promises.deactivateProject(projectId)
  78. logger.debug({ projectId }, 'successfully deactivated project')
  79. succeededCount++
  80. } catch (error) {
  81. failedCount++
  82. logger.error({ projectId, err: error }, 'failed to deactivate project')
  83. }
  84. }
  85. // Centralized project processing function
  86. async function processProjects(projectCursor, concurrency) {
  87. const queue = new PQueue({ concurrency })
  88. for await (const project of projectCursor) {
  89. if (gracefulShutdownInitiated || hasMaxRuntimeExceeded()) {
  90. skippedCount++
  91. break
  92. }
  93. await queue.onEmpty()
  94. logger.debug(
  95. { queueSize: queue.size, queuePending: queue.pending },
  96. 'queue size before adding new job'
  97. )
  98. queue.add(async () => {
  99. await deactivateSingleProject(project)
  100. })
  101. }
  102. await queue.onIdle()
  103. }
  104. const usage = `
  105. Usage: scripts/deactivate_projects.mjs [options]
  106. Options:
  107. --limit <number> Max number of projects to process (default: 10)
  108. --daysOld <number> Min age in days for a project to be considered inactive (default: 7)
  109. --concurrency <number> Number of deactivations to run in parallel (default: 1)
  110. --max-time <number> Maximum runtime in seconds before graceful shutdown (default: no limit)
  111. --dry-run, -n Simulate deactivation without making changes (default: false)
  112. --help Display this usage message
  113. `
  114. async function main() {
  115. const argv = minimist(process.argv.slice(2), {
  116. string: ['limit', 'daysOld', 'concurrency', 'maxTime'],
  117. boolean: ['dryRun', 'help'],
  118. alias: {
  119. dryRun: ['dry-run', 'n'],
  120. maxTime: 'max-time',
  121. help: 'h',
  122. },
  123. default: {
  124. limit: '10',
  125. daysOld: '7',
  126. concurrency: '1',
  127. maxTime: '',
  128. dryRun: false,
  129. },
  130. })
  131. if (argv.help || process.argv.length <= 2) {
  132. console.log(usage)
  133. process.exit(0)
  134. }
  135. const limit = parseInt(argv.limit, 10)
  136. const daysOld = parseInt(argv.daysOld, 10)
  137. const concurrency = parseInt(argv.concurrency, 10)
  138. const maxRuntimeInSeconds = parseInt(argv.maxTime, 10)
  139. DRY_RUN = argv.dryRun
  140. MAX_RUNTIME = maxRuntimeInSeconds * 1000 // Convert seconds to milliseconds
  141. if (DRY_RUN) {
  142. logger.info(
  143. {},
  144. 'DRY RUN MODE ENABLED: No actual deactivations will be performed'
  145. )
  146. }
  147. logger.info(
  148. {
  149. limit,
  150. daysOld,
  151. concurrency,
  152. dryRun: DRY_RUN,
  153. maxRuntimeSeconds: maxRuntimeInSeconds || 'unlimited',
  154. },
  155. 'finding inactive projects'
  156. )
  157. try {
  158. // Find projects to deactivate
  159. const projectCursor = await InactiveProjectManager.findInactiveProjects(
  160. limit,
  161. daysOld
  162. )
  163. // Process the projects
  164. await processProjects(projectCursor, concurrency)
  165. } catch (error) {
  166. logger.error({ err: error }, 'critical error during script execution')
  167. process.exitCode = 1
  168. } finally {
  169. logger.info(
  170. {
  171. jobCount,
  172. succeededCount,
  173. failedCount,
  174. skippedCount,
  175. currentAgeInDays,
  176. currentLastOpened,
  177. elapsedTimeInSeconds: Math.floor(
  178. (Date.now() - SCRIPT_START_TIME) / 1000
  179. ),
  180. maxRuntimeInSeconds: maxRuntimeInSeconds || 'unlimited',
  181. },
  182. 'project deactivation process completed'
  183. )
  184. }
  185. }
  186. main()
  187. .then(async () => {
  188. await gracefulShutdown()
  189. })
  190. .catch(err => {
  191. logger.fatal({ err }, 'unhandled error in main execution')
  192. process.exit(1)
  193. })