fixed reference
This commit is contained in:
parent
13c2f10db9
commit
ed424b87ef
2 changed files with 2 additions and 278 deletions
|
|
@ -23,6 +23,7 @@
|
||||||
{ "path": "../../libs/http-client" },
|
{ "path": "../../libs/http-client" },
|
||||||
{ "path": "../../libs/types" },
|
{ "path": "../../libs/types" },
|
||||||
{ "path": "../../libs/cache" },
|
{ "path": "../../libs/cache" },
|
||||||
{ "path": "../../libs/utils" }
|
{ "path": "../../libs/utils" },
|
||||||
|
{ "path": "../../libs/shutdown" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ShutdownResult> {
|
|
||||||
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<never>((_, 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<never> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue