From 7bf1f9c4591404c7756344c8db8a2f35e395654b Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Thu, 29 Jan 2026 13:19:18 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Add=20graceful=20shutdown=20util?= =?UTF-8?q?ity=20for=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/utils/graceful-shutdown.js | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 code/websites/pokedex.online/server/utils/graceful-shutdown.js diff --git a/code/websites/pokedex.online/server/utils/graceful-shutdown.js b/code/websites/pokedex.online/server/utils/graceful-shutdown.js new file mode 100644 index 0000000..6965066 --- /dev/null +++ b/code/websites/pokedex.online/server/utils/graceful-shutdown.js @@ -0,0 +1,136 @@ +/** + * Graceful Shutdown Handler + * + * Handles server shutdown gracefully by: + * - Closing server to stop accepting new connections + * - Waiting for existing connections to finish + * - Cleaning up resources + * - Exiting with appropriate code + */ + +import logger from './logger.js'; + +/** + * Setup graceful shutdown handlers for Express server + * @param {Object} server - HTTP server instance + * @param {Object} options - Configuration options + */ +export function setupGracefulShutdown(server, options = {}) { + const { + timeout = 30000, // 30 seconds + onShutdown = null // Optional cleanup function + } = options; + + let isShuttingDown = false; + const connections = new Set(); + + // Track all connections + server.on('connection', (connection) => { + connections.add(connection); + connection.on('close', () => { + connections.delete(connection); + }); + }); + + /** + * Perform graceful shutdown + * @param {string} signal - Signal that triggered shutdown + */ + async function shutdown(signal) { + if (isShuttingDown) { + logger.warn('Shutdown already in progress, forcing exit'); + process.exit(1); + return; + } + + isShuttingDown = true; + logger.info(`Received ${signal}, starting graceful shutdown...`); + + // Stop accepting new connections + server.close(() => { + logger.info('Server closed, all requests completed'); + }); + + // Set shutdown timeout + const shutdownTimeout = setTimeout(() => { + logger.error(`Shutdown timeout (${timeout}ms), forcing exit`); + connections.forEach((conn) => conn.destroy()); + process.exit(1); + }, timeout); + + try { + // Run custom cleanup if provided + if (onShutdown) { + logger.info('Running custom shutdown cleanup...'); + await onShutdown(); + } + + // Wait for all connections to close + logger.info(`Waiting for ${connections.size} connections to close...`); + + // Close all idle connections + connections.forEach((conn) => { + if (!conn.destroyed) { + conn.end(); + } + }); + + // Give connections time to finish + await new Promise((resolve) => { + const checkInterval = setInterval(() => { + if (connections.size === 0) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); + + clearTimeout(shutdownTimeout); + logger.info('Graceful shutdown complete'); + process.exit(0); + } catch (error) { + logger.error('Error during shutdown', { error: error.message }); + clearTimeout(shutdownTimeout); + process.exit(1); + } + } + + // Register shutdown handlers for various signals + const signals = ['SIGTERM', 'SIGINT', 'SIGUSR2']; + signals.forEach((signal) => { + process.on(signal, () => shutdown(signal)); + }); + + logger.info('Graceful shutdown handlers registered'); +} + +/** + * Health check middleware that returns 503 during shutdown + */ +export function createHealthCheckMiddleware() { + let isHealthy = true; + + process.on('SIGTERM', () => { + isHealthy = false; + }); + process.on('SIGINT', () => { + isHealthy = false; + }); + + return (req, res) => { + if (!isHealthy) { + return res.status(503).json({ + status: 'shutting_down', + message: 'Server is shutting down' + }); + } + + res.json({ + status: 'ok', + uptime: process.uptime(), + timestamp: new Date().toISOString(), + memory: process.memoryUsage(), + environment: process.env.NODE_ENV + }); + }; +}