UNPKG

@gftdcojp/ksqldb-orm

Version:

ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions

726 lines 27 kB
"use strict"; /** * Resilience utilities for ksqlDB partition rebalancing and error handling * * @description このモジュールは、ksqlDBのパーティション再バランシングエラーを * 検出し、適切なリトライ戦略やフォールバック機構を提供します。 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ResilientExecutor = exports.MetricsCollector = exports.CircuitBreaker = exports.RetryStrategy = exports.ErrorClassifier = void 0; const types_1 = require("./types"); const logger_1 = require("./utils/logger"); /** * パーティション再バランシングに関連するエラーパターン */ const PARTITION_ERROR_PATTERNS = [ /cannot determine which host contains the required partitions/i, /partition rebalancing/i, /kafka rebalance/i, /partition assignment/i, /leader not available/i, /no available brokers/i, /partition leader not yet known/i, /connection refused.*partition/i, /metadata not available.*partition/i, /invalid partition/i, /partition out of range/i, /no current assignment.*partition/i, /rebalance in progress/i, /consumer group.*rebalance/i ]; /** * ネットワーク関連エラーパターン */ const NETWORK_ERROR_PATTERNS = [ /econnrefused/i, /enotfound/i, /timeout/i, /network.*error/i, /connection.*reset/i, /socket.*hang.*up/i, /connection.*timed.*out/i, /dns.*lookup.*failed/i ]; /** * サーバーエラーパターン(5xx) */ const SERVER_ERROR_PATTERNS = [ /internal.*server.*error/i, /service.*unavailable/i, /bad.*gateway/i, /gateway.*timeout/i, /5\d{2}/ // HTTP 5xx status codes ]; /** * エラー分類ユーティリティ */ class ErrorClassifier { /** * エラーの種類を分類 * @param error エラーオブジェクト * @returns 分類されたエラータイプ */ static classifyError(error) { if (!error) { return types_1.ResilienceErrorType.UNKNOWN_ERROR; } const errorMessage = this.extractErrorMessage(error); const statusCode = this.extractStatusCode(error); // タイムアウトエラーの検出(早期検出) if (this.isTimeoutError(errorMessage)) { return types_1.ResilienceErrorType.CONNECTION_TIMEOUT; } // パーティション再バランシングエラーの検出 if (this.isPartitionRebalancingError(errorMessage, statusCode)) { logger_1.log.warn('Partition rebalancing error detected', { message: errorMessage, statusCode, errorType: types_1.ResilienceErrorType.PARTITION_REBALANCING }); return types_1.ResilienceErrorType.PARTITION_REBALANCING; } // ネットワークエラーの検出 if (this.isNetworkError(errorMessage, statusCode)) { return types_1.ResilienceErrorType.NETWORK_ERROR; } // サーバーエラーの検出 if (this.isServerError(errorMessage, statusCode)) { return types_1.ResilienceErrorType.SERVER_ERROR; } return types_1.ResilienceErrorType.UNKNOWN_ERROR; } /** * パーティション再バランシングエラーかどうかを判定 */ static isPartitionRebalancingError(message, statusCode) { // HTTPステータスコードでの判定 if (statusCode === 503 || statusCode === 502) { return true; } // エラーメッセージパターンでの判定 return PARTITION_ERROR_PATTERNS.some(pattern => pattern.test(message)); } /** * ネットワークエラーかどうかを判定 */ static isNetworkError(message, _statusCode) { return NETWORK_ERROR_PATTERNS.some(pattern => pattern.test(message)); } /** * サーバーエラーかどうかを判定 */ static isServerError(message, statusCode) { if (statusCode && statusCode >= 500 && statusCode < 600) { return true; } return SERVER_ERROR_PATTERNS.some(pattern => pattern.test(message)); } /** * タイムアウトエラーかどうかを判定 */ static isTimeoutError(message) { return /timeout/i.test(message) || /timed.*out/i.test(message); } /** * エラーからメッセージを抽出 */ static extractErrorMessage(error) { if (typeof error === 'string') { return error; } if (error?.message) { return error.message; } if (error?.response?.data) { if (typeof error.response.data === 'string') { return error.response.data; } if (error.response.data.message) { return error.response.data.message; } if (error.response.data.error_code) { return `${error.response.data.error_code}: ${error.response.data.message || 'Unknown error'}`; } } return JSON.stringify(error); } /** * エラーからHTTPステータスコードを抽出 */ static extractStatusCode(error) { if (error?.response?.status) { return error.response.status; } if (error?.status) { return error.status; } return undefined; } /** * エラーが再試行可能かどうかを判定 * @param errorType エラータイプ * @param retryableErrors 再試行可能なエラータイプのリスト * @returns 再試行可能な場合はtrue */ static isRetryableError(errorType, retryableErrors) { // カスタム設定がある場合はそれを使用 if (retryableErrors && retryableErrors.length > 0) { return retryableErrors.includes(errorType); } // デフォルトの再試行可能エラー return [ types_1.ResilienceErrorType.PARTITION_REBALANCING, types_1.ResilienceErrorType.CONNECTION_TIMEOUT, types_1.ResilienceErrorType.NETWORK_ERROR, types_1.ResilienceErrorType.SERVER_ERROR ].includes(errorType); } } exports.ErrorClassifier = ErrorClassifier; /** * リトライ戦略ユーティリティ */ class RetryStrategy { /** * 指数バックオフによる遅延時間を計算 * @param attempt 試行回数(1から開始) * @param baseDelay 基本遅延時間(ミリ秒) * @param maxDelay 最大遅延時間(ミリ秒) * @param jitter ジッター係数(0-1) * @returns 遅延時間(ミリ秒) */ static calculateExponentialBackoff(attempt, baseDelay = 1500, maxDelay = 30000, jitter = 0.1) { const exponentialDelay = baseDelay * Math.pow(2, attempt - 1); const delayWithCap = Math.min(exponentialDelay, maxDelay); // ジッターを追加(ランダム性) const jitterRange = delayWithCap * jitter; const jitterValue = (Math.random() - 0.5) * 2 * jitterRange; return Math.max(0, delayWithCap + jitterValue); } /** * 線形バックオフによる遅延時間を計算 * @param attempt 試行回数(1から開始) * @param baseDelay 基本遅延時間(ミリ秒) * @param maxDelay 最大遅延時間(ミリ秒) * @param jitter ジッター係数(0-1) * @returns 遅延時間(ミリ秒) */ static calculateLinearBackoff(attempt, baseDelay = 1500, maxDelay = 30000, jitter = 0.1) { const linearDelay = baseDelay * attempt; const delayWithCap = Math.min(linearDelay, maxDelay); // ジッターを追加 const jitterRange = delayWithCap * jitter; const jitterValue = (Math.random() - 0.5) * 2 * jitterRange; return Math.max(0, delayWithCap + jitterValue); } /** * 固定遅延時間を計算 * @param baseDelay 基本遅延時間(ミリ秒) * @param jitter ジッター係数(0-1) * @returns 遅延時間(ミリ秒) */ static calculateFixedBackoff(baseDelay = 1500, jitter = 0.1) { // ジッターを追加 const jitterRange = baseDelay * jitter; const jitterValue = (Math.random() - 0.5) * 2 * jitterRange; return Math.max(0, baseDelay + jitterValue); } /** * バックオフ戦略に基づいて遅延時間を計算 * @param strategy バックオフ戦略 * @param attempt 試行回数 * @param baseDelay 基本遅延時間 * @param maxDelay 最大遅延時間 * @param jitter ジッター係数 * @returns 遅延時間(ミリ秒) */ static calculateDelay(strategy, attempt, baseDelay = 1500, maxDelay = 30000, jitter = 0.1) { switch (strategy) { case 'exponential': return this.calculateExponentialBackoff(attempt, baseDelay, maxDelay, jitter); case 'linear': return this.calculateLinearBackoff(attempt, baseDelay, maxDelay, jitter); case 'fixed': return this.calculateFixedBackoff(baseDelay, jitter); default: return this.calculateExponentialBackoff(attempt, baseDelay, maxDelay, jitter); } } /** * 遅延を実行 * @param delayMs 遅延時間(ミリ秒) * @returns Promise */ static async delay(delayMs) { return new Promise(resolve => setTimeout(resolve, delayMs)); } } exports.RetryStrategy = RetryStrategy; /** * サーキットブレーカー実装 */ class CircuitBreaker { constructor(failureThreshold = 5, _timeWindow = 60000, openTimeout = 30000, successThreshold = 2) { this.state = types_1.CircuitBreakerState.CLOSED; this.failureCount = 0; this.successCount = 0; this.lastFailureTime = null; this.failureThreshold = failureThreshold; this.openTimeout = openTimeout; this.successThreshold = successThreshold; } /** * サーキットブレーカーの現在状態を取得 */ getState() { this.updateStateIfNeeded(); return this.state; } /** * リクエスト実行前のチェック * @returns 実行可能な場合はtrue */ canExecute() { this.updateStateIfNeeded(); if (this.state === types_1.CircuitBreakerState.OPEN) { logger_1.log.warn('Circuit breaker is OPEN, rejecting request'); return false; } return true; } /** * 成功時の処理 */ onSuccess() { this.successCount++; if (this.state === types_1.CircuitBreakerState.HALF_OPEN) { if (this.successCount >= this.successThreshold) { this.state = types_1.CircuitBreakerState.CLOSED; this.failureCount = 0; this.successCount = 0; this.lastFailureTime = null; logger_1.log.info('Circuit breaker state changed to CLOSED'); } } else if (this.state === types_1.CircuitBreakerState.CLOSED) { // 成功時は失敗カウントをリセット this.failureCount = 0; this.lastFailureTime = null; } } /** * 失敗時の処理 */ onFailure() { this.failureCount++; this.lastFailureTime = new Date(); this.successCount = 0; // 失敗時は成功カウントをリセット if (this.state === types_1.CircuitBreakerState.CLOSED || this.state === types_1.CircuitBreakerState.HALF_OPEN) { if (this.failureCount >= this.failureThreshold) { this.state = types_1.CircuitBreakerState.OPEN; logger_1.log.warn(`Circuit breaker opened due to ${this.failureCount} failures`); } } } /** * 状態を必要に応じて更新 */ updateStateIfNeeded() { if (this.state === types_1.CircuitBreakerState.OPEN && this.lastFailureTime) { const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime(); if (timeSinceLastFailure >= this.openTimeout) { this.state = types_1.CircuitBreakerState.HALF_OPEN; this.successCount = 0; logger_1.log.info('Circuit breaker state changed to HALF_OPEN'); } } } /** * 統計情報を取得 */ getStats() { return { state: this.getState(), failureCount: this.failureCount, successCount: this.successCount, lastFailureTime: this.lastFailureTime }; } /** * サーキットブレーカーをリセット */ reset() { this.state = types_1.CircuitBreakerState.CLOSED; this.failureCount = 0; this.successCount = 0; this.lastFailureTime = null; logger_1.log.info('Circuit breaker reset to CLOSED state'); } } exports.CircuitBreaker = CircuitBreaker; /** * メトリクス収集器 */ class MetricsCollector { constructor() { this.responseTimes = []; this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, retriedRequests: 0, circuitBreakerTrips: 0, fallbackActivations: 0, averageResponseTime: 0, partitionRebalanceEvents: 0, lastUpdated: new Date() }; } /** * リクエスト開始を記録 */ recordRequestStart() { this.metrics.totalRequests++; } /** * 成功を記録 * @param responseTime レスポンス時間(ミリ秒) */ recordSuccess(responseTime) { this.metrics.successfulRequests++; this.recordResponseTime(responseTime); } /** * 失敗を記録 * @param errorType エラータイプ * @param responseTime レスポンス時間(ミリ秒) */ recordFailure(errorType, responseTime) { this.metrics.failedRequests++; this.recordResponseTime(responseTime); if (errorType === types_1.ResilienceErrorType.PARTITION_REBALANCING) { this.metrics.partitionRebalanceEvents++; } } /** * リトライを記録 */ recordRetry() { this.metrics.retriedRequests++; } /** * サーキットブレーカートリップを記録 */ recordCircuitBreakerTrip() { this.metrics.circuitBreakerTrips++; } /** * フォールバック使用を記録 */ recordFallbackActivation() { this.metrics.fallbackActivations++; } /** * レスポンス時間を記録 */ recordResponseTime(responseTime) { this.responseTimes.push(responseTime); // 最新100件のレスポンス時間のみ保持 if (this.responseTimes.length > 100) { this.responseTimes.shift(); } // 平均レスポンス時間を更新 this.metrics.averageResponseTime = this.responseTimes.reduce((sum, time) => sum + time, 0) / this.responseTimes.length; } /** * 現在のメトリクスを取得 */ getMetrics() { this.metrics.lastUpdated = new Date(); return { ...this.metrics }; } /** * メトリクスをリセット */ reset() { this.metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, retriedRequests: 0, circuitBreakerTrips: 0, fallbackActivations: 0, averageResponseTime: 0, partitionRebalanceEvents: 0, lastUpdated: new Date() }; this.responseTimes = []; } /** * 定期的なメトリクス収集を開始 * @param interval 収集間隔(ミリ秒) * @param collector カスタムコレクター関数 */ startPeriodicCollection(interval = 60000, collector) { this.stopPeriodicCollection(); this.metricsInterval = setInterval(() => { const currentMetrics = this.getMetrics(); if (collector) { try { collector(currentMetrics); } catch (error) { logger_1.log.error('Error in custom metrics collector', error); } } logger_1.log.debug('Resilience metrics collected', currentMetrics); }, interval); } /** * 定期的なメトリクス収集を停止 */ stopPeriodicCollection() { if (this.metricsInterval) { clearInterval(this.metricsInterval); this.metricsInterval = undefined; } } } exports.MetricsCollector = MetricsCollector; /** * リトライ機構を統合したリジリエント実行器 */ class ResilientExecutor { constructor(retryConfig, circuitBreakerConfig, _partitionConfig, fallbackConfig, metricsConfig) { // デフォルト設定を適用 this.retryConfig = { maxRetries: retryConfig?.maxRetries ?? 5, baseDelay: retryConfig?.baseDelay ?? 1500, maxDelay: retryConfig?.maxDelay ?? 30000, backoffStrategy: retryConfig?.backoffStrategy ?? 'exponential', jitter: retryConfig?.jitter ?? 0.1, retryableErrors: retryConfig?.retryableErrors ?? [] }; this.fallbackConfig = { fallbackToHttp: fallbackConfig?.fallbackToHttp ?? true, alternativeEndpoints: fallbackConfig?.alternativeEndpoints ?? [], fallbackTimeout: fallbackConfig?.fallbackTimeout ?? 5000 }; // サーキットブレーカーとメトリクス収集器を初期化 this.circuitBreaker = new CircuitBreaker(circuitBreakerConfig?.failureThreshold ?? 5, circuitBreakerConfig?.timeWindow ?? 60000, circuitBreakerConfig?.openTimeout ?? 30000, circuitBreakerConfig?.successThreshold ?? 2); this.metricsCollector = new MetricsCollector(); // メトリクス収集を開始 if (metricsConfig?.enabled !== false) { this.metricsCollector.startPeriodicCollection(metricsConfig?.interval ?? 60000, metricsConfig?.collector); } } /** * リジリエント実行(リトライ・サーキットブレーカー・フォールバック対応) * @param operation 実行する操作 * @param operationName 操作名(ログ用) * @param options 実行オプション * @returns 実行結果 */ async execute(operation, operationName = 'operation', options) { const startTime = Date.now(); let retryCount = 0; let fallbackUsed = false; let lastError = null; const maxRetries = options?.retries ?? this.retryConfig.maxRetries; const backoffStrategy = options?.backoffStrategy ?? this.retryConfig.backoffStrategy; // メトリクス記録開始 this.metricsCollector.recordRequestStart(); // サーキットブレーカーチェック if (!this.circuitBreaker.canExecute()) { this.metricsCollector.recordCircuitBreakerTrip(); const executionTime = Date.now() - startTime; return { data: null, error: new Error('Circuit breaker is OPEN - operation rejected'), resilience: { retryCount: 0, fallbackUsed: false, executionTime, circuitBreakerState: this.circuitBreaker.getState() } }; } // リトライループ for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { try { logger_1.log.debug(`Executing ${operationName} - attempt ${attempt}/${maxRetries + 1}`); // 操作実行 const result = await this.executeWithTimeout(operation, options?.timeout); // 成功時の処理 this.circuitBreaker.onSuccess(); const executionTime = Date.now() - startTime; this.metricsCollector.recordSuccess(executionTime); logger_1.log.info(`${operationName} succeeded on attempt ${attempt}`, { retryCount, executionTime, circuitBreakerState: this.circuitBreaker.getState() }); return { data: result, resilience: { retryCount, fallbackUsed, executionTime, circuitBreakerState: this.circuitBreaker.getState() } }; } catch (error) { lastError = error; const errorType = ErrorClassifier.classifyError(error); logger_1.log.warn(`${operationName} failed on attempt ${attempt}`, { error: error instanceof Error ? error.message : String(error), errorType, attempt, maxRetries: maxRetries + 1 }); // サーキットブレーカーに失敗を通知 this.circuitBreaker.onFailure(); // 最後の試行の場合、またはリトライ不可能なエラーの場合は終了 if (attempt === maxRetries + 1 || !ErrorClassifier.isRetryableError(errorType, this.retryConfig.retryableErrors)) { const executionTime = Date.now() - startTime; this.metricsCollector.recordFailure(errorType, executionTime); logger_1.log.error(`${operationName} failed after ${attempt} attempts`, { finalError: error instanceof Error ? error.message : String(error), errorType, retryCount, executionTime }); return { data: null, error, resilience: { retryCount, fallbackUsed, executionTime, circuitBreakerState: this.circuitBreaker.getState() } }; } // リトライカウントを増加 retryCount++; this.metricsCollector.recordRetry(); // 遅延時間を計算 const delay = RetryStrategy.calculateDelay(backoffStrategy, attempt, this.retryConfig.baseDelay, this.retryConfig.maxDelay, this.retryConfig.jitter); logger_1.log.debug(`Retrying ${operationName} in ${delay}ms`, { attempt, delay, errorType, strategy: backoffStrategy }); // 遅延実行 await RetryStrategy.delay(delay); } } // ここには到達しないはずだが、念のため const executionTime = Date.now() - startTime; return { data: null, error: lastError || new Error('Unknown error occurred'), resilience: { retryCount, fallbackUsed, executionTime, circuitBreakerState: this.circuitBreaker.getState() } }; } /** * フォールバック戦略付きでクエリを実行 * @param primaryOperation メインの操作 * @param fallbackOperation フォールバック操作 * @param operationName 操作名 * @param options 実行オプション * @returns 実行結果 */ async executeWithFallback(primaryOperation, fallbackOperation, operationName = 'operation', options) { // まずプライマリ操作を試行 const primaryResult = await this.execute(primaryOperation, `${operationName} (primary)`, { ...options, fallbackToHttp: false // プライマリではフォールバック無効 }); // プライマリが成功した場合はそのまま返す if (!primaryResult.error) { return primaryResult; } // フォールバック設定が無効の場合はプライマリの結果を返す if (!this.fallbackConfig.fallbackToHttp) { return primaryResult; } logger_1.log.info(`Primary ${operationName} failed, attempting fallback`, { primaryError: primaryResult.error instanceof Error ? primaryResult.error.message : String(primaryResult.error) }); // フォールバック操作を実行 this.metricsCollector.recordFallbackActivation(); const fallbackResult = await this.execute(fallbackOperation, `${operationName} (fallback)`, { ...options, timeout: options?.timeout ?? this.fallbackConfig.fallbackTimeout }); // フォールバックの結果にフラグを設定 return { ...fallbackResult, resilience: { ...fallbackResult.resilience, fallbackUsed: true, retryCount: primaryResult.resilience.retryCount + fallbackResult.resilience.retryCount } }; } /** * タイムアウト付きで操作を実行 * @param operation 実行する操作 * @param timeoutMs タイムアウト時間(ミリ秒) * @returns 実行結果 */ async executeWithTimeout(operation, timeoutMs) { if (!timeoutMs) { return operation(); } return Promise.race([ operation(), new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timed out after ${timeoutMs}ms`)); }, timeoutMs); }) ]); } /** * サーキットブレーカーの状態を取得 */ getCircuitBreakerState() { return this.circuitBreaker.getState(); } /** * メトリクス情報を取得 */ getMetrics() { return this.metricsCollector.getMetrics(); } /** * サーキットブレーカーをリセット */ resetCircuitBreaker() { this.circuitBreaker.reset(); } /** * メトリクスをリセット */ resetMetrics() { this.metricsCollector.reset(); } /** * リソースクリーンアップ */ dispose() { this.metricsCollector.stopPeriodicCollection(); } } exports.ResilientExecutor = ResilientExecutor; //# sourceMappingURL=resilience.js.map