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:
140
ts/core/plugins/built-in/retry-plugin.ts
Normal file
140
ts/core/plugins/built-in/retry-plugin.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user