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