UNPKG

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
/** * 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