GracefulShutdown.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. /*
  2. Graceful shutdown sequence:
  3. - Stop background tasks that depend on the DB, like redis queues
  4. - Stop processing new HTTP requests
  5. - Wait for background tasks that depend on the DB, like polling that was
  6. triggered by HTTP requests
  7. - Drain/Close db connections
  8. - Cleanup other background tasks, like metrics collectors
  9. - By now the node app should exit on its own.
  10. */
  11. const logger = require('@overleaf/logger')
  12. const OError = require('@overleaf/o-error')
  13. const Settings = require('@overleaf/settings')
  14. const Metrics = require('@overleaf/metrics')
  15. const sleep = require('util').promisify(setTimeout)
  16. const optionalCleanupHandlersBeforeStoppingTraffic = []
  17. const requiredCleanupHandlersBeforeDrainingConnections = []
  18. const optionalCleanupHandlersAfterDrainingConnections = []
  19. const connectionDrainer = []
  20. function addConnectionDrainer(label, handler) {
  21. connectionDrainer.push({ label, handler })
  22. }
  23. function addOptionalCleanupHandlerBeforeStoppingTraffic(label, handler) {
  24. optionalCleanupHandlersBeforeStoppingTraffic.push({ label, handler })
  25. }
  26. function addRequiredCleanupHandlerBeforeDrainingConnections(label, handler) {
  27. requiredCleanupHandlersBeforeDrainingConnections.push({ label, handler })
  28. }
  29. function addOptionalCleanupHandlerAfterDrainingConnections(label, handler) {
  30. optionalCleanupHandlersAfterDrainingConnections.push({ label, handler })
  31. }
  32. async function runHandlers(stage, handlers, logOnly) {
  33. logger.info({ stage }, 'graceful shutdown: run handlers')
  34. for (const { label, handler } of handlers) {
  35. try {
  36. await handler()
  37. } catch (e) {
  38. const err = OError.tag(e, 'handler failed', { stage, label })
  39. if (logOnly) {
  40. logger.err({ err }, 'graceful shutdown: incomplete cleanup')
  41. } else {
  42. throw err
  43. }
  44. }
  45. }
  46. }
  47. /**
  48. * @param {import('net').Server} server
  49. * @param {string} signal
  50. */
  51. async function gracefulShutdown(server, signal) {
  52. logger.warn({ signal }, 'graceful shutdown: started shutdown sequence')
  53. Settings.shuttingDown = true
  54. await runHandlers(
  55. 'optionalBeforeStoppingTraffic',
  56. optionalCleanupHandlersBeforeStoppingTraffic,
  57. true
  58. )
  59. await sleep(Settings.gracefulShutdownDelayInMs)
  60. try {
  61. await new Promise((resolve, reject) => {
  62. logger.warn({}, 'graceful shutdown: closing http server')
  63. server.close(err => {
  64. if (err) {
  65. reject(OError.tag(err, 'http.Server.close failed'))
  66. } else {
  67. resolve()
  68. }
  69. })
  70. })
  71. } catch (err) {
  72. throw OError.tag(err, 'stop traffic')
  73. }
  74. await runHandlers(
  75. 'requiredBeforeDrainingConnections',
  76. requiredCleanupHandlersBeforeDrainingConnections
  77. )
  78. try {
  79. await runHandlers('connectionDrainer', connectionDrainer)
  80. await runHandlers(
  81. 'optionalAfterDrainingConnections',
  82. optionalCleanupHandlersAfterDrainingConnections.concat([
  83. { label: 'metrics module', handler: () => Metrics.close() },
  84. {
  85. label: 'logger module',
  86. handler: () => logger.logLevelChecker?.stop(),
  87. },
  88. ])
  89. )
  90. } catch (err) {
  91. logger.err(
  92. { err },
  93. 'graceful shutdown: failed after stopping traffic, exiting'
  94. )
  95. // wait for logs to flush
  96. await sleep(1000)
  97. process.exit(1)
  98. return
  99. }
  100. logger.info({}, 'graceful shutdown: ready to exit')
  101. }
  102. function triggerGracefulShutdown(server, signal) {
  103. gracefulShutdown(server, signal).catch(err => {
  104. logger.err(
  105. { err },
  106. 'graceful shutdown: incomplete cleanup, waiting for kill'
  107. )
  108. })
  109. }
  110. class BackgroundTaskTracker {
  111. constructor(label) {
  112. // Do not leak any handles, just record the number of pending jobs.
  113. // In case we miss the cleanup of one job, the worst thing that can happen
  114. // is that we do not stop web "gracefully" before k8s kills it forcefully.
  115. this.pendingBackgroundTasks = 0
  116. addRequiredCleanupHandlerBeforeDrainingConnections(label, async () => {
  117. while (this.pendingBackgroundTasks > 0) {
  118. await sleep(100) // try again in 100ms.
  119. }
  120. })
  121. }
  122. add() {
  123. this.pendingBackgroundTasks++
  124. }
  125. done() {
  126. this.pendingBackgroundTasks--
  127. }
  128. }
  129. module.exports = {
  130. BackgroundTaskTracker,
  131. addConnectionDrainer,
  132. addOptionalCleanupHandlerBeforeStoppingTraffic,
  133. addOptionalCleanupHandlerAfterDrainingConnections,
  134. addRequiredCleanupHandlerBeforeDrainingConnections,
  135. triggerGracefulShutdown,
  136. gracefulShutdown,
  137. }