adpa-enterprise-framework-automation
Version:
Modular, standards-compliant Node.js/TypeScript automation framework for enterprise requirements, project, and data management. Provides CLI and API for BABOK v3, PMBOK 7th Edition, and DMBOK 2.0 (in progress). Production-ready Express.js API with TypeSpe
168 lines • 6.54 kB
JavaScript
/**
* Enhanced Retry Manager with intelligent backoff and circuit breaker
*/
export class RetryManager {
static instance;
circuitBreakers = new Map();
DEFAULT_CIRCUIT_BREAKER_CONFIG = {
failureThreshold: 5,
recoveryTimeMs: 60000, // 1 minute
halfOpenMaxCalls: 3
};
DEFAULT_RETRY_CONFIG = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
retryableErrors: ['429', 'rate limit', 'timeout', '500', '502', '503', '504', 'ECONNRESET', 'ETIMEDOUT']
};
constructor() { }
static getInstance() {
if (!RetryManager.instance) {
RetryManager.instance = new RetryManager();
}
return RetryManager.instance;
}
async executeWithRetry(operation, operationName, provider, retryConfig = this.DEFAULT_RETRY_CONFIG) {
// Check circuit breaker
if (!this.canExecute(provider)) {
throw new Error(`Circuit breaker open for provider: ${provider}`);
}
let lastError;
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
try {
const result = await operation();
// Reset circuit breaker on success
this.onSuccess(provider);
if (attempt > 0) {
console.log(`✅ ${operationName} succeeded on attempt ${attempt + 1}`);
}
return result;
}
catch (error) {
lastError = error;
// Update circuit breaker
this.onFailure(provider);
if (attempt === retryConfig.maxRetries) {
console.error(`❌ ${operationName} failed after ${retryConfig.maxRetries + 1} attempts:`, error.message);
throw this.enhanceError(error, operationName, provider);
}
if (!this.isRetryableError(error, retryConfig.retryableErrors)) {
console.error(`❌ ${operationName} failed with non-retryable error:`, error.message);
throw this.enhanceError(error, operationName, provider);
}
const delay = this.calculateDelay(attempt, retryConfig, error);
console.log(`⚠️ ${operationName} failed on attempt ${attempt + 1}. Retrying in ${delay}ms...`);
await this.sleep(delay);
}
}
throw lastError;
}
canExecute(provider) {
const breaker = this.circuitBreakers.get(provider);
if (!breaker)
return true;
const now = Date.now();
switch (breaker.state) {
case 'closed':
return true;
case 'open':
if (now - breaker.lastFailureTime > this.DEFAULT_CIRCUIT_BREAKER_CONFIG.recoveryTimeMs) {
breaker.state = 'half-open';
return true;
}
return false;
case 'half-open':
return true;
default:
return true;
}
}
onSuccess(provider) {
const breaker = this.circuitBreakers.get(provider);
if (breaker) {
breaker.failures = 0;
breaker.state = 'closed';
}
}
onFailure(provider) {
let breaker = this.circuitBreakers.get(provider);
if (!breaker) {
breaker = { failures: 0, lastFailureTime: 0, state: 'closed' };
this.circuitBreakers.set(provider, breaker);
}
breaker.failures++;
breaker.lastFailureTime = Date.now();
if (breaker.failures >= this.DEFAULT_CIRCUIT_BREAKER_CONFIG.failureThreshold) {
breaker.state = 'open';
console.warn(`🔴 Circuit breaker opened for provider: ${provider}`);
}
}
calculateDelay(attempt, config, error) {
let baseDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt);
// Add jitter to prevent thundering herd
const jitter = Math.random() * 0.3 + 0.85; // 85-115% of base delay
baseDelay *= jitter;
// Special handling for rate limits
if (this.isRateLimitError(error)) {
const retryAfter = this.extractRetryAfter(error);
if (retryAfter) {
baseDelay = Math.max(baseDelay, retryAfter * 1000);
}
else {
baseDelay *= 2; // Extra penalty for rate limits
}
}
return Math.min(baseDelay, config.maxDelay);
}
isRetryableError(error, retryableErrors) {
const errorMessage = error.message?.toLowerCase() || '';
const errorStatus = error.status?.toString() || '';
return retryableErrors.some(retryableError => errorMessage.includes(retryableError.toLowerCase()) ||
errorStatus === retryableError);
}
isRateLimitError(error) {
const message = error.message?.toLowerCase() || '';
return error.status === 429 ||
message.includes('rate limit') ||
message.includes('too many requests');
}
extractRetryAfter(error) {
// Try to extract retry-after header value
const retryAfter = error.headers?.['retry-after'] ||
error.response?.headers?.['retry-after'];
if (retryAfter) {
const parsed = parseInt(retryAfter);
return isNaN(parsed) ? null : parsed;
}
return null;
}
enhanceError(error, operationName, provider) {
const breaker = this.circuitBreakers.get(provider);
const circuitStatus = breaker ? ` (Circuit: ${breaker.state}, Failures: ${breaker.failures})` : '';
const enhancedMessage = `${operationName} failed with ${provider}${circuitStatus}: ${error.message}`;
const enhancedError = new Error(enhancedMessage);
enhancedError.stack = error.stack;
return enhancedError;
}
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Circuit breaker status
getCircuitBreakerStatus() {
const status = {};
for (const [provider, breaker] of this.circuitBreakers) {
status[provider] = { ...breaker };
}
return status;
}
resetCircuitBreaker(provider) {
if (provider) {
this.circuitBreakers.delete(provider);
}
else {
this.circuitBreakers.clear();
}
}
}
//# sourceMappingURL=RetryManager.js.map