Files
elasticsearch/ts/core/plugins/built-in/retry-plugin.ts

141 lines
3.6 KiB
TypeScript

/**
* Retry Plugin
*
* Automatically retries failed requests with exponential backoff
*/
import { defaultLogger } from '../../observability/logger.js';
import type { Plugin, PluginErrorContext, RetryPluginConfig } from '../types.js';
/**
* Default configuration
*/
const DEFAULT_CONFIG: Required<RetryPluginConfig> = {
maxRetries: 3,
initialDelay: 1000, // 1 second
maxDelay: 30000, // 30 seconds
backoffMultiplier: 2,
retryableStatusCodes: [429, 502, 503, 504],
retryableErrors: [
'ECONNRESET',
'ENOTFOUND',
'ESOCKETTIMEDOUT',
'ETIMEDOUT',
'ECONNREFUSED',
'EHOSTUNREACH',
'EPIPE',
'EAI_AGAIN',
],
};
/**
* Create retry plugin
*/
export function createRetryPlugin(config: RetryPluginConfig = {}): Plugin {
const pluginConfig = { ...DEFAULT_CONFIG, ...config };
const logger = defaultLogger;
/**
* Check if error is retryable
*/
function isRetryable(context: PluginErrorContext): boolean {
// Check if we've exceeded max retries
if (context.attempts >= pluginConfig.maxRetries) {
return false;
}
// Check status code if response is available
if (context.response) {
return pluginConfig.retryableStatusCodes.includes(context.response.statusCode);
}
// Check error code/type
const errorCode = (context.error as any).code;
const errorType = context.error.name;
if (errorCode && pluginConfig.retryableErrors.includes(errorCode)) {
return true;
}
if (pluginConfig.retryableErrors.includes(errorType)) {
return true;
}
// Check for timeout errors
if (
errorType === 'TimeoutError' ||
context.error.message.toLowerCase().includes('timeout')
) {
return true;
}
// Check for connection errors
if (
errorType === 'ConnectionError' ||
context.error.message.toLowerCase().includes('connection')
) {
return true;
}
return false;
}
/**
* Calculate retry delay with exponential backoff
*/
function calculateDelay(attempt: number): number {
const delay = pluginConfig.initialDelay * Math.pow(pluginConfig.backoffMultiplier, attempt);
return Math.min(delay, pluginConfig.maxDelay);
}
/**
* Sleep for specified duration
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
return {
name: 'retry',
version: '1.0.0',
priority: 90, // Execute late, close to the actual request
onError: async (context: PluginErrorContext) => {
// Check if error is retryable
if (!isRetryable(context)) {
logger.debug('Error not retryable', {
error: context.error.name,
attempts: context.attempts,
maxRetries: pluginConfig.maxRetries,
});
return null;
}
// Calculate delay
const delay = calculateDelay(context.attempts);
logger.info('Retrying request', {
requestId: context.request.requestId,
attempt: context.attempts + 1,
maxRetries: pluginConfig.maxRetries,
delay,
error: context.error.message,
statusCode: context.response?.statusCode,
});
// Wait before retrying
await sleep(delay);
// Note: We don't actually retry the request here because we can't
// access the client from the plugin. Instead, we return null to
// indicate that the error was not handled, and the caller should
// handle the retry logic.
//
// In a real implementation, you would integrate this with the
// connection manager to actually retry the request.
return null;
},
};
}