import { getLogger } from '@stock-bot/logger'; import { SHUTDOWN_DEFAULTS } from './constants'; import type { ShutdownCallback, ShutdownOptions } from './types'; interface CallbackEntry { callback: ShutdownCallback; priority: number; name?: string; } export class Shutdown { private static instance: Shutdown | null = null; private readonly logger = getLogger('shutdown'); private isShuttingDown = false; private shutdownTimeout: number; private callbacks: CallbackEntry[] = []; private signalHandlersRegistered = false; private shutdownPromise: Promise | null = null; private triggerSignal: string | null = null; constructor(options: ShutdownOptions = {}) { this.shutdownTimeout = options.timeout || SHUTDOWN_DEFAULTS.TIMEOUT; if (options.autoRegister !== false) { // Register signal handlers immediately in constructor this.setupSignalHandlers(); } } static getInstance(options?: ShutdownOptions): Shutdown { if (!Shutdown.instance) { Shutdown.instance = new Shutdown(options); } return Shutdown.instance; } /** * Get current shutdown status */ getStatus(): { isShuttingDown: boolean; signal: string | null; callbackCount: number } { return { isShuttingDown: this.isShuttingDown, signal: this.triggerSignal, callbackCount: this.callbacks.length }; } /** * Register a cleanup callback */ onShutdown( callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string ): void { if (this.isShuttingDown) { return; } this.callbacks.push({ callback, priority, name }); } /** * Initiate graceful shutdown */ async shutdown(signal?: string): Promise { // If already shutting down, return the existing promise if (this.shutdownPromise) { this.logger.warn('Shutdown already in progress, ignoring additional request', { originalSignal: this.triggerSignal, newSignal: signal }); return this.shutdownPromise; } if (this.isShuttingDown) { this.logger.warn('Shutdown flag already set but no promise, creating new shutdown'); } this.isShuttingDown = true; this.triggerSignal = signal || 'manual'; this.logger.info('Starting graceful shutdown', { signal: this.triggerSignal, timeout: this.shutdownTimeout, callbackCount: this.callbacks.length }); // Create and store the shutdown promise this.shutdownPromise = this.performShutdown(); try { await this.shutdownPromise; this.logger.info('Graceful shutdown completed successfully'); } catch (error) { this.logger.error('Shutdown failed', { error, signal: this.triggerSignal }); throw error; } } private async performShutdown(): Promise { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout) ); try { await Promise.race([this.executeCallbacks(), timeout]); } catch (error) { throw error; } } private async executeCallbacks(): Promise { const sorted = [...this.callbacks].sort((a, b) => a.priority - b.priority); this.logger.debug('Executing shutdown callbacks', { count: sorted.length, callbacks: sorted.map(c => ({ name: c.name || 'unnamed', priority: c.priority })) }); let completed = 0; let failed = 0; for (const { callback, name, priority } of sorted) { const callbackName = name || 'unnamed'; try { this.logger.debug(`Starting shutdown callback: ${callbackName}`, { priority }); const startTime = Date.now(); await callback(); const duration = Date.now() - startTime; completed++; this.logger.debug(`Completed shutdown callback: ${callbackName}`, { duration, priority }); } catch (error) { failed++; this.logger.error(`Shutdown callback failed: ${callbackName}`, { error, priority }); } } this.logger.info('Shutdown callbacks execution completed', { total: sorted.length, completed, failed }); } private setupSignalHandlers(): void { if (this.signalHandlersRegistered) { this.logger.debug('Signal handlers already registered'); return; } const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT']; signals.forEach(signal => { // Use process.on instead of process.once to handle multiple signals process.on(signal, async () => { this.logger.info(`Received ${signal} signal`); // If already shutting down, ignore the signal if (this.isShuttingDown || this.shutdownPromise) { this.logger.info(`Already shutting down, ignoring ${signal}`); return; } try { await this.shutdown(signal); // Add small delay to ensure logs are flushed await new Promise(resolve => setTimeout(resolve, 100)); this.logger.info('Exiting process with code 0'); process.exit(0); } catch (error) { this.logger.error('Shutdown error, exiting with code 1', { error }); // Add small delay to ensure error logs are flushed await new Promise(resolve => setTimeout(resolve, 100)); process.exit(1); } }); this.logger.debug(`Registered handler for ${signal}`); }); this.signalHandlersRegistered = true; this.logger.info('Signal handlers registered for graceful shutdown', { signals }); } }