initial shutdown functionality

This commit is contained in:
Bojan Kucera 2025-06-07 10:13:41 -04:00
parent 0f510bfa33
commit 8d0da5cf5c
4 changed files with 315 additions and 4 deletions

View file

@ -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';

View file

@ -15,6 +15,10 @@ import type { LogLevel, LogContext, LogMetadata } from './types';
// Simple cache for logger instances
const loggerCache = new Map<string, pino.Logger>();
// Track shutdown state
let isShuttingDown = false;
const shutdownCallbacks: Array<() => Promise<void> | 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): 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<void> {
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<never>((_, 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<void> {
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<void> {
this.logger.info('Flushing all loggers');
const flushPromises = Array.from(loggerCache.values()).map(logger => {
return new Promise<void>((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): void {
GracefulShutdown.getInstance().onShutdown(callback);
}
export function setShutdownTimeout(timeout: number): void {
GracefulShutdown.getInstance().setTimeout(timeout);
}
export function initiateShutdown(): Promise<void> {
return GracefulShutdown.getInstance().shutdown('manual');
}
// Auto-initialize graceful shutdown
GracefulShutdown.getInstance();
// Export types for convenience
export type { LogLevel, LogContext, LogMetadata } from './types';