BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
196
ts/core/errors/retry-policy.ts
Normal file
196
ts/core/errors/retry-policy.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user