diff --git a/apps/data-service/package.json b/apps/data-service/package.json index e11da9b..2b4eb32 100644 --- a/apps/data-service/package.json +++ b/apps/data-service/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@stock-bot/config": "*", "@stock-bot/logger": "*", + "@stock-bot/shutdown": "*", "@stock-bot/types": "*", "@stock-bot/questdb-client": "*", "@stock-bot/mongodb-client": "*", diff --git a/apps/data-service/src/graceful-shutdown-test.ts b/apps/data-service/src/graceful-shutdown-test.ts index 17799b4..11a052c 100644 --- a/apps/data-service/src/graceful-shutdown-test.ts +++ b/apps/data-service/src/graceful-shutdown-test.ts @@ -1,4 +1,5 @@ -import { getLogger, onShutdown, setShutdownTimeout, initiateShutdown } from '@stock-bot/logger'; +import { getLogger, shutdownLoggers } from '@stock-bot/logger'; +import { onShutdown, setShutdownTimeout, initiateShutdown } from '@stock-bot/shutdown'; const logger = getLogger('shutdown-test'); @@ -25,6 +26,16 @@ onShutdown(() => { logger.info('✅ Shutdown handler 3 completed'); }); +onShutdown(async () => { + logger.info('🔧 Shutdown handler 4: Logger cleanup'); + try { + await shutdownLoggers(); + console.log('✅ Logger shutdown completed'); + } catch (error) { + console.error('❌ Logger shutdown failed:', error); + } +}); + // Simulate some work let counter = 0; const workInterval = setInterval(() => { diff --git a/apps/data-service/src/proxy-demo.ts b/apps/data-service/src/proxy-demo.ts index 470ce30..1532d88 100644 --- a/apps/data-service/src/proxy-demo.ts +++ b/apps/data-service/src/proxy-demo.ts @@ -1,5 +1,6 @@ import { proxyService, ProxySource } from './services/proxy.service.js'; -import { getLogger, onShutdown, setShutdownTimeout } from '@stock-bot/logger'; +import { getLogger, shutdownLoggers } from '@stock-bot/logger'; +import { onShutdown, setShutdownTimeout } from '@stock-bot/shutdown'; // Initialize logger for the demo const logger = getLogger('proxy-demo'); @@ -138,6 +139,16 @@ onShutdown(async () => { } }); +onShutdown(async () => { + logger.info('🔧 Shutting down loggers...'); + try { + await shutdownLoggers(); + console.log('✅ Logger shutdown completed'); + } catch (error) { + console.error('❌ Logger shutdown failed:', error); + } +}); + onShutdown(async () => { logger.info('🔧 Performing final cleanup...'); // Any additional cleanup can go here diff --git a/libs/http-client/src/proxy-manager.ts b/libs/http-client/src/proxy-manager.ts index 950dd5e..ce9a225 100644 --- a/libs/http-client/src/proxy-manager.ts +++ b/libs/http-client/src/proxy-manager.ts @@ -1,4 +1,5 @@ import { ProxyAgent } from 'undici'; +import { SocksProxyAgent } from 'socks-proxy-agent'; import type { ProxyConfig } from './types.js'; export class ProxyManager { @@ -22,7 +23,23 @@ export class ProxyManager { } /** - * Create Undici proxy agent for SOCKS proxies + * Create agent for SOCKS proxies (used with undici) + */ + static createSocksAgent(proxy: ProxyConfig): SocksProxyAgent { + const { protocol, host, port, username, password } = proxy; + + let proxyUrl: string; + if (username && password) { + proxyUrl = `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`; + } else { + proxyUrl = `${protocol}://${host}:${port}`; + } + + return new SocksProxyAgent(proxyUrl); + } + + /** + * Create Undici proxy agent for HTTP/HTTPS proxies (fallback) */ static createUndiciAgent(proxy: ProxyConfig): ProxyAgent { const { protocol, host, port, username, password } = proxy; diff --git a/libs/logger/src/index.ts b/libs/logger/src/index.ts index 7850150..b044a3d 100644 --- a/libs/logger/src/index.ts +++ b/libs/logger/src/index.ts @@ -9,10 +9,7 @@ export { Logger, createLogger, getLogger, - GracefulShutdown, - onShutdown, - setShutdownTimeout, - initiateShutdown + shutdownLoggers } from './logger'; // Type definitions diff --git a/libs/logger/src/logger.ts b/libs/logger/src/logger.ts index c99a03e..24f256c 100644 --- a/libs/logger/src/logger.ts +++ b/libs/logger/src/logger.ts @@ -15,10 +15,6 @@ 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 */ @@ -204,203 +200,34 @@ export function createLogger(serviceName: string): pino.Logger { } /** - * Graceful shutdown functionality + * Gracefully shutdown all logger instances + * This should be called during application shutdown to ensure all logs are flushed */ -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 { +export async function shutdownLoggers(): Promise { + 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(); - } - }); - }); - - 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); }); - }); + } else { + resolve(); + } }); + }); - // 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); - }); - }); + try { + await Promise.allSettled(flushPromises); + console.log('All loggers flushed successfully'); + } catch (error) { + console.error('Logger flush failed:', error); + } finally { + loggerCache.clear(); } } -/** - * 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'; diff --git a/libs/shutdown/README.md b/libs/shutdown/README.md new file mode 100644 index 0000000..c995f25 --- /dev/null +++ b/libs/shutdown/README.md @@ -0,0 +1,206 @@ +# @stock-bot/shutdown + +Graceful shutdown management library for Node.js applications in the Stock Bot platform. + +## Features + +- ✅ **Automatic Signal Handling** - SIGTERM, SIGINT, SIGUSR2 (Unix), uncaught exceptions +- ✅ **Platform Support** - Windows and Unix/Linux compatible +- ✅ **Multiple Callbacks** - Register multiple cleanup functions +- ✅ **Timeout Protection** - Configurable shutdown timeout +- ✅ **Error Handling** - Failed callbacks don't block shutdown +- ✅ **Debug Logging** - Optional detailed logging +- ✅ **TypeScript Support** - Full type definitions + +## Installation + +```bash +bun add @stock-bot/shutdown +``` + +## Quick Start + +```typescript +import { onShutdown, setShutdownTimeout } from '@stock-bot/shutdown'; + +// Configure shutdown timeout (optional, default: 30 seconds) +setShutdownTimeout(15000); // 15 seconds + +// Register cleanup callbacks +onShutdown(async () => { + console.log('Closing database connections...'); + await database.close(); +}); + +onShutdown(async () => { + console.log('Stopping background jobs...'); + await jobQueue.stop(); +}); + +onShutdown(() => { + console.log('Final cleanup...'); + // Synchronous cleanup +}); + +console.log('Application started. Press Ctrl+C to test graceful shutdown.'); +``` + +## API Reference + +### Convenience Functions + +#### `onShutdown(callback)` +Register a cleanup callback. + +```typescript +onShutdown(async () => { + await cleanup(); +}); +``` + +#### `setShutdownTimeout(timeout)` +Set shutdown timeout in milliseconds. + +```typescript +setShutdownTimeout(30000); // 30 seconds +``` + +#### `initiateShutdown(signal?)` +Manually trigger shutdown. + +```typescript +const result = await initiateShutdown('manual'); +console.log(result.success); // true/false +``` + +#### `shutdownAndExit(signal?, exitCode?)` +Trigger shutdown and exit process. + +```typescript +await shutdownAndExit('manual', 0); +``` + +### Advanced Usage + +#### Manual Instance Management + +```typescript +import { GracefulShutdown } from '@stock-bot/shutdown'; + +const shutdown = new GracefulShutdown({ + timeout: 20000, + autoRegister: true, + debug: true +}); + +shutdown.onShutdown(async () => { + await cleanup(); +}); + +// Manual shutdown +const result = await shutdown.shutdown('manual'); +``` + +#### Configuration Options + +```typescript +interface ShutdownOptions { + timeout?: number; // Timeout in ms (default: 30000) + autoRegister?: boolean; // Auto-register signals (default: true) + debug?: boolean; // Enable debug logging (default: false) +} +``` + +#### Shutdown Result + +```typescript +interface ShutdownResult { + success: boolean; + callbacksExecuted: number; + callbacksFailed: number; + duration: number; + error?: string; +} +``` + +## Examples + +### Express Server + +```typescript +import express from 'express'; +import { onShutdown, setShutdownTimeout } from '@stock-bot/shutdown'; + +const app = express(); +const server = app.listen(3000); + +setShutdownTimeout(10000); + +onShutdown(async () => { + console.log('Closing HTTP server...'); + await new Promise(resolve => server.close(resolve)); +}); + +onShutdown(async () => { + console.log('Closing database...'); + await database.close(); +}); +``` + +### Worker Process + +```typescript +import { onShutdown, setShutdownDebug } from '@stock-bot/shutdown'; + +setShutdownDebug(true); // Enable debug logging + +let isRunning = true; + +onShutdown(() => { + console.log('Stopping worker...'); + isRunning = false; +}); + +// Worker loop +while (isRunning) { + await processJob(); + await new Promise(resolve => setTimeout(resolve, 1000)); +} +``` + +## Signal Handling + +The library automatically handles these signals: + +- **SIGTERM** - Termination request +- **SIGINT** - Interrupt (Ctrl+C) +- **SIGUSR2** - User-defined signal (Unix only) +- **uncaughtException** - Unhandled exceptions +- **unhandledRejection** - Unhandled promise rejections + +On Windows, only SIGTERM and SIGINT are supported due to platform limitations. + +## Best Practices + +1. **Register callbacks early** in your application startup +2. **Keep callbacks simple** and focused on cleanup +3. **Use appropriate timeouts** based on your cleanup needs +4. **Handle errors gracefully** in callbacks +5. **Test shutdown behavior** in your CI/CD pipeline + +## Testing + +```typescript +import { resetShutdown, onShutdown } from '@stock-bot/shutdown'; + +beforeEach(() => { + resetShutdown(); // Clear previous state +}); + +test('should register shutdown callback', () => { + let cleaned = false; + onShutdown(() => { cleaned = true; }); + + // Test shutdown behavior +}); +``` diff --git a/libs/shutdown/package.json b/libs/shutdown/package.json new file mode 100644 index 0000000..48326e1 --- /dev/null +++ b/libs/shutdown/package.json @@ -0,0 +1,27 @@ +{ + "name": "@stock-bot/shutdown", + "version": "1.0.0", + "description": "Graceful shutdown management for Stock Bot platform", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist" + }, + "dependencies": {}, + "devDependencies": { + "typescript": "^5.0.0", + "@types/node": "^20.0.0" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ] +} diff --git a/libs/shutdown/src/graceful-shutdown.ts b/libs/shutdown/src/graceful-shutdown.ts new file mode 100644 index 0000000..1f197b0 --- /dev/null +++ b/libs/shutdown/src/graceful-shutdown.ts @@ -0,0 +1,277 @@ +/** + * 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; + } + } +} diff --git a/libs/shutdown/src/index.ts b/libs/shutdown/src/index.ts new file mode 100644 index 0000000..30dae33 --- /dev/null +++ b/libs/shutdown/src/index.ts @@ -0,0 +1,86 @@ +/** + * @stock-bot/shutdown - Graceful shutdown management library + * + * Main exports for the shutdown library + */ + +// Core shutdown classes and types +export { GracefulShutdown } from './graceful-shutdown.js'; +export type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types.js'; + +import { GracefulShutdown } from './graceful-shutdown.js'; +import type { ShutdownResult } from './types.js'; + +// Global singleton instance +let globalInstance: GracefulShutdown | null = null; + +/** + * Get the global shutdown instance (creates one if it doesn't exist) + */ +function getGlobalInstance(): GracefulShutdown { + if (!globalInstance) { + globalInstance = GracefulShutdown.getInstance(); + } + return globalInstance; +} + +/** + * Convenience functions for global shutdown management + */ + +/** + * Register a cleanup callback that will be executed during shutdown + */ +export function onShutdown(callback: () => Promise | void): void { + getGlobalInstance().onShutdown(callback); +} + +/** + * Set the shutdown timeout in milliseconds + */ +export function setShutdownTimeout(timeout: number): void { + getGlobalInstance().setTimeout(timeout); +} + +/** + * Enable or disable debug logging + */ +export function setShutdownDebug(enabled: boolean): void { + getGlobalInstance().setDebug(enabled); +} + +/** + * Check if shutdown is currently in progress + */ +export function isShuttingDown(): boolean { + return globalInstance?.isShutdownInProgress() || false; +} + +/** + * Get the number of registered shutdown callbacks + */ +export function getShutdownCallbackCount(): number { + return globalInstance?.getCallbackCount() || 0; +} + +/** + * Manually initiate graceful shutdown + */ +export function initiateShutdown(signal?: string): Promise { + return getGlobalInstance().shutdown(signal); +} + +/** + * Manually initiate graceful shutdown and exit the process + */ +export function shutdownAndExit(signal?: string, exitCode = 0): Promise { + return getGlobalInstance().shutdownAndExit(signal, exitCode); +} + +/** + * Reset the global instance (mainly for testing) + */ +export function resetShutdown(): void { + globalInstance = null; + GracefulShutdown.reset(); +} diff --git a/libs/shutdown/src/types.ts b/libs/shutdown/src/types.ts new file mode 100644 index 0000000..7cab84b --- /dev/null +++ b/libs/shutdown/src/types.ts @@ -0,0 +1,36 @@ +/** + * Types for graceful shutdown functionality + */ + +/** + * Callback function for shutdown cleanup + */ +export type ShutdownCallback = () => Promise | void; + +/** + * Options for configuring graceful shutdown behavior + */ +export interface ShutdownOptions { + /** Timeout in milliseconds before forcing shutdown (default: 30000) */ + timeout?: number; + /** Whether to automatically register signal handlers (default: true) */ + autoRegister?: boolean; + /** Whether to enable debug logging (default: false) */ + debug?: boolean; +} + +/** + * Shutdown result information + */ +export interface ShutdownResult { + /** Whether shutdown completed successfully */ + success: boolean; + /** Number of callbacks executed */ + callbacksExecuted: number; + /** Number of callbacks that failed */ + callbacksFailed: number; + /** Time taken for shutdown in milliseconds */ + duration: number; + /** Error message if shutdown failed */ + error?: string; +} diff --git a/libs/shutdown/tsconfig.json b/libs/shutdown/tsconfig.json new file mode 100644 index 0000000..d64bb64 --- /dev/null +++ b/libs/shutdown/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.lib.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "test" + ] +}