moved shutdown handler to own library
This commit is contained in:
parent
8d0da5cf5c
commit
97dcd30223
12 changed files with 712 additions and 201 deletions
|
|
@ -13,6 +13,7 @@
|
||||||
}, "dependencies": {
|
}, "dependencies": {
|
||||||
"@stock-bot/config": "*",
|
"@stock-bot/config": "*",
|
||||||
"@stock-bot/logger": "*",
|
"@stock-bot/logger": "*",
|
||||||
|
"@stock-bot/shutdown": "*",
|
||||||
"@stock-bot/types": "*",
|
"@stock-bot/types": "*",
|
||||||
"@stock-bot/questdb-client": "*",
|
"@stock-bot/questdb-client": "*",
|
||||||
"@stock-bot/mongodb-client": "*",
|
"@stock-bot/mongodb-client": "*",
|
||||||
|
|
|
||||||
|
|
@ -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');
|
const logger = getLogger('shutdown-test');
|
||||||
|
|
||||||
|
|
@ -25,6 +26,16 @@ onShutdown(() => {
|
||||||
logger.info('✅ Shutdown handler 3 completed');
|
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
|
// Simulate some work
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const workInterval = setInterval(() => {
|
const workInterval = setInterval(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { proxyService, ProxySource } from './services/proxy.service.js';
|
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
|
// Initialize logger for the demo
|
||||||
const logger = getLogger('proxy-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 () => {
|
onShutdown(async () => {
|
||||||
logger.info('🔧 Performing final cleanup...');
|
logger.info('🔧 Performing final cleanup...');
|
||||||
// Any additional cleanup can go here
|
// Any additional cleanup can go here
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ProxyAgent } from 'undici';
|
import { ProxyAgent } from 'undici';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import type { ProxyConfig } from './types.js';
|
import type { ProxyConfig } from './types.js';
|
||||||
|
|
||||||
export class ProxyManager {
|
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 {
|
static createUndiciAgent(proxy: ProxyConfig): ProxyAgent {
|
||||||
const { protocol, host, port, username, password } = proxy;
|
const { protocol, host, port, username, password } = proxy;
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ export {
|
||||||
Logger,
|
Logger,
|
||||||
createLogger,
|
createLogger,
|
||||||
getLogger,
|
getLogger,
|
||||||
GracefulShutdown,
|
shutdownLoggers
|
||||||
onShutdown,
|
|
||||||
setShutdownTimeout,
|
|
||||||
initiateShutdown
|
|
||||||
} from './logger';
|
} from './logger';
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,6 @@ import type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
// Simple cache for logger instances
|
// Simple cache for logger instances
|
||||||
const loggerCache = new Map<string, pino.Logger>();
|
const loggerCache = new Map<string, pino.Logger>();
|
||||||
|
|
||||||
// Track shutdown state
|
|
||||||
let isShuttingDown = false;
|
|
||||||
const shutdownCallbacks: Array<() => Promise<void> | void> = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create transport configuration
|
* 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 {
|
export async function shutdownLoggers(): Promise<void> {
|
||||||
private static instance: GracefulShutdown;
|
const flushPromises = Array.from(loggerCache.values()).map(logger => {
|
||||||
private isShuttingDown = false;
|
return new Promise<void>((resolve) => {
|
||||||
private shutdownTimeout = 30000; // 30 seconds default
|
if (typeof logger.flush === 'function') {
|
||||||
private logger: Logger;
|
logger.flush((err) => {
|
||||||
|
if (err) {
|
||||||
constructor() {
|
console.error('Logger flush error:', err);
|
||||||
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();
|
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
|
try {
|
||||||
if (process.platform === 'win32') {
|
await Promise.allSettled(flushPromises);
|
||||||
process.on('SIGTERM', () => {
|
console.log('All loggers flushed successfully');
|
||||||
this.logger.info('Received SIGTERM signal (Windows)');
|
} catch (error) {
|
||||||
this.shutdown('SIGTERM').catch(error => {
|
console.error('Logger flush failed:', error);
|
||||||
console.error('Shutdown failed:', error);
|
} finally {
|
||||||
process.exit(1);
|
loggerCache.clear();
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 types for convenience
|
||||||
export type { LogLevel, LogContext, LogMetadata } from './types';
|
export type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
|
|
|
||||||
206
libs/shutdown/README.md
Normal file
206
libs/shutdown/README.md
Normal file
|
|
@ -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
|
||||||
|
});
|
||||||
|
```
|
||||||
27
libs/shutdown/package.json
Normal file
27
libs/shutdown/package.json
Normal file
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
277
libs/shutdown/src/graceful-shutdown.ts
Normal file
277
libs/shutdown/src/graceful-shutdown.ts
Normal file
|
|
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
libs/shutdown/src/index.ts
Normal file
86
libs/shutdown/src/index.ts
Normal file
|
|
@ -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): 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<ShutdownResult> {
|
||||||
|
return getGlobalInstance().shutdown(signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually initiate graceful shutdown and exit the process
|
||||||
|
*/
|
||||||
|
export function shutdownAndExit(signal?: string, exitCode = 0): Promise<never> {
|
||||||
|
return getGlobalInstance().shutdownAndExit(signal, exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the global instance (mainly for testing)
|
||||||
|
*/
|
||||||
|
export function resetShutdown(): void {
|
||||||
|
globalInstance = null;
|
||||||
|
GracefulShutdown.reset();
|
||||||
|
}
|
||||||
36
libs/shutdown/src/types.ts
Normal file
36
libs/shutdown/src/types.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Types for graceful shutdown functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for shutdown cleanup
|
||||||
|
*/
|
||||||
|
export type ShutdownCallback = () => Promise<void> | 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;
|
||||||
|
}
|
||||||
15
libs/shutdown/tsconfig.json
Normal file
15
libs/shutdown/tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.lib.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue