@gftdcojp/ksqldb-orm
Version:
ksqldb-orm - Server-Side TypeScript ORM for ksqlDB with enterprise security extensions
726 lines • 27 kB
JavaScript
"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