Files
elasticsearch/ts/core/errors/retry-policy.ts

197 lines
5.0 KiB
TypeScript
Raw Normal View History

import { RetryConfig, RetryStrategy } from './types.js';
import { ElasticsearchError } from './elasticsearch-error.js';
/**
* Calculates delay based on retry strategy
*/
export class RetryDelayCalculator {
constructor(private config: RetryConfig) {}
/**
* Calculate delay for the given attempt number
*/
calculateDelay(attempt: number): number {
let delay: number;
switch (this.config.strategy) {
case 'none':
return 0;
case 'fixed':
delay = this.config.initialDelay;
break;
case 'linear':
delay = this.config.initialDelay * attempt;
break;
case 'exponential':
const multiplier = this.config.backoffMultiplier || 2;
delay = this.config.initialDelay * Math.pow(multiplier, attempt - 1);
break;
default:
delay = this.config.initialDelay;
}
// Cap at max delay
delay = Math.min(delay, this.config.maxDelay);
// Add jitter if configured
if (this.config.jitterFactor && this.config.jitterFactor > 0) {
const jitter = delay * this.config.jitterFactor * Math.random();
delay = delay + jitter;
}
return Math.floor(delay);
}
}
/**
* Default retry configuration
*/
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxAttempts: 3,
strategy: 'exponential',
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
jitterFactor: 0.1,
};
/**
* Determines if an error should be retried based on its characteristics
*/
export function shouldRetryError(error: Error): boolean {
// If it's our custom error, check the retryable flag
if (error instanceof ElasticsearchError) {
return error.retryable;
}
// For native errors, check specific types
if (error.name === 'TimeoutError') return true;
if (error.message.includes('ECONNREFUSED')) return true;
if (error.message.includes('ECONNRESET')) return true;
if (error.message.includes('ETIMEDOUT')) return true;
if (error.message.includes('ENETUNREACH')) return true;
if (error.message.includes('EHOSTUNREACH')) return true;
// HTTP status codes that are retryable
if ('statusCode' in error) {
const statusCode = (error as any).statusCode;
if (statusCode === 429) return true; // Too Many Requests
if (statusCode === 503) return true; // Service Unavailable
if (statusCode === 504) return true; // Gateway Timeout
if (statusCode >= 500 && statusCode < 600) return true; // Server errors
}
return false;
}
/**
* Retry policy for executing operations with automatic retry
*
* @example
* ```typescript
* const policy = new RetryPolicy({
* maxAttempts: 5,
* strategy: 'exponential',
* initialDelay: 1000,
* maxDelay: 30000,
* });
*
* const result = await policy.execute(async () => {
* return await someElasticsearchOperation();
* });
* ```
*/
export class RetryPolicy {
private config: RetryConfig;
private delayCalculator: RetryDelayCalculator;
constructor(config: Partial<RetryConfig> = {}) {
this.config = { ...DEFAULT_RETRY_CONFIG, ...config };
this.delayCalculator = new RetryDelayCalculator(this.config);
}
/**
* Execute an operation with retry logic
*/
async execute<T>(
operation: () => Promise<T>,
context?: {
operationName?: string;
onRetry?: (attempt: number, error: Error, delayMs: number) => void;
}
): Promise<T> {
let lastError: Error | undefined;
let attempt = 0;
while (attempt < this.config.maxAttempts) {
attempt++;
try {
return await operation();
} catch (error) {
lastError = error as Error;
// Check if we should retry
const shouldRetry = this.shouldRetry(lastError, attempt);
if (!shouldRetry || attempt >= this.config.maxAttempts) {
throw lastError;
}
// Calculate delay
const delay = this.delayCalculator.calculateDelay(attempt);
// Call retry callback if provided
if (context?.onRetry) {
context.onRetry(attempt, lastError, delay);
}
// Wait before retrying
await this.sleep(delay);
}
}
// Should never reach here, but TypeScript doesn't know that
throw lastError || new Error('Retry policy exhausted');
}
/**
* Determine if an error should be retried
*/
private shouldRetry(error: Error, attempt: number): boolean {
// Check custom shouldRetry function first
if (this.config.shouldRetry) {
return this.config.shouldRetry(error, attempt);
}
// Use default retry logic
return shouldRetryError(error);
}
/**
* Sleep for the specified number of milliseconds
*/
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Get current configuration
*/
getConfig(): RetryConfig {
return { ...this.config };
}
/**
* Update configuration
*/
updateConfig(config: Partial<RetryConfig>): void {
this.config = { ...this.config, ...config };
this.delayCalculator = new RetryDelayCalculator(this.config);
}
}