UNPKG

@claude-vector/core

Version:

Core vector search engine for code intelligence

514 lines (435 loc) 15.2 kB
/** * APIレジリエンス(耐性)システム - 包括的エラーハンドリングとリカバリー * * 機能: * - 指数バックオフによるアダプティブリトライ * - APIレート制限の検出と適応的待機 * - ネットワークエラーからの自動回復 * - 回路ブレーカーパターン * - フォールバック戦略 * - 詳細なエラー分類と対応 */ import { EventEmitter } from 'events'; export class APIResilience extends EventEmitter { constructor(options = {}) { super(); this.options = { // リトライ設定 maxRetries: options.maxRetries ?? 5, baseDelay: options.baseDelay ?? 1000, // 初期遅延(ミリ秒) maxDelay: options.maxDelay ?? 30000, // 最大遅延(ミリ秒) exponentialBase: options.exponentialBase ?? 2, jitter: options.jitter ?? true, // ランダムジッター追加 // 回路ブレーカー設定 circuitBreakerEnabled: options.circuitBreakerEnabled ?? true, failureThreshold: options.failureThreshold ?? 5, // 連続失敗しきい値 recoveryTimeout: options.recoveryTimeout ?? 60000, // 回復タイムアウト(ミリ秒) halfOpenRetryCount: options.halfOpenRetryCount ?? 3, // レート制限設定 rateLimitDetection: options.rateLimitDetection ?? true, rateLimitBackoff: options.rateLimitBackoff ?? 60000, // レート制限時の待機時間 adaptiveRateLimiting: options.adaptiveRateLimiting ?? true, // タイムアウト設定 requestTimeout: options.requestTimeout ?? 30000, connectionTimeout: options.connectionTimeout ?? 10000, // ログ設定 verboseLogging: options.verboseLogging ?? false, ...options }; // 回路ブレーカー状態 this.circuitBreaker = { state: 'CLOSED', // CLOSED, OPEN, HALF_OPEN failures: 0, lastFailureTime: null, nextRetryTime: null, halfOpenAttempts: 0 }; // 統計情報 this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, retriedRequests: 0, circuitBreakerTrips: 0, rateLimitHits: 0, totalRetryDelay: 0, averageResponseTime: 0 }; // エラー分類 this.errorTypes = { NETWORK_ERROR: 'network_error', TIMEOUT_ERROR: 'timeout_error', RATE_LIMIT_ERROR: 'rate_limit_error', AUTH_ERROR: 'auth_error', VALIDATION_ERROR: 'validation_error', SERVER_ERROR: 'server_error', QUOTA_EXCEEDED: 'quota_exceeded', UNKNOWN_ERROR: 'unknown_error' }; // 動的レート制限調整 this.rateLimitState = { requestsPerMinute: 60, // 初期値 lastAdjustment: Date.now(), consecutiveSuccesses: 0, adaptiveMode: this.options.adaptiveRateLimiting }; } /** * APIコールを耐性機能付きで実行 */ async executeWithResilience(apiCall, context = {}) { const startTime = Date.now(); this.stats.totalRequests++; // 回路ブレーカーチェック if (this.circuitBreaker.state === 'OPEN') { if (Date.now() < this.circuitBreaker.nextRetryTime) { const error = new Error('Circuit breaker is OPEN'); error.type = 'CIRCUIT_BREAKER_OPEN'; throw error; } else { // ハーフオープン状態に移行 this.circuitBreaker.state = 'HALF_OPEN'; this.circuitBreaker.halfOpenAttempts = 0; this.emit('circuit-breaker-half-open'); } } let lastError = null; let retryCount = 0; while (retryCount <= this.options.maxRetries) { try { // レート制限チェック await this.enforceRateLimit(); // API呼び出し実行(タイムアウト付き) const result = await this.executeWithTimeout(apiCall, context); // 成功時の処理 this.handleSuccess(startTime); return result; } catch (error) { lastError = error; const errorType = this.classifyError(error); // エラー統計更新 this.stats.failedRequests++; this.handleFailure(error, errorType); // リトライ可能性判定 if (!this.isRetryableError(errorType) || retryCount >= this.options.maxRetries) { break; } retryCount++; this.stats.retriedRequests++; // リトライ前の待機 const delay = this.calculateRetryDelay(retryCount, errorType); this.stats.totalRetryDelay += delay; this.emit('retry-attempt', { attempt: retryCount, delay, error: lastError, errorType, context }); if (this.options.verboseLogging) { console.log(`🔄 Retry attempt ${retryCount}/${this.options.maxRetries} after ${delay}ms delay`); } await this.delay(delay); } } // 全リトライ失敗 this.emit('all-retries-failed', { totalAttempts: retryCount + 1, finalError: lastError, context }); throw lastError; } /** * エラーの分類 */ classifyError(error) { const message = error.message?.toLowerCase() || ''; const statusCode = error.status || error.statusCode; // OpenAI APIの特定エラー if (error.type === 'invalid_request_error') { return this.errorTypes.VALIDATION_ERROR; } if (error.type === 'authentication_error') { return this.errorTypes.AUTH_ERROR; } if (error.type === 'permission_error') { return this.errorTypes.AUTH_ERROR; } if (error.type === 'rate_limit_error' || statusCode === 429) { return this.errorTypes.RATE_LIMIT_ERROR; } if (error.type === 'quota_exceeded' || message.includes('quota')) { return this.errorTypes.QUOTA_EXCEEDED; } // HTTPステータスコードベース if (statusCode >= 500) { return this.errorTypes.SERVER_ERROR; } if (statusCode >= 400 && statusCode < 500) { return this.errorTypes.VALIDATION_ERROR; } // ネットワークエラー if (message.includes('network') || message.includes('connection') || message.includes('fetch') || error.code === 'ENOTFOUND' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { return this.errorTypes.NETWORK_ERROR; } // タイムアウトエラー if (message.includes('timeout') || error.name === 'TimeoutError') { return this.errorTypes.TIMEOUT_ERROR; } return this.errorTypes.UNKNOWN_ERROR; } /** * リトライ可能性判定 */ isRetryableError(errorType) { const retryableErrors = [ this.errorTypes.NETWORK_ERROR, this.errorTypes.TIMEOUT_ERROR, this.errorTypes.RATE_LIMIT_ERROR, this.errorTypes.SERVER_ERROR, this.errorTypes.UNKNOWN_ERROR ]; return retryableErrors.includes(errorType); } /** * リトライ遅延時間計算(指数バックオフ + ジッター) */ calculateRetryDelay(retryCount, errorType) { let baseDelay = this.options.baseDelay; // エラータイプに応じた遅延調整 switch (errorType) { case this.errorTypes.RATE_LIMIT_ERROR: baseDelay = this.options.rateLimitBackoff; break; case this.errorTypes.NETWORK_ERROR: baseDelay = this.options.baseDelay * 0.5; // ネットワークエラーは早めにリトライ break; case this.errorTypes.SERVER_ERROR: baseDelay = this.options.baseDelay * 1.5; // サーバーエラーは長めに待機 break; } // 指数バックオフ const exponentialDelay = baseDelay * Math.pow(this.options.exponentialBase, retryCount - 1); // 最大遅延時間の制限 let delay = Math.min(exponentialDelay, this.options.maxDelay); // ランダムジッター追加(thundering herd問題回避) if (this.options.jitter) { const jitterRange = delay * 0.1; // 10%のジッター const jitter = (Math.random() - 0.5) * 2 * jitterRange; delay = Math.max(0, delay + jitter); } return Math.round(delay); } /** * タイムアウト付きAPIコール実行 */ async executeWithTimeout(apiCall, _context) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { const error = new Error(`Request timeout after ${this.options.requestTimeout}ms`); error.name = 'TimeoutError'; reject(error); }, this.options.requestTimeout); apiCall() .then(result => { clearTimeout(timeoutId); resolve(result); }) .catch(error => { clearTimeout(timeoutId); reject(error); }); }); } /** * 成功時の処理 */ handleSuccess(startTime) { this.stats.successfulRequests++; const responseTime = Date.now() - startTime; this.stats.averageResponseTime = this.calculateMovingAverage( this.stats.averageResponseTime, responseTime, this.stats.successfulRequests ); // 回路ブレーカーの回復処理 if (this.circuitBreaker.state === 'HALF_OPEN') { this.circuitBreaker.halfOpenAttempts++; if (this.circuitBreaker.halfOpenAttempts >= this.options.halfOpenRetryCount) { this.circuitBreaker.state = 'CLOSED'; this.circuitBreaker.failures = 0; this.emit('circuit-breaker-closed'); } } else if (this.circuitBreaker.state === 'CLOSED') { this.circuitBreaker.failures = Math.max(0, this.circuitBreaker.failures - 1); } // 動的レート制限調整 if (this.rateLimitState.adaptiveMode) { this.rateLimitState.consecutiveSuccesses++; if (this.rateLimitState.consecutiveSuccesses >= 10) { this.adjustRateLimit(1.1); // 10%増加 this.rateLimitState.consecutiveSuccesses = 0; } } } /** * 失敗時の処理 */ handleFailure(error, errorType) { // 回路ブレーカーの失敗カウント if (this.options.circuitBreakerEnabled) { this.circuitBreaker.failures++; this.circuitBreaker.lastFailureTime = Date.now(); if (this.circuitBreaker.failures >= this.options.failureThreshold && this.circuitBreaker.state === 'CLOSED') { this.circuitBreaker.state = 'OPEN'; this.circuitBreaker.nextRetryTime = Date.now() + this.options.recoveryTimeout; this.stats.circuitBreakerTrips++; this.emit('circuit-breaker-open', { failures: this.circuitBreaker.failures, nextRetryTime: this.circuitBreaker.nextRetryTime }); } } // レート制限時の統計更新 if (errorType === this.errorTypes.RATE_LIMIT_ERROR) { this.stats.rateLimitHits++; if (this.rateLimitState.adaptiveMode) { this.adjustRateLimit(0.8); // 20%減少 this.rateLimitState.consecutiveSuccesses = 0; } } this.emit('api-error', { error, errorType, circuitBreakerState: this.circuitBreaker.state, retryCount: this.circuitBreaker.failures }); } /** * レート制限の強制 */ async enforceRateLimit() { if (!this.rateLimitState.adaptiveMode) return; const now = Date.now(); const timeSinceLastAdjustment = now - this.rateLimitState.lastAdjustment; // 1分間のリクエスト制限チェック if (timeSinceLastAdjustment < 60000) { const requestsThisMinute = this.stats.totalRequests % 100; // 簡易実装 const maxRequests = this.rateLimitState.requestsPerMinute; if (requestsThisMinute >= maxRequests) { const waitTime = 60000 - timeSinceLastAdjustment; await this.delay(waitTime); } } } /** * 動的レート制限調整 */ adjustRateLimit(factor) { const oldRate = this.rateLimitState.requestsPerMinute; this.rateLimitState.requestsPerMinute = Math.max(1, Math.min(120, Math.round(oldRate * factor)) ); if (this.options.verboseLogging && oldRate !== this.rateLimitState.requestsPerMinute) { console.log(`⚙️ Rate limit adjusted: ${oldRate} → ${this.rateLimitState.requestsPerMinute} requests/min`); } this.emit('rate-limit-adjusted', { oldRate, newRate: this.rateLimitState.requestsPerMinute, factor }); } /** * 遅延実行 */ async delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 移動平均計算 */ calculateMovingAverage(current, newValue, count) { const alpha = Math.min(0.1, 2 / (count + 1)); // 指数移動平均 return current * (1 - alpha) + newValue * alpha; } /** * 統計情報取得 */ getStats() { const successRate = this.stats.totalRequests > 0 ? (this.stats.successfulRequests / this.stats.totalRequests) * 100 : 0; return { ...this.stats, successRate: Math.round(successRate * 100) / 100, circuitBreakerState: this.circuitBreaker.state, currentRateLimit: this.rateLimitState.requestsPerMinute, averageRetryDelay: this.stats.retriedRequests > 0 ? Math.round(this.stats.totalRetryDelay / this.stats.retriedRequests) : 0 }; } /** * 統計リセット */ resetStats() { this.stats = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, retriedRequests: 0, circuitBreakerTrips: 0, rateLimitHits: 0, totalRetryDelay: 0, averageResponseTime: 0 }; this.emit('stats-reset'); } /** * 回路ブレーカーの手動リセット */ resetCircuitBreaker() { this.circuitBreaker = { state: 'CLOSED', failures: 0, lastFailureTime: null, nextRetryTime: null, halfOpenAttempts: 0 }; this.emit('circuit-breaker-reset'); } /** * ヘルスチェック */ getHealthStatus() { const stats = this.getStats(); const recentSuccessRate = stats.successRate; let status = 'healthy'; let issues = []; if (this.circuitBreaker.state === 'OPEN') { status = 'unhealthy'; issues.push('Circuit breaker is open'); } else if (this.circuitBreaker.state === 'HALF_OPEN') { status = 'degraded'; issues.push('Circuit breaker is in half-open state'); } if (recentSuccessRate < 80) { status = status === 'healthy' ? 'degraded' : status; issues.push(`Low success rate: ${recentSuccessRate}%`); } if (stats.rateLimitHits > stats.totalRequests * 0.1) { status = status === 'healthy' ? 'degraded' : status; issues.push('High rate limit hit rate'); } return { status, issues, stats, timestamp: Date.now() }; } } // デフォルトインスタンス export const defaultApiResilience = new APIResilience();