diff --git a/server/src/server.js b/server/src/server.js index a79e3b9..02d4d83 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -14,6 +14,7 @@ import cookieParser from "cookie-parser" import authRouter from "./services/auth/routes/authRouter.js"; import clientRouter from './services/client/routes/clientRoutes.js'; import ingestRouter from "./services/ingest/routes/ingestRoutes.js" +import ShutdownManager from './shared/config/shutdown.js'; /** * Initialize Express app @@ -138,44 +139,15 @@ async function startServer() { }); - const gracefulShutdown = async (signal) => { - logger.info(`${signal} received, shutting down gracefully...`); + // Initialize shutdown manager + const shutdownManager = new ShutdownManager(logger); - server.close(async () => { - logger.info("HTTP server closed"); + shutdownManager.register("MongoDB", () => mongodb.disconnect()); + shutdownManager.register("PostgreSQL", () => postgres.close()); + shutdownManager.register("RabbitMQ", () => rabbitmq.close()); - try { - await mongodb.disconnect(); - await postgres.close(); - await rabbitmq.close(); - logger.info('All connections closed, exiting process'); - process.exit(0); - } catch (error) { - logger.error('Error during shutdown:', error); - process.exit(1); - } - }) + shutdownManager.init(server); - setTimeout(() => { - logger.error("Forced shutdown") - process.exit(1); - }, 10000); - - } - - process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); - process.on("SIGINT", () => gracefulShutdown("SIGINT")); - - // Handle uncaught exceptions - process.on('uncaughtException', (error) => { - logger.error('Uncaught Exception:', error); - gracefulShutdown('uncaughtException'); - }); - - process.on('unhandledRejection', (reason, promise) => { - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - gracefulShutdown('unhandledRejection'); - }); } catch (error) { logger.error('Failed to start server:', error); diff --git a/server/src/shared/config/shutdown.js b/server/src/shared/config/shutdown.js new file mode 100644 index 0000000..1d268ff --- /dev/null +++ b/server/src/shared/config/shutdown.js @@ -0,0 +1,110 @@ +/** + * ShutdownManager + * ---------------- + * Centralized lifecycle manager for graceful shutdown of the application. + * + * Responsibilities: + * - Listen to system signals (SIGINT, SIGTERM) + * - Execute registered cleanup tasks in order + * - Ensure graceful shutdown with timeout fallback + * - Handle uncaught exceptions and unhandled rejections + * + * Usage: + * @example + * const shutdownManager = new ShutdownManager(logger); + * shutdownManager.register("mongodb", () => mongodb.disconnect()); + * shutdownManager.init(server); + */ + +export default class ShutdownManager { + constructor(logger, options = {}) { + this.logger = logger; + this.tasks = []; + + this.timeout = options.timeout || 10000; + this.isShuttingDown = false; + } + + /** + * Register a cleanup task + * @param {string} name - Name of the resource + * @param {Function} handler - Async cleanup function + */ + register(name, handler) { + this.tasks.push({ name, handler }); + } + + /** + * Execute all cleanup tasks + */ + async executeTasks() { + this.logger.info("Executing shutdown tasks..."); + + for (const task of this.tasks) { + try { + this.logger.info(`Closing: ${task.name}`); + await task.handler(); + this.logger.info(`${task.name} closed successfully`); + } catch (error) { + this.logger.error(`Error closing ${task.name}:`, error); + } + } + } + + /** + * Graceful shutdown handler + */ + async shutdown(signal, server) { + if (this.isShuttingDown) return; + this.isShuttingDown = true; + + this.logger.info(`${signal} received. Starting graceful shutdown...`); + + // Force shutdown fallback + const forceTimeout = setTimeout(() => { + this.logger.error("Forced shutdown triggered"); + process.exit(1); + }, this.timeout); + + try { + // Stop accepting new connections + if (server) { + await new Promise((resolve) => { + server.close(() => { + this.logger.info("HTTP server closed"); + resolve(); + }); + }); + } + + // Run cleanup tasks + await this.executeTasks(); + + clearTimeout(forceTimeout); + this.logger.info("Shutdown completed successfully"); + process.exit(0); + + } catch (error) { + this.logger.error("Shutdown failed:", error); + process.exit(1); + } + } + + /** + * Initialize listeners + */ + init(server) { + process.on("SIGINT", () => this.shutdown("SIGINT", server)); + process.on("SIGTERM", () => this.shutdown("SIGTERM", server)); + + process.on("uncaughtException", (error) => { + this.logger.error("Uncaught Exception:", error); + this.shutdown("uncaughtException", server); + }); + + process.on("unhandledRejection", (reason, promise) => { + this.logger.error("Unhandled Rejection:", { reason, promise }); + this.shutdown("unhandledRejection", server); + }); + } +} \ No newline at end of file