@flightstream/core-client
Version:
Core Apache Arrow Flight client framework for Node.js
183 lines (156 loc) • 4.87 kB
JavaScript
import { DEFAULT_CLIENT_CONFIG } from '../config/client-config.js';
/**
* Retry Handler for Flight Client Operations
*
* This utility class provides sophisticated retry logic with exponential
* backoff, circuit breaker patterns, and configurable retry strategies.
*/
export class RetryHandler {
constructor(options = {}) {
this.options = {
maxAttempts: options.maxAttempts || DEFAULT_CLIENT_CONFIG.retryAttempts,
baseDelay: options.baseDelay || DEFAULT_CLIENT_CONFIG.retryDelay,
maxDelay: options.maxDelay || 30000,
backoffMultiplier: options.backoffMultiplier || 2,
jitter: options.jitter !== false,
retryableErrors: options.retryableErrors || [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'UNAVAILABLE',
'DEADLINE_EXCEEDED'
],
logger: options.logger || console,
...options
};
this.failureCount = 0;
this.lastFailureTime = 0;
this.circuitBreakerState = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
/**
* Execute an operation with retry logic
* @param {Function} operation - The operation to execute
* @param {Object} options - Retry options
* @returns {Promise<any>} Operation result
*/
async execute(operation, options = {}) {
const config = { ...this.options, ...options };
let lastError;
// Ensure maxAttempts is valid
const maxAttempts = Math.max(1, config.maxAttempts || 1);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
// Check circuit breaker
if (this.circuitBreakerState === 'OPEN') {
const circuitBreakerTimeout = config.circuitBreakerTimeout || 60000;
if (Date.now() - this.lastFailureTime < circuitBreakerTimeout) {
throw new Error('Circuit breaker is open');
}
this.circuitBreakerState = 'HALF_OPEN';
}
const result = await operation();
// Success - reset circuit breaker
this._onSuccess();
return result;
} catch (error) {
lastError = error;
// Check if error is retryable
if (!this._isRetryableError(error)) {
throw error;
}
// Check if we should stop retrying
if (attempt === maxAttempts) {
this._onFailure();
throw error;
}
// Calculate delay with exponential backoff
const delay = this._calculateDelay(attempt, config);
config.logger.warn(`Operation failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms:`, error.message);
// Wait before retrying
await this._wait(delay);
}
}
throw lastError;
}
/**
* Check if an error is retryable
* @param {Error} error - The error to check
* @returns {boolean} True if error is retryable
* @private
*/
_isRetryableError(error) {
const errorCode = error.code || error.message;
return this.options.retryableErrors.some(retryableError =>
errorCode.includes(retryableError) ||
error.message.includes(retryableError)
);
}
/**
* Calculate delay for retry with exponential backoff
* @param {number} attempt - Current attempt number
* @param {Object} config - Retry configuration
* @returns {number} Delay in milliseconds
* @private
*/
_calculateDelay(attempt, config) {
let delay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1);
// Cap at max delay
delay = Math.min(delay, config.maxDelay);
// Add jitter to prevent thundering herd
if (config.jitter) {
const jitter = delay * 0.1; // 10% jitter
delay += Math.random() * jitter - jitter / 2;
}
return Math.floor(delay);
}
/**
* Wait for a specified delay
* @param {number} delay - Delay in milliseconds
* @returns {Promise<void>}
* @private
*/
_wait(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
/**
* Handle successful operation
* @private
*/
_onSuccess() {
this.failureCount = 0;
this.circuitBreakerState = 'CLOSED';
}
/**
* Handle failed operation
* @private
*/
_onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
// Open circuit breaker if too many failures
if (this.failureCount >= (this.options.circuitBreakerThreshold || 5)) {
this.circuitBreakerState = 'OPEN';
}
}
/**
* Get retry statistics
* @returns {Object} Retry statistics
*/
getStats() {
return {
failureCount: this.failureCount,
circuitBreakerState: this.circuitBreakerState,
lastFailureTime: this.lastFailureTime,
config: this.options
};
}
/**
* Reset retry handler state
*/
reset() {
this.failureCount = 0;
this.lastFailureTime = 0;
this.circuitBreakerState = 'CLOSED';
}
}