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 { Command } from 'commander';
|
||||||
import { VERSION } from '../index.js';
|
import { VERSION } from '../index.js';
|
||||||
import { CoordinationServer } from '../server/index.js';
|
import { CoordinationServer } from '../server/index.js';
|
||||||
|
import { GracefulShutdown } from '../server/shutdown.js';
|
||||||
import { ProcessManager, ProcessRegistry } from '../process/index.js';
|
import { ProcessManager, ProcessRegistry } from '../process/index.js';
|
||||||
import { LogManager } from '../logging/index.js';
|
import { LogManager } from '../logging/index.js';
|
||||||
|
|
||||||
@@ -42,17 +43,9 @@ async function startServer(port?: number): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import shutdown handler (will be implemented in Task 2)
|
// Install graceful shutdown handlers
|
||||||
// For now, just handle basic signals
|
const shutdown = new GracefulShutdown(server, processManager, logManager);
|
||||||
const handleSignal = async (signal: string) => {
|
shutdown.install();
|
||||||
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'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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