@claude-vector/core
Version:
Core vector search engine for code intelligence
710 lines (601 loc) • 20.4 kB
JavaScript
/**
* 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();