UNPKG

@claude-vector/core

Version:

Core vector search engine for code intelligence

710 lines (601 loc) 20.4 kB
/** * GracefulShutdownManager - プロセス終了時の安全な終了処理管理 * * 機能: * - プロセスシグナル監視(SIGINT, SIGTERM, uncaughtException等) * - 段階的シャットダウンプロセス管理 * - 現在の処理状態の安全な保存 * - リソースクリーンアップ(メモリ、ファイル、ネットワーク) * - 次回起動時の復旧情報保存 * - シャットダウン進捗の可視化 */ import fs from 'fs/promises'; import fsSync from 'fs'; import { EventEmitter } from 'events'; export class GracefulShutdownManager extends EventEmitter { constructor(options = {}) { super(); this.options = { // シャットダウン設定 shutdownTimeout: options.shutdownTimeout ?? 30000, // 30秒 forceKillTimeout: options.forceKillTimeout ?? 5000, // 5秒後に強制終了 gracefulTimeout: options.gracefulTimeout ?? 10000, // 10秒のグレースフルタイムアウト // 状態保存設定 enableStatePersistence: options.enableStatePersistence ?? true, stateFile: options.stateFile ?? './.claude-shutdown-state.json', backupStateFile: options.backupStateFile ?? './.claude-shutdown-state.backup.json', // ログ設定 verboseLogging: options.verboseLogging ?? false, logFile: options.logFile ?? './.claude-shutdown.log', // 復旧設定 enableRecovery: options.enableRecovery ?? true, maxRecoveryAttempts: options.maxRecoveryAttempts ?? 3, ...options }; // シャットダウン状態 this.shutdownState = { isShuttingDown: false, shutdownReason: null, shutdownStartTime: null, currentPhase: null, completedPhases: [], errors: [] }; // 登録されたクリーンアップハンドラー this.cleanupHandlers = new Map(); this.criticalHandlers = new Map(); // 重要なクリーンアップ(例:インデックス保存) // 統計情報 this.stats = { totalShutdowns: 0, gracefulShutdowns: 0, forcedShutdowns: 0, averageShutdownTime: 0, lastShutdownTime: null, recoveryAttempts: 0, successfulRecoveries: 0 }; // 処理中のタスク状態 this.activeOperations = new Map(); // 初期化 this.initialize(); } /** * 初期化 */ initialize() { // プロセスシグナルハンドラーの設定 this.setupSignalHandlers(); // 既存の状態ファイルがあるかチェック if (this.options.enableRecovery) { this.checkForRecoveryState(); } this.emit('initialized', this.options); } /** * プロセスシグナルハンドラーの設定 */ setupSignalHandlers() { // SIGINT (Ctrl+C) process.on('SIGINT', () => { this.initiateShutdown('SIGINT', 'User interrupted (Ctrl+C)'); }); // SIGTERM (プロセス終了要求) process.on('SIGTERM', () => { this.initiateShutdown('SIGTERM', 'Process termination requested'); }); // Uncaught Exception process.on('uncaughtException', (error) => { this.log('error', `Uncaught exception: ${error.message}`, error); this.initiateShutdown('UNCAUGHT_EXCEPTION', `Uncaught exception: ${error.message}`, { error, critical: true }); }); // Unhandled Promise Rejection process.on('unhandledRejection', (reason, promise) => { this.log('error', `Unhandled promise rejection: ${reason}`, { reason, promise }); this.initiateShutdown('UNHANDLED_REJECTION', `Unhandled promise rejection: ${reason}`, { reason, promise, critical: true }); }); // Windows対応のための追加シグナル if (process.platform === 'win32') { process.on('SIGBREAK', () => { this.initiateShutdown('SIGBREAK', 'Windows break signal'); }); } } /** * クリーンアップハンドラーの登録 */ registerCleanupHandler(name, handler, priority = 'normal') { const handlerInfo = { name, handler, priority, registered: Date.now() }; if (priority === 'critical') { this.criticalHandlers.set(name, handlerInfo); } else { this.cleanupHandlers.set(name, handlerInfo); } if (this.options.verboseLogging) { this.log('info', `Cleanup handler registered: ${name} (${priority})`); } this.emit('handler-registered', { name, priority }); } /** * クリーンアップハンドラーの削除 */ unregisterCleanupHandler(name) { const removed = this.cleanupHandlers.delete(name) || this.criticalHandlers.delete(name); if (removed && this.options.verboseLogging) { this.log('info', `Cleanup handler unregistered: ${name}`); } return removed; } /** * アクティブな操作の登録 */ registerActiveOperation(id, operation) { this.activeOperations.set(id, { ...operation, startTime: Date.now(), registered: Date.now() }); this.emit('operation-registered', { id, operation }); } /** * アクティブな操作の完了 */ completeActiveOperation(id, result = {}) { const operation = this.activeOperations.get(id); if (operation) { this.activeOperations.delete(id); this.emit('operation-completed', { id, operation, result, duration: Date.now() - operation.startTime }); } } /** * シャットダウンの開始 */ async initiateShutdown(reason, message, metadata = {}) { if (this.shutdownState.isShuttingDown) { this.log('warn', `Shutdown already in progress (${this.shutdownState.shutdownReason})`); return; } this.shutdownState = { isShuttingDown: true, shutdownReason: reason, shutdownMessage: message, shutdownStartTime: Date.now(), currentPhase: 'initiated', completedPhases: [], errors: [], metadata }; this.stats.totalShutdowns++; this.log('info', `🛑 Graceful shutdown initiated: ${message}`); this.emit('shutdown-initiated', { reason, message, metadata, activeOperations: this.activeOperations.size, registeredHandlers: this.cleanupHandlers.size + this.criticalHandlers.size }); try { // 状態の保存 if (this.options.enableStatePersistence) { await this.persistCurrentState(); } // タイムアウト設定 const shutdownTimer = setTimeout(() => { this.log('warn', '⚠️ Graceful shutdown timeout, forcing exit'); this.forceShutdown(); }, this.options.shutdownTimeout); // 段階的シャットダウンの実行 await this.executeGracefulShutdown(); clearTimeout(shutdownTimer); this.stats.gracefulShutdowns++; this.stats.averageShutdownTime = this.calculateAverageShutdownTime(); this.stats.lastShutdownTime = Date.now(); this.log('info', '✅ Graceful shutdown completed'); this.emit('shutdown-completed', { duration: Date.now() - this.shutdownState.shutdownStartTime, phases: this.shutdownState.completedPhases, errors: this.shutdownState.errors }); // 強制終了タイマー設定(万が一に備えて) setTimeout(() => { process.exit(0); }, 1000); } catch (error) { this.log('error', `Graceful shutdown failed: ${error.message}`, error); this.shutdownState.errors.push({ phase: this.shutdownState.currentPhase, error: error.message, timestamp: Date.now() }); this.forceShutdown(); } } /** * 段階的シャットダウンの実行 */ async executeGracefulShutdown() { const phases = [ { name: 'stop-new-operations', timeout: 2000 }, { name: 'complete-active-operations', timeout: 8000 }, { name: 'execute-critical-cleanup', timeout: 5000 }, { name: 'execute-normal-cleanup', timeout: 10000 }, { name: 'save-final-state', timeout: 3000 }, { name: 'final-cleanup', timeout: 2000 } ]; for (const phase of phases) { try { this.shutdownState.currentPhase = phase.name; this.emit('shutdown-phase-started', phase); if (this.options.verboseLogging) { this.log('info', `📋 Executing shutdown phase: ${phase.name}`); } await this.executePhaseWithTimeout(phase); this.shutdownState.completedPhases.push({ name: phase.name, completedAt: Date.now(), duration: Date.now() - this.shutdownState.shutdownStartTime }); this.emit('shutdown-phase-completed', phase); } catch (error) { this.log('error', `Phase ${phase.name} failed: ${error.message}`, error); this.shutdownState.errors.push({ phase: phase.name, error: error.message, timestamp: Date.now() }); // 重要フェーズが失敗した場合は中断 if (['execute-critical-cleanup', 'save-final-state'].includes(phase.name)) { throw error; } } } } /** * フェーズをタイムアウト付きで実行 */ async executePhaseWithTimeout(phase) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Phase ${phase.name} timed out after ${phase.timeout}ms`)); }, phase.timeout); const executePhase = async () => { switch (phase.name) { case 'stop-new-operations': await this.stopNewOperations(); break; case 'complete-active-operations': await this.completeActiveOperations(); break; case 'execute-critical-cleanup': await this.executeCriticalCleanup(); break; case 'execute-normal-cleanup': await this.executeNormalCleanup(); break; case 'save-final-state': await this.saveFinalState(); break; case 'final-cleanup': await this.finalCleanup(); break; } }; executePhase() .then(() => { clearTimeout(timer); resolve(); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } /** * 新しい操作の停止 */ async stopNewOperations() { this.emit('stop-new-operations'); // 新しい操作の受付停止(外部システムが対応) await this.delay(100); // 短い待機 } /** * アクティブな操作の完了待ち */ async completeActiveOperations() { const maxWait = 5000; // 最大5秒待機 const checkInterval = 100; // 100ms毎にチェック const startTime = Date.now(); while (this.activeOperations.size > 0 && (Date.now() - startTime) < maxWait) { if (this.options.verboseLogging) { this.log('info', `⏳ Waiting for ${this.activeOperations.size} active operations...`); } this.emit('waiting-for-operations', { count: this.activeOperations.size, elapsed: Date.now() - startTime }); await this.delay(checkInterval); } if (this.activeOperations.size > 0) { this.log('warn', `⚠️ ${this.activeOperations.size} operations still active, proceeding anyway`); // 残りの操作を記録 for (const [id, operation] of this.activeOperations) { this.log('warn', ` Active operation: ${id} (${operation.type || 'unknown'})`); } } } /** * 重要なクリーンアップの実行 */ async executeCriticalCleanup() { const handlers = Array.from(this.criticalHandlers.values()); for (const handlerInfo of handlers) { try { if (this.options.verboseLogging) { this.log('info', `🔧 Executing critical cleanup: ${handlerInfo.name}`); } await handlerInfo.handler(); this.emit('cleanup-handler-completed', { name: handlerInfo.name, priority: 'critical' }); } catch (error) { this.log('error', `Critical cleanup failed (${handlerInfo.name}): ${error.message}`, error); this.shutdownState.errors.push({ phase: 'critical-cleanup', handler: handlerInfo.name, error: error.message, timestamp: Date.now() }); } } } /** * 通常のクリーンアップの実行 */ async executeNormalCleanup() { const handlers = Array.from(this.cleanupHandlers.values()); // 並列実行(ただしエラーは個別にキャッチ) const cleanupPromises = handlers.map(async (handlerInfo) => { try { if (this.options.verboseLogging) { this.log('info', `🔧 Executing cleanup: ${handlerInfo.name}`); } await handlerInfo.handler(); this.emit('cleanup-handler-completed', { name: handlerInfo.name, priority: 'normal' }); } catch (error) { this.log('error', `Cleanup failed (${handlerInfo.name}): ${error.message}`, error); this.shutdownState.errors.push({ phase: 'normal-cleanup', handler: handlerInfo.name, error: error.message, timestamp: Date.now() }); } }); await Promise.allSettled(cleanupPromises); } /** * 最終状態の保存 */ async saveFinalState() { if (!this.options.enableStatePersistence) return; const finalState = { shutdownInfo: this.shutdownState, stats: this.stats, timestamp: new Date().toISOString(), version: '1.0.0', completedSuccessfully: true }; try { await fs.writeFile(this.options.stateFile, JSON.stringify(finalState, null, 2)); if (this.options.verboseLogging) { this.log('info', `💾 Final state saved: ${this.options.stateFile}`); } } catch (error) { this.log('error', `Failed to save final state: ${error.message}`, error); throw error; } } /** * 最終クリーンアップ */ async finalCleanup() { // ログファイルのフラッシュ // タイマーのクリア // 一時ファイルの削除等 this.emit('final-cleanup'); if (this.options.verboseLogging) { this.log('info', '🧹 Final cleanup completed'); } } /** * 強制シャットダウン */ forceShutdown() { this.stats.forcedShutdowns++; this.log('warn', '🚨 Forcing process exit'); this.emit('forced-shutdown', { reason: this.shutdownState.shutdownReason, duration: Date.now() - this.shutdownState.shutdownStartTime, errors: this.shutdownState.errors }); // 最小限の状態保存を試行 try { if (this.options.enableStatePersistence) { const emergencyState = { forcedShutdown: true, reason: this.shutdownState.shutdownReason, timestamp: new Date().toISOString(), activeOperations: Array.from(this.activeOperations.keys()) }; fsSync.writeFileSync(this.options.stateFile + '.emergency', JSON.stringify(emergencyState)); } } catch (error) { // エラーは無視(強制終了中のため) } setTimeout(() => { process.exit(1); }, this.options.forceKillTimeout); } /** * 現在の状態の保存 */ async persistCurrentState() { if (!this.options.enableStatePersistence) return; const currentState = { shutdownInfo: this.shutdownState, activeOperations: Array.from(this.activeOperations.entries()), registeredHandlers: { critical: Array.from(this.criticalHandlers.keys()), normal: Array.from(this.cleanupHandlers.keys()) }, stats: this.stats, timestamp: new Date().toISOString(), pid: process.pid }; try { // バックアップの作成 if (fsSync.existsSync(this.options.stateFile)) { await fs.copyFile(this.options.stateFile, this.options.backupStateFile); } await fs.writeFile(this.options.stateFile, JSON.stringify(currentState, null, 2)); } catch (error) { this.log('error', `Failed to persist state: ${error.message}`, error); } } /** * 復旧状態のチェック */ async checkForRecoveryState() { try { if (fsSync.existsSync(this.options.stateFile)) { const stateContent = await fs.readFile(this.options.stateFile, 'utf8'); const state = JSON.parse(stateContent); this.log('info', '🔄 Previous shutdown state found, checking for recovery...'); if (state.shutdownInfo && !state.completedSuccessfully) { this.stats.recoveryAttempts++; this.emit('recovery-available', { previousShutdown: state.shutdownInfo, activeOperations: state.activeOperations, timestamp: state.timestamp }); if (this.options.verboseLogging) { this.log('info', ` Previous shutdown: ${state.shutdownInfo.shutdownReason}`); this.log('info', ` Time: ${state.timestamp}`); this.log('info', ` Active operations: ${state.activeOperations?.length || 0}`); } } // 状態ファイルのクリーンアップ await fs.unlink(this.options.stateFile); } } catch (error) { this.log('error', `Recovery state check failed: ${error.message}`, error); } } /** * 平均シャットダウン時間の計算 */ calculateAverageShutdownTime() { if (this.stats.gracefulShutdowns === 0) return 0; const currentDuration = Date.now() - this.shutdownState.shutdownStartTime; const alpha = 0.1; return this.stats.averageShutdownTime * (1 - alpha) + currentDuration * alpha; } /** * 遅延実行 */ async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * ログ出力 */ log(level, message, data = null) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, data, pid: process.pid }; // コンソール出力 const logMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`; switch (level) { case 'error': console.error(logMessage); break; case 'warn': console.warn(logMessage); break; default: console.log(logMessage); } // ファイル出力(オプション) if (this.options.logFile) { try { const logLine = JSON.stringify(logEntry) + '\n'; fsSync.appendFileSync(this.options.logFile, logLine); } catch (error) { // ログ出力エラーは無視 } } } /** * 統計情報取得 */ getStats() { return { ...this.stats, currentState: this.shutdownState, activeOperations: this.activeOperations.size, registeredHandlers: { critical: this.criticalHandlers.size, normal: this.cleanupHandlers.size, total: this.criticalHandlers.size + this.cleanupHandlers.size } }; } /** * 手動シャットダウン開始 */ async shutdown(reason = 'MANUAL', message = 'Manual shutdown requested') { await this.initiateShutdown(reason, message, { manual: true }); } /** * クリーンアップ */ cleanup() { // イベントリスナーの削除 process.removeAllListeners('SIGINT'); process.removeAllListeners('SIGTERM'); process.removeAllListeners('uncaughtException'); process.removeAllListeners('unhandledRejection'); if (process.platform === 'win32') { process.removeAllListeners('SIGBREAK'); } this.emit('cleanup'); } } // デフォルトインスタンス export const defaultGracefulShutdownManager = new GracefulShutdownManager();