finished shutdown lib
This commit is contained in:
parent
97dcd30223
commit
13c2f10db9
4 changed files with 213 additions and 28 deletions
198
libs/shutdown/src/shutdown.ts
Normal file
198
libs/shutdown/src/shutdown.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* 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)
|
||||
*/
|
||||
|
||||
import type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types.js';
|
||||
|
||||
export class Shutdown {
|
||||
private static instance: Shutdown | null = null;
|
||||
private isShuttingDown = false;
|
||||
private shutdownTimeout = 30000; // 30 seconds default
|
||||
private callbacks: ShutdownCallback[] = [];
|
||||
private signalHandlersRegistered = false;
|
||||
|
||||
constructor(options: ShutdownOptions = {}) {
|
||||
this.shutdownTimeout = options.timeout || 30000;
|
||||
|
||||
if (options.autoRegister !== false) {
|
||||
this.setupSignalHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create singleton instance
|
||||
*/
|
||||
static getInstance(options?: ShutdownOptions): Shutdown {
|
||||
if (!Shutdown.instance) {
|
||||
Shutdown.instance = new Shutdown(options);
|
||||
}
|
||||
return Shutdown.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset singleton instance (mainly for testing)
|
||||
*/
|
||||
static reset(): void {
|
||||
Shutdown.instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a cleanup callback
|
||||
*/
|
||||
onShutdown(callback: ShutdownCallback): void {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
this.callbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set shutdown timeout in milliseconds
|
||||
*/
|
||||
setTimeout(timeout: number): void {
|
||||
if (timeout <= 0) {
|
||||
throw new Error('Shutdown timeout must be positive');
|
||||
}
|
||||
this.shutdownTimeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return {
|
||||
success: false,
|
||||
callbacksExecuted: 0,
|
||||
callbacksFailed: 0,
|
||||
duration: 0,
|
||||
error: 'Shutdown already in progress'
|
||||
};
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
const startTime = Date.now();
|
||||
|
||||
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
|
||||
};
|
||||
} 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
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
process.exit(finalExitCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all registered callbacks
|
||||
*/
|
||||
private async executeCallbacks(): Promise<{ executed: number; failed: number }> {
|
||||
if (this.callbacks.length === 0) {
|
||||
return { executed: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
this.callbacks.map(async (callback) => {
|
||||
await callback();
|
||||
})
|
||||
);
|
||||
|
||||
const failed = results.filter(result => result.status === 'rejected').length;
|
||||
const executed = results.length;
|
||||
|
||||
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.shutdownAndExit(signal).catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', () => {
|
||||
this.shutdownAndExit('uncaughtException', 1).catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', () => {
|
||||
this.shutdownAndExit('unhandledRejection', 1).catch(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
this.signalHandlersRegistered = true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue