finished shutdown lib

This commit is contained in:
Bojan Kucera 2025-06-07 10:40:08 -04:00
parent 97dcd30223
commit 13c2f10db9
4 changed files with 213 additions and 28 deletions

View file

@ -1,6 +1,6 @@
# @stock-bot/shutdown # @stock-bot/shutdown
Graceful shutdown management library for Node.js applications in the Stock Bot platform. Shutdown management library for Node.js applications in the Stock Bot platform.
## Features ## Features
@ -9,8 +9,8 @@ Graceful shutdown management library for Node.js applications in the Stock Bot p
- ✅ **Multiple Callbacks** - Register multiple cleanup functions - ✅ **Multiple Callbacks** - Register multiple cleanup functions
- ✅ **Timeout Protection** - Configurable shutdown timeout - ✅ **Timeout Protection** - Configurable shutdown timeout
- ✅ **Error Handling** - Failed callbacks don't block shutdown - ✅ **Error Handling** - Failed callbacks don't block shutdown
- ✅ **Debug Logging** - Optional detailed logging
- ✅ **TypeScript Support** - Full type definitions - ✅ **TypeScript Support** - Full type definitions
- ✅ **Zero Dependencies** - Lightweight and efficient
## Installation ## Installation
@ -85,12 +85,11 @@ await shutdownAndExit('manual', 0);
#### Manual Instance Management #### Manual Instance Management
```typescript ```typescript
import { GracefulShutdown } from '@stock-bot/shutdown'; import { Shutdown } from '@stock-bot/shutdown';
const shutdown = new GracefulShutdown({ const shutdown = new Shutdown({
timeout: 20000, timeout: 20000,
autoRegister: true, autoRegister: true
debug: true
}); });
shutdown.onShutdown(async () => { shutdown.onShutdown(async () => {
@ -107,7 +106,6 @@ const result = await shutdown.shutdown('manual');
interface ShutdownOptions { interface ShutdownOptions {
timeout?: number; // Timeout in ms (default: 30000) timeout?: number; // Timeout in ms (default: 30000)
autoRegister?: boolean; // Auto-register signals (default: true) autoRegister?: boolean; // Auto-register signals (default: true)
debug?: boolean; // Enable debug logging (default: false)
} }
``` ```
@ -150,9 +148,7 @@ onShutdown(async () => {
### Worker Process ### Worker Process
```typescript ```typescript
import { onShutdown, setShutdownDebug } from '@stock-bot/shutdown'; import { onShutdown } from '@stock-bot/shutdown';
setShutdownDebug(true); // Enable debug logging
let isRunning = true; let isRunning = true;

View file

@ -1,25 +1,25 @@
/** /**
* @stock-bot/shutdown - Graceful shutdown management library * @stock-bot/shutdown - Shutdown management library
* *
* Main exports for the shutdown library * Main exports for the shutdown library
*/ */
// Core shutdown classes and types // Core shutdown classes and types
export { GracefulShutdown } from './graceful-shutdown.js'; export { Shutdown } from './shutdown.js';
export type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types.js'; export type { ShutdownCallback, ShutdownOptions, ShutdownResult } from './types.js';
import { GracefulShutdown } from './graceful-shutdown.js'; import { Shutdown } from './shutdown.js';
import type { ShutdownResult } from './types.js'; import type { ShutdownResult } from './types.js';
// Global singleton instance // Global singleton instance
let globalInstance: GracefulShutdown | null = null; let globalInstance: Shutdown | null = null;
/** /**
* Get the global shutdown instance (creates one if it doesn't exist) * Get the global shutdown instance (creates one if it doesn't exist)
*/ */
function getGlobalInstance(): GracefulShutdown { function getGlobalInstance(): Shutdown {
if (!globalInstance) { if (!globalInstance) {
globalInstance = GracefulShutdown.getInstance(); globalInstance = Shutdown.getInstance();
} }
return globalInstance; return globalInstance;
} }
@ -42,13 +42,6 @@ export function setShutdownTimeout(timeout: number): void {
getGlobalInstance().setTimeout(timeout); getGlobalInstance().setTimeout(timeout);
} }
/**
* Enable or disable debug logging
*/
export function setShutdownDebug(enabled: boolean): void {
getGlobalInstance().setDebug(enabled);
}
/** /**
* Check if shutdown is currently in progress * Check if shutdown is currently in progress
*/ */
@ -82,5 +75,5 @@ export function shutdownAndExit(signal?: string, exitCode = 0): Promise<never> {
*/ */
export function resetShutdown(): void { export function resetShutdown(): void {
globalInstance = null; globalInstance = null;
GracefulShutdown.reset(); Shutdown.reset();
} }

View 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;
}
}

View file

@ -1,5 +1,5 @@
/** /**
* Types for graceful shutdown functionality * Types for shutdown functionality
*/ */
/** /**
@ -8,15 +8,13 @@
export type ShutdownCallback = () => Promise<void> | void; export type ShutdownCallback = () => Promise<void> | void;
/** /**
* Options for configuring graceful shutdown behavior * Options for configuring shutdown behavior
*/ */
export interface ShutdownOptions { export interface ShutdownOptions {
/** Timeout in milliseconds before forcing shutdown (default: 30000) */ /** Timeout in milliseconds before forcing shutdown (default: 30000) */
timeout?: number; timeout?: number;
/** Whether to automatically register signal handlers (default: true) */ /** Whether to automatically register signal handlers (default: true) */
autoRegister?: boolean; autoRegister?: boolean;
/** Whether to enable debug logging (default: false) */
debug?: boolean;
} }
/** /**