Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | 1x 6x 6x 6x 6x 6x 6x 3x 3x 3x 3x 3x 3x 3x 3x 3x | /**
* 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';
import type { PreviewManager } from '../preview/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 readonly previewManager?: PreviewManager;
private isShuttingDown = false;
private forceExitCount = 0;
constructor(
server: CoordinationServer,
processManager: ProcessManager,
logManager: LogManager,
previewManager?: PreviewManager,
) {
this.server = server;
this.processManager = processManager;
this.logManager = logManager;
this.previewManager = previewManager;
}
/**
* 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: Stop all preview deployments
Iif (this.previewManager) {
console.log(' Stopping preview deployments...');
await this.previewManager.stopAll();
}
// Step 4: 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
}
}
|