initial shutdown functionality
This commit is contained in:
parent
0f510bfa33
commit
8d0da5cf5c
4 changed files with 315 additions and 4 deletions
54
apps/data-service/src/graceful-shutdown-test.ts
Normal file
54
apps/data-service/src/graceful-shutdown-test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { getLogger, onShutdown, setShutdownTimeout, initiateShutdown } from '@stock-bot/logger';
|
||||||
|
|
||||||
|
const logger = getLogger('shutdown-test');
|
||||||
|
|
||||||
|
logger.info('🚀 Starting graceful shutdown test...');
|
||||||
|
|
||||||
|
// Configure shutdown
|
||||||
|
setShutdownTimeout(10000); // 10 seconds
|
||||||
|
|
||||||
|
// Register multiple shutdown handlers
|
||||||
|
onShutdown(async () => {
|
||||||
|
logger.info('🔧 Shutdown handler 1: Cleaning up resources...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
logger.info('✅ Shutdown handler 1 completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
onShutdown(async () => {
|
||||||
|
logger.info('🔧 Shutdown handler 2: Closing connections...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
logger.info('✅ Shutdown handler 2 completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
onShutdown(() => {
|
||||||
|
logger.info('🔧 Shutdown handler 3: Final cleanup (sync)');
|
||||||
|
logger.info('✅ Shutdown handler 3 completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simulate some work
|
||||||
|
let counter = 0;
|
||||||
|
const workInterval = setInterval(() => {
|
||||||
|
counter++;
|
||||||
|
logger.info(`🔄 Working... ${counter}`);
|
||||||
|
|
||||||
|
if (counter >= 5) {
|
||||||
|
logger.info('🎯 Work completed, triggering graceful shutdown in 2 seconds...');
|
||||||
|
setTimeout(async () => {
|
||||||
|
logger.info('📡 Initiating manual graceful shutdown...');
|
||||||
|
try {
|
||||||
|
await initiateShutdown();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Manual shutdown failed', { error });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
clearInterval(workInterval);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Log process info
|
||||||
|
logger.info('📊 Process info', {
|
||||||
|
pid: process.pid,
|
||||||
|
platform: process.platform,
|
||||||
|
node: process.version
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { proxyService, ProxySource } from './services/proxy.service.js';
|
import { proxyService, ProxySource } from './services/proxy.service.js';
|
||||||
import { getLogger } from '@stock-bot/logger';
|
import { getLogger, onShutdown, setShutdownTimeout } from '@stock-bot/logger';
|
||||||
|
|
||||||
// Initialize logger for the demo
|
// Initialize logger for the demo
|
||||||
const logger = getLogger('proxy-demo');
|
const logger = getLogger('proxy-demo');
|
||||||
|
|
@ -120,14 +120,60 @@ export {
|
||||||
demonstrateCustomProxySource
|
demonstrateCustomProxySource
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute the demo with enhanced logging
|
// Execute the demo with enhanced logging and graceful shutdown
|
||||||
logger.info('🚀 Starting enhanced proxy demo...');
|
logger.info('🚀 Starting enhanced proxy demo...');
|
||||||
|
|
||||||
|
// Configure graceful shutdown
|
||||||
|
setShutdownTimeout(15000); // 15 seconds shutdown timeout
|
||||||
|
|
||||||
|
// Register shutdown handlers
|
||||||
|
onShutdown(async () => {
|
||||||
|
logger.info('🔧 Cleaning up proxy service...');
|
||||||
|
try {
|
||||||
|
// Clean up proxy service resources if needed
|
||||||
|
// await proxyService.shutdown();
|
||||||
|
logger.info('✅ Proxy service cleanup completed');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Proxy service cleanup failed', { error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onShutdown(async () => {
|
||||||
|
logger.info('🔧 Performing final cleanup...');
|
||||||
|
// Any additional cleanup can go here
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate cleanup work
|
||||||
|
logger.info('✅ Final cleanup completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demonstrate graceful shutdown by setting up a timer to shutdown after demo
|
||||||
|
const shutdownTimer = setTimeout(() => {
|
||||||
|
logger.info('⏰ Demo timeout reached, initiating graceful shutdown...');
|
||||||
|
process.kill(process.pid, 'SIGTERM');
|
||||||
|
}, 20000); // Shutdown after 20 seconds
|
||||||
|
|
||||||
|
// Clear timer if demo completes early
|
||||||
|
const clearShutdownTimer = () => {
|
||||||
|
clearTimeout(shutdownTimer);
|
||||||
|
logger.info('⏰ Demo completed, canceling automatic shutdown timer');
|
||||||
|
};
|
||||||
|
|
||||||
demonstrateCustomProxySource()
|
demonstrateCustomProxySource()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info('🎉 Proxy demo completed successfully!');
|
logger.info('🎉 Proxy demo completed successfully!');
|
||||||
|
clearShutdownTimer();
|
||||||
|
|
||||||
|
// Demonstrate manual shutdown after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info('🔄 Demonstrating manual graceful shutdown in 3 seconds...');
|
||||||
|
setTimeout(() => {
|
||||||
|
process.kill(process.pid, 'SIGTERM');
|
||||||
|
}, 3000);
|
||||||
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('💥 Proxy demo failed', error);
|
logger.error('💥 Proxy demo failed', { error });
|
||||||
|
clearShutdownTimer();
|
||||||
|
setTimeout(() => process.exit(1), 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If this file is run directly, execute the demo
|
// If this file is run directly, execute the demo
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Core logger classes and functions
|
// Core logger classes and functions
|
||||||
export { Logger, createLogger, getLogger } from './logger';
|
export {
|
||||||
|
Logger,
|
||||||
|
createLogger,
|
||||||
|
getLogger,
|
||||||
|
GracefulShutdown,
|
||||||
|
onShutdown,
|
||||||
|
setShutdownTimeout,
|
||||||
|
initiateShutdown
|
||||||
|
} from './logger';
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
export type { LogLevel, LogContext, LogMetadata } from './types';
|
export type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|
@ -199,5 +203,204 @@ export function createLogger(serviceName: string): pino.Logger {
|
||||||
return getPinoLogger(serviceName);
|
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 types for convenience
|
||||||
export type { LogLevel, LogContext, LogMetadata } from './types';
|
export type { LogLevel, LogContext, LogMetadata } from './types';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue