diff --git a/apps/data-service/tsconfig.json b/apps/data-service/tsconfig.json index d505a53..7e400bd 100644 --- a/apps/data-service/tsconfig.json +++ b/apps/data-service/tsconfig.json @@ -23,6 +23,7 @@ { "path": "../../libs/http-client" }, { "path": "../../libs/types" }, { "path": "../../libs/cache" }, - { "path": "../../libs/utils" } + { "path": "../../libs/utils" }, + { "path": "../../libs/shutdown" }, ] } diff --git a/libs/shutdown/src/graceful-shutdown.ts b/libs/shutdown/src/graceful-shutdown.ts deleted file mode 100644 index 1f197b0..0000000 --- a/libs/shutdown/src/graceful-shutdown.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Graceful shutdown management for Node.js applications - * - * Features: - * - Automatic signal handling (SIGTERM, SIGINT, etc.) - * - Configurable shutdown timeout - * - Multiple cleanup callbacks with error handling - * - Platform-specific signal support (Windows/Unix) - * - Debug logging support - */ - -import type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types.js'; - -export class GracefulShutdown { - private static instance: GracefulShutdown | null = null; - private isShuttingDown = false; - private shutdownTimeout = 30000; // 30 seconds default - private callbacks: ShutdownCallback[] = []; - private debug = false; - private signalHandlersRegistered = false; - - constructor(options: ShutdownOptions = {}) { - this.shutdownTimeout = options.timeout || 30000; - this.debug = options.debug || false; - - if (options.autoRegister !== false) { - this.setupSignalHandlers(); - } - } - - /** - * Get or create singleton instance - */ - static getInstance(options?: ShutdownOptions): GracefulShutdown { - if (!GracefulShutdown.instance) { - GracefulShutdown.instance = new GracefulShutdown(options); - } - return GracefulShutdown.instance; - } - - /** - * Reset singleton instance (mainly for testing) - */ - static reset(): void { - GracefulShutdown.instance = null; - } - - /** - * Register a cleanup callback - */ - onShutdown(callback: ShutdownCallback): void { - if (this.isShuttingDown) { - this.log('warn', 'Cannot register shutdown callback during shutdown'); - return; - } - this.callbacks.push(callback); - this.log('debug', `Registered shutdown callback (total: ${this.callbacks.length})`); - } - - /** - * Set shutdown timeout in milliseconds - */ - setTimeout(timeout: number): void { - if (timeout <= 0) { - throw new Error('Shutdown timeout must be positive'); - } - this.shutdownTimeout = timeout; - this.log('debug', `Shutdown timeout set to ${timeout}ms`); - } - - /** - * Enable or disable debug logging - */ - setDebug(enabled: boolean): void { - this.debug = enabled; - } - - /** - * Get current shutdown state - */ - isShutdownInProgress(): boolean { - return this.isShuttingDown; - } - - /** - * Get number of registered callbacks - */ - getCallbackCount(): number { - return this.callbacks.length; - } - - /** - * Initiate graceful shutdown - */ - async shutdown(signal?: string): Promise { - if (this.isShuttingDown) { - this.log('warn', 'Shutdown already in progress'); - return { - success: false, - callbacksExecuted: 0, - callbacksFailed: 0, - duration: 0, - error: 'Shutdown already in progress' - }; - } - - this.isShuttingDown = true; - const startTime = Date.now(); - - this.log('info', `🛑 Graceful shutdown initiated${signal ? ` (${signal})` : ''}`); - - const shutdownPromise = this.executeCallbacks(); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout); - }); - - let result: ShutdownResult; - - try { - const callbackResult = await Promise.race([shutdownPromise, timeoutPromise]); - const duration = Date.now() - startTime; - - result = { - success: true, - callbacksExecuted: callbackResult.executed, - callbacksFailed: callbackResult.failed, - duration, - error: callbackResult.failed > 0 ? `${callbackResult.failed} callbacks failed` : undefined - }; - - this.log('info', `✅ Graceful shutdown completed (${duration}ms)`); - } catch (error) { - const duration = Date.now() - startTime; - const errorMessage = error instanceof Error ? error.message : String(error); - - result = { - success: false, - callbacksExecuted: 0, - callbacksFailed: 0, - duration, - error: errorMessage - }; - - this.log('error', `❌ Shutdown failed: ${errorMessage}`); - } - - // Don't call process.exit here - let the caller decide - return result; - } - - /** - * Initiate shutdown and exit process - */ - async shutdownAndExit(signal?: string, exitCode = 0): Promise { - const result = await this.shutdown(signal); - const finalExitCode = result.success ? exitCode : 1; - - this.log('info', `Exiting process with code ${finalExitCode}`); - process.exit(finalExitCode); - } - - /** - * Execute all registered callbacks - */ - private async executeCallbacks(): Promise<{ executed: number; failed: number }> { - if (this.callbacks.length === 0) { - this.log('info', 'No shutdown callbacks registered'); - return { executed: 0, failed: 0 }; - } - - this.log('info', `Executing ${this.callbacks.length} shutdown callbacks`); - - const results = await Promise.allSettled( - this.callbacks.map(async (callback, index) => { - try { - this.log('debug', `Executing shutdown callback ${index + 1}`); - await callback(); - this.log('debug', `✅ Callback ${index + 1} completed`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - this.log('error', `❌ Callback ${index + 1} failed: ${errorMessage}`); - throw error; - } - }) - ); - - const failed = results.filter(result => result.status === 'rejected').length; - const executed = results.length; - - if (failed > 0) { - this.log('warn', `⚠️ ${failed}/${executed} shutdown callbacks failed`); - } else { - this.log('info', `✅ All ${executed} shutdown callbacks completed successfully`); - } - - return { executed, failed }; - } - - /** - * Setup signal handlers for graceful shutdown - */ - private setupSignalHandlers(): void { - if (this.signalHandlersRegistered) { - return; - } - - // Platform-specific signals - const signals: NodeJS.Signals[] = process.platform === 'win32' - ? ['SIGINT', 'SIGTERM'] - : ['SIGTERM', 'SIGINT', 'SIGUSR2']; - - signals.forEach(signal => { - process.on(signal, () => { - this.log('info', `📡 Received ${signal} signal`); - this.shutdownAndExit(signal).catch(error => { - console.error('Emergency shutdown failed:', error); - process.exit(1); - }); - }); - }); - - // Handle uncaught exceptions - process.on('uncaughtException', (error) => { - this.log('error', `💥 Uncaught exception: ${error.message}`); - this.shutdownAndExit('uncaughtException', 1).catch(() => { - console.error('Emergency shutdown failed'); - process.exit(1); - }); - }); - - // Handle unhandled promise rejections - process.on('unhandledRejection', (reason) => { - const message = reason instanceof Error ? reason.message : String(reason); - this.log('error', `💥 Unhandled promise rejection: ${message}`); - this.shutdownAndExit('unhandledRejection', 1).catch(() => { - console.error('Emergency shutdown failed'); - process.exit(1); - }); - }); - - this.signalHandlersRegistered = true; - this.log('debug', `Signal handlers registered for: ${signals.join(', ')}`); - } - - /** - * Simple logging method - */ - private log(level: 'debug' | 'info' | 'warn' | 'error', message: string): void { - if (level === 'debug' && !this.debug) { - return; - } - - // Format timestamp to match Pino logger: [2025-06-07 14:25:48.065] - const now = new Date(); - const timestamp = now.toISOString() - .replace('T', ' ') - .replace('Z', '') - .slice(0, 23); - - const prefix = `[${timestamp}] ${level.toUpperCase()}: [graceful-shutdown]`; - - switch (level) { - case 'debug': - console.debug(`${prefix} ${message}`); - break; - case 'info': - console.log(`${prefix} ${message}`); - break; - case 'warn': - console.warn(`${prefix} ${message}`); - break; - case 'error': - console.error(`${prefix} ${message}`); - break; - } - } -}