feat(01-05): implement graceful shutdown with signal handlers
- GracefulShutdown class handles SIGTERM, SIGINT, SIGHUP - 10-second timeout before force exit - Double SIGINT forces immediate exit - Shutdown sequence: stop HTTP server, stop processes, cleanup - Integrates with CoordinationServer and ProcessManager
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
import { Command } from 'commander';
|
||||
import { VERSION } from '../index.js';
|
||||
import { CoordinationServer } from '../server/index.js';
|
||||
import { GracefulShutdown } from '../server/shutdown.js';
|
||||
import { ProcessManager, ProcessRegistry } from '../process/index.js';
|
||||
import { LogManager } from '../logging/index.js';
|
||||
|
||||
@@ -42,17 +43,9 @@ async function startServer(port?: number): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Import shutdown handler (will be implemented in Task 2)
|
||||
// For now, just handle basic signals
|
||||
const handleSignal = async (signal: string) => {
|
||||
console.log(`\nReceived ${signal}, shutting down...`);
|
||||
await server.stop();
|
||||
await processManager.stopAll();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
||||
process.on('SIGINT', () => handleSignal('SIGINT'));
|
||||
// Install graceful shutdown handlers
|
||||
const shutdown = new GracefulShutdown(server, processManager, logManager);
|
||||
shutdown.install();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
125
src/server/shutdown.ts
Normal file
125
src/server/shutdown.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Graceful Shutdown Handler
|
||||
*
|
||||
* Orchestrates clean shutdown of the coordination server.
|
||||
* Handles SIGTERM, SIGINT, SIGHUP with proper cleanup sequence.
|
||||
*/
|
||||
|
||||
import type { CoordinationServer } from './index.js';
|
||||
import type { ProcessManager } from '../process/index.js';
|
||||
import type { LogManager } from '../logging/index.js';
|
||||
|
||||
/** Timeout before force exit in milliseconds */
|
||||
const SHUTDOWN_TIMEOUT_MS = 10000;
|
||||
|
||||
/**
|
||||
* Handles graceful shutdown of the coordination server.
|
||||
*
|
||||
* Shutdown sequence:
|
||||
* 1. Log shutdown initiation
|
||||
* 2. Stop accepting new connections
|
||||
* 3. Stop all managed processes
|
||||
* 4. Close all log file handles (not implemented yet in LogManager)
|
||||
* 5. Remove PID file
|
||||
* 6. Exit with code 0
|
||||
*
|
||||
* If cleanup takes longer than SHUTDOWN_TIMEOUT_MS, force exit with code 1.
|
||||
* Double SIGINT forces immediate exit.
|
||||
*/
|
||||
export class GracefulShutdown {
|
||||
private readonly server: CoordinationServer;
|
||||
private readonly processManager: ProcessManager;
|
||||
private readonly logManager: LogManager;
|
||||
private isShuttingDown = false;
|
||||
private forceExitCount = 0;
|
||||
|
||||
constructor(
|
||||
server: CoordinationServer,
|
||||
processManager: ProcessManager,
|
||||
logManager: LogManager
|
||||
) {
|
||||
this.server = server;
|
||||
this.processManager = processManager;
|
||||
this.logManager = logManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs signal handlers for graceful shutdown.
|
||||
* Call this after the server has started.
|
||||
*/
|
||||
install(): void {
|
||||
// Handle SIGTERM (kill, docker stop)
|
||||
process.on('SIGTERM', () => this.handleSignal('SIGTERM'));
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
process.on('SIGINT', () => this.handleSignal('SIGINT'));
|
||||
|
||||
// Handle SIGHUP (terminal closed)
|
||||
process.on('SIGHUP', () => this.handleSignal('SIGHUP'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a shutdown signal.
|
||||
* @param signal - The signal that triggered shutdown
|
||||
*/
|
||||
private async handleSignal(signal: string): Promise<void> {
|
||||
// Handle double SIGINT for force exit
|
||||
if (signal === 'SIGINT' && this.isShuttingDown) {
|
||||
this.forceExitCount++;
|
||||
if (this.forceExitCount >= 1) {
|
||||
console.log('\nForce exit requested. Exiting immediately.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Only run shutdown sequence once
|
||||
if (this.isShuttingDown) {
|
||||
console.log(`\nAlready shutting down... Press Ctrl+C again to force exit.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
console.log(`\nReceived ${signal}, shutting down gracefully...`);
|
||||
|
||||
// Set timeout for force exit
|
||||
const forceExitTimer = setTimeout(() => {
|
||||
console.error('Shutdown timeout exceeded. Forcing exit.');
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT_MS);
|
||||
|
||||
// Ensure timeout doesn't keep the process alive
|
||||
forceExitTimer.unref();
|
||||
|
||||
try {
|
||||
await this.shutdown();
|
||||
clearTimeout(forceExitTimer);
|
||||
console.log('Shutdown complete.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
clearTimeout(forceExitTimer);
|
||||
console.error('Error during shutdown:', (error as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the shutdown sequence.
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
// Step 1: Stop the HTTP server (stops accepting new connections)
|
||||
console.log(' Stopping HTTP server...');
|
||||
await this.server.stop();
|
||||
|
||||
// Step 2: Stop all managed processes
|
||||
console.log(' Stopping managed processes...');
|
||||
await this.processManager.stopAll();
|
||||
|
||||
// Step 3: Clean up log manager resources (future: close open file handles)
|
||||
// Currently LogManager doesn't maintain persistent handles that need closing
|
||||
// This is a placeholder for future cleanup needs
|
||||
console.log(' Cleaning up resources...');
|
||||
|
||||
// Step 4: PID file is removed by server.stop()
|
||||
// Nothing additional needed here
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user