UNPKG

deepsource-mcp-server

Version:
246 lines 9.3 kB
/** * @fileoverview Retry executor that combines all retry components * This module orchestrates retry logic using policies, backoff, circuit breakers, and budgets. */ import { getRetryPolicyForEndpoint } from './retry-policy.js'; import { calculateRetryDelay, sleep } from './exponential-backoff.js'; import { CircuitBreakerManager } from './circuit-breaker.js'; import { RetryBudgetManager } from './retry-budget.js'; import { ErrorCategory } from '../errors/categories.js'; import { createLogger } from '../logging/logger.js'; const logger = createLogger('RetryExecutor'); /** * Execute a function with automatic retry logic * @template T The return type of the function * @param fn The function to execute * @param options Retry execution options * @returns The result of the execution * @public */ export async function executeWithRetry(fn, options = {}) { const endpoint = options.endpoint || 'default'; const policy = options.policy || getRetryPolicyForEndpoint(endpoint); const circuitBreakerManager = CircuitBreakerManager.getInstance(); const retryBudgetManager = RetryBudgetManager.getInstance(); const circuitBreaker = circuitBreakerManager.getBreaker(endpoint); const startTime = Date.now(); let lastError; let attemptNumber = 0; const maxTotalDuration = options.maxTotalDuration || 120000; // 2 minutes default logger.debug('Starting retry execution', { endpoint, policy: policy.name, maxAttempts: policy.maxAttempts, }); // Check if circuit breaker allows the initial request if (!circuitBreaker.canRequest()) { logger.warn('Circuit breaker is open, blocking request', { endpoint, state: circuitBreaker.getState(), }); return { success: false, error: new Error(`Circuit breaker is ${circuitBreaker.getState()} for ${endpoint}`), attempts: 0, totalDurationMs: 0, circuitBreakerBlocked: true, budgetExhausted: false, }; } while (attemptNumber <= policy.maxAttempts) { const elapsedMs = Date.now() - startTime; // Check if we've exceeded the maximum total duration if (elapsedMs > maxTotalDuration) { logger.warn('Maximum retry duration exceeded', { endpoint, elapsedMs, maxTotalDuration, }); break; } // For retries (not the first attempt), check circuit breaker and budget if (attemptNumber > 0) { // Check circuit breaker if (!circuitBreaker.canRequest()) { logger.warn('Circuit breaker blocking retry', { endpoint, attemptNumber, state: circuitBreaker.getState(), }); break; } // Check retry budget if (!retryBudgetManager.canRetry(endpoint)) { logger.warn('Retry budget exhausted', { endpoint, attemptNumber, }); return { success: false, error: lastError, attempts: attemptNumber, totalDurationMs: elapsedMs, circuitBreakerBlocked: false, budgetExhausted: true, }; } } try { logger.debug('Executing attempt', { endpoint, attemptNumber, totalAttempts: policy.maxAttempts + 1, }); // Call the retry callback if provided if (options.onRetryAttempt && attemptNumber > 0) { const context = { endpoint, attemptNumber, elapsedMs, lastError, isLastAttempt: attemptNumber === policy.maxAttempts, }; options.onRetryAttempt(context); } // Execute the function const result = await fn(); // Record success circuitBreaker.recordSuccess(); logger.info('Operation succeeded', { endpoint, attemptNumber, totalDurationMs: Date.now() - startTime, }); return { success: true, data: result, attempts: attemptNumber + 1, totalDurationMs: Date.now() - startTime, circuitBreakerBlocked: false, budgetExhausted: false, }; } catch (error) { lastError = error; logger.debug('Attempt failed', { endpoint, attemptNumber, error: error instanceof Error ? error.message : String(error), }); // Check if the error is retriable const isRetriable = isErrorRetriable(error, policy); if (!isRetriable) { logger.info('Error is not retriable', { endpoint, attemptNumber, errorType: error instanceof Error ? error.constructor.name : typeof error, }); circuitBreaker.recordFailure(); break; } // Record failure circuitBreaker.recordFailure(); // If this was the last attempt, don't calculate delay if (attemptNumber >= policy.maxAttempts) { break; } // Consume retry budget for the next attempt if (!retryBudgetManager.consumeRetry(endpoint)) { logger.warn('Failed to consume retry from budget', { endpoint, attemptNumber, }); return { success: false, error: lastError, attempts: attemptNumber + 1, totalDurationMs: Date.now() - startTime, circuitBreakerBlocked: false, budgetExhausted: true, }; } // Calculate and apply backoff delay const retryAfterHeader = options.extractRetryAfter?.(error); const retryInfo = calculateRetryDelay(attemptNumber, policy, retryAfterHeader); logger.info('Waiting before retry', { endpoint, attemptNumber, delayMs: retryInfo.delayMs, delaySource: retryInfo.source, }); await sleep(retryInfo.delayMs); attemptNumber++; } } // All attempts failed logger.error('All retry attempts failed', { endpoint, attempts: attemptNumber + 1, totalDurationMs: Date.now() - startTime, }); return { success: false, error: lastError, attempts: attemptNumber + 1, totalDurationMs: Date.now() - startTime, circuitBreakerBlocked: false, budgetExhausted: false, }; } /** * Check if an error is retriable based on the policy * @param error The error to check * @param policy The retry policy * @returns True if the error is retriable * @private */ function isErrorRetriable(error, policy) { // Check if it's a classified error if (error && typeof error === 'object' && 'category' in error) { const classifiedError = error; return policy.retriableErrors.includes(classifiedError.category); } // Check for specific error patterns if (error instanceof Error) { const message = error.message.toLowerCase(); // Network errors if (message.includes('network') || message.includes('econnrefused') || message.includes('econnreset') || message.includes('etimedout')) { return policy.retriableErrors.includes(ErrorCategory.NETWORK); } // Rate limit errors if (message.includes('rate limit') || message.includes('429')) { return policy.retriableErrors.includes(ErrorCategory.RATE_LIMIT); } // Server errors if (message.includes('500') || message.includes('502') || message.includes('503')) { return policy.retriableErrors.includes(ErrorCategory.SERVER); } // Timeout errors if (message.includes('timeout')) { return policy.retriableErrors.includes(ErrorCategory.TIMEOUT); } } // Default to not retriable for unknown errors return false; } /** * Create a retry-enabled wrapper for a function * @template T The return type of the function * @param fn The function to wrap * @param options Default retry options for the wrapper * @returns A retry-enabled version of the function * @public */ export function withRetry(fn, options = {}) { return async () => { const result = await executeWithRetry(fn, options); if (result.success && result.data !== undefined) { return result.data; } throw result.error || new Error('Operation failed after retries'); }; } //# sourceMappingURL=retry-executor.js.map