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:
Lukas May
2026-01-30 13:23:58 +01:00
parent bec46aa234
commit 59b233792a
2 changed files with 129 additions and 11 deletions

View File

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