From 8d0da5cf5cb1683d5ea95221c8d4f3d0615f7762 Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Sat, 7 Jun 2025 10:13:41 -0400 Subject: [PATCH] initial shutdown functionality --- .../src/graceful-shutdown-test.ts | 54 +++++ apps/data-service/src/proxy-demo.ts | 52 ++++- libs/logger/src/index.ts | 10 +- libs/logger/src/logger.ts | 203 ++++++++++++++++++ 4 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 apps/data-service/src/graceful-shutdown-test.ts diff --git a/apps/data-service/src/graceful-shutdown-test.ts b/apps/data-service/src/graceful-shutdown-test.ts new file mode 100644 index 0000000..17799b4 --- /dev/null +++ b/apps/data-service/src/graceful-shutdown-test.ts @@ -0,0 +1,54 @@ +import { getLogger, onShutdown, setShutdownTimeout, initiateShutdown } from '@stock-bot/logger'; + +const logger = getLogger('shutdown-test'); + +logger.info('🚀 Starting graceful shutdown test...'); + +// Configure shutdown +setShutdownTimeout(10000); // 10 seconds + +// Register multiple shutdown handlers +onShutdown(async () => { + logger.info('🔧 Shutdown handler 1: Cleaning up resources...'); + await new Promise(resolve => setTimeout(resolve, 1000)); + logger.info('✅ Shutdown handler 1 completed'); +}); + +onShutdown(async () => { + logger.info('🔧 Shutdown handler 2: Closing connections...'); + await new Promise(resolve => setTimeout(resolve, 500)); + logger.info('✅ Shutdown handler 2 completed'); +}); + +onShutdown(() => { + logger.info('🔧 Shutdown handler 3: Final cleanup (sync)'); + logger.info('✅ Shutdown handler 3 completed'); +}); + +// Simulate some work +let counter = 0; +const workInterval = setInterval(() => { + counter++; + logger.info(`🔄 Working... ${counter}`); + + if (counter >= 5) { + logger.info('🎯 Work completed, triggering graceful shutdown in 2 seconds...'); + setTimeout(async () => { + logger.info('📡 Initiating manual graceful shutdown...'); + try { + await initiateShutdown(); + } catch (error) { + logger.error('Manual shutdown failed', { error }); + process.exit(1); + } + }, 2000); + clearInterval(workInterval); + } +}, 1000); + +// Log process info +logger.info('📊 Process info', { + pid: process.pid, + platform: process.platform, + node: process.version +}); diff --git a/apps/data-service/src/proxy-demo.ts b/apps/data-service/src/proxy-demo.ts index 30adb08..470ce30 100644 --- a/apps/data-service/src/proxy-demo.ts +++ b/apps/data-service/src/proxy-demo.ts @@ -1,5 +1,5 @@ import { proxyService, ProxySource } from './services/proxy.service.js'; -import { getLogger } from '@stock-bot/logger'; +import { getLogger, onShutdown, setShutdownTimeout } from '@stock-bot/logger'; // Initialize logger for the demo const logger = getLogger('proxy-demo'); @@ -120,14 +120,60 @@ export { demonstrateCustomProxySource }; -// Execute the demo with enhanced logging +// Execute the demo with enhanced logging and graceful shutdown logger.info('🚀 Starting enhanced proxy demo...'); + +// Configure graceful shutdown +setShutdownTimeout(15000); // 15 seconds shutdown timeout + +// Register shutdown handlers +onShutdown(async () => { + logger.info('🔧 Cleaning up proxy service...'); + try { + // Clean up proxy service resources if needed + // await proxyService.shutdown(); + logger.info('✅ Proxy service cleanup completed'); + } catch (error) { + logger.error('❌ Proxy service cleanup failed', { error }); + } +}); + +onShutdown(async () => { + logger.info('🔧 Performing final cleanup...'); + // Any additional cleanup can go here + await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate cleanup work + logger.info('✅ Final cleanup completed'); +}); + +// Demonstrate graceful shutdown by setting up a timer to shutdown after demo +const shutdownTimer = setTimeout(() => { + logger.info('⏰ Demo timeout reached, initiating graceful shutdown...'); + process.kill(process.pid, 'SIGTERM'); +}, 20000); // Shutdown after 20 seconds + +// Clear timer if demo completes early +const clearShutdownTimer = () => { + clearTimeout(shutdownTimer); + logger.info('⏰ Demo completed, canceling automatic shutdown timer'); +}; + demonstrateCustomProxySource() .then(() => { logger.info('🎉 Proxy demo completed successfully!'); + clearShutdownTimer(); + + // Demonstrate manual shutdown after a short delay + setTimeout(() => { + logger.info('🔄 Demonstrating manual graceful shutdown in 3 seconds...'); + setTimeout(() => { + process.kill(process.pid, 'SIGTERM'); + }, 3000); + }, 2000); }) .catch((error) => { - logger.error('💥 Proxy demo failed', error); + logger.error('💥 Proxy demo failed', { error }); + clearShutdownTimer(); + setTimeout(() => process.exit(1), 1000); }); // If this file is run directly, execute the demo diff --git a/libs/logger/src/index.ts b/libs/logger/src/index.ts index e5ab7f6..7850150 100644 --- a/libs/logger/src/index.ts +++ b/libs/logger/src/index.ts @@ -5,7 +5,15 @@ */ // Core logger classes and functions -export { Logger, createLogger, getLogger } from './logger'; +export { + Logger, + createLogger, + getLogger, + GracefulShutdown, + onShutdown, + setShutdownTimeout, + initiateShutdown +} from './logger'; // Type definitions export type { LogLevel, LogContext, LogMetadata } from './types'; diff --git a/libs/logger/src/logger.ts b/libs/logger/src/logger.ts index bb32aaa..c99a03e 100644 --- a/libs/logger/src/logger.ts +++ b/libs/logger/src/logger.ts @@ -15,6 +15,10 @@ import type { LogLevel, LogContext, LogMetadata } from './types'; // Simple cache for logger instances const loggerCache = new Map(); +// Track shutdown state +let isShuttingDown = false; +const shutdownCallbacks: Array<() => Promise | void> = []; + /** * Create transport configuration */ @@ -199,5 +203,204 @@ export function createLogger(serviceName: string): pino.Logger { return getPinoLogger(serviceName); } +/** + * Graceful shutdown functionality + */ +export class GracefulShutdown { + private static instance: GracefulShutdown; + private isShuttingDown = false; + private shutdownTimeout = 30000; // 30 seconds default + private logger: Logger; + + constructor() { + this.logger = getLogger('graceful-shutdown'); + this.setupSignalHandlers(); + } + + static getInstance(): GracefulShutdown { + if (!GracefulShutdown.instance) { + GracefulShutdown.instance = new GracefulShutdown(); + } + return GracefulShutdown.instance; + } + + /** + * Register a cleanup callback + */ + onShutdown(callback: () => Promise | void): void { + if (this.isShuttingDown) { + this.logger.warn('Attempting to register shutdown callback during shutdown'); + return; + } + shutdownCallbacks.push(callback); + } + + /** + * Set shutdown timeout (milliseconds) + */ + setTimeout(timeout: number): void { + this.shutdownTimeout = timeout; + } + + /** + * Initiate graceful shutdown + */ + async shutdown(signal?: string): Promise { + if (this.isShuttingDown) { + this.logger.warn('Shutdown already in progress'); + return; + } + + this.isShuttingDown = true; + isShuttingDown = true; + + this.logger.info(`Graceful shutdown initiated${signal ? ` (${signal})` : ''}`); + + const shutdownPromise = this.executeShutdown(); + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Shutdown timeout')), this.shutdownTimeout); + }); + + try { + await Promise.race([shutdownPromise, timeoutPromise]); + this.logger.info('Graceful shutdown completed successfully'); + } catch (error) { + this.logger.error('Shutdown error or timeout', { error }); + } finally { + // Final flush and exit + await this.flushLoggers(); + process.exit(0); + } + } + + /** + * Execute all shutdown callbacks + */ + private async executeShutdown(): Promise { + if (shutdownCallbacks.length === 0) { + this.logger.info('No shutdown callbacks registered'); + return; + } + + this.logger.info(`Executing ${shutdownCallbacks.length} shutdown callbacks`); + + const results = await Promise.allSettled( + shutdownCallbacks.map(async (callback, index) => { + try { + this.logger.debug(`Executing shutdown callback ${index + 1}`); + await callback(); + this.logger.debug(`Shutdown callback ${index + 1} completed`); + } catch (error) { + this.logger.error(`Shutdown callback ${index + 1} failed`, { error }); + throw error; + } + }) + ); + + // Log any failures + const failures = results.filter(result => result.status === 'rejected'); + if (failures.length > 0) { + this.logger.warn(`${failures.length} shutdown callbacks failed`); + } else { + this.logger.info('All shutdown callbacks completed successfully'); + } + } + + /** + * Flush all logger instances + */ + private async flushLoggers(): Promise { + this.logger.info('Flushing all loggers'); + + const flushPromises = Array.from(loggerCache.values()).map(logger => { + return new Promise((resolve) => { + if (typeof logger.flush === 'function') { + logger.flush((err) => { + if (err) { + console.error('Logger flush error:', err); + } + resolve(); + }); + } else { + resolve(); + } + }); + }); + + try { + await Promise.allSettled(flushPromises); + this.logger.info('Logger flush completed'); + } catch (error) { + console.error('Logger flush failed:', error); + } + } + /** + * Setup signal handlers for graceful shutdown + */ + private setupSignalHandlers(): void { + // On Windows, SIGUSR2 is not supported, and SIGTERM behavior is different + const signals: NodeJS.Signals[] = process.platform === 'win32' + ? ['SIGINT'] + : ['SIGTERM', 'SIGINT', 'SIGUSR2']; + + signals.forEach(signal => { + process.on(signal, () => { + this.logger.info(`Received ${signal} signal`); + this.shutdown(signal).catch(error => { + console.error('Shutdown failed:', error); + process.exit(1); + }); + }); + }); + + // On Windows, also handle SIGTERM but with different behavior + if (process.platform === 'win32') { + process.on('SIGTERM', () => { + this.logger.info('Received SIGTERM signal (Windows)'); + this.shutdown('SIGTERM').catch(error => { + console.error('Shutdown failed:', error); + process.exit(1); + }); + }); + } + + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + this.logger.error('Uncaught exception, initiating shutdown', { error }); + this.shutdown('uncaughtException').catch(() => { + console.error('Emergency shutdown failed'); + process.exit(1); + }); + }); + + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason) => { + this.logger.error('Unhandled promise rejection, initiating shutdown', { error: reason }); + this.shutdown('unhandledRejection').catch(() => { + console.error('Emergency shutdown failed'); + process.exit(1); + }); + }); + } +} + +/** + * Convenience functions for graceful shutdown + */ +export function onShutdown(callback: () => Promise | void): void { + GracefulShutdown.getInstance().onShutdown(callback); +} + +export function setShutdownTimeout(timeout: number): void { + GracefulShutdown.getInstance().setTimeout(timeout); +} + +export function initiateShutdown(): Promise { + return GracefulShutdown.getInstance().shutdown('manual'); +} + +// Auto-initialize graceful shutdown +GracefulShutdown.getInstance(); + // Export types for convenience export type { LogLevel, LogContext, LogMetadata } from './types';