UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

384 lines (383 loc) 12.2 kB
/** * Retry Policy * Configurable retry strategies for observability exporters */ import { logger } from "../utils/logger.js"; /** * Base retry policy with common configuration */ export class BaseRetryPolicy { maxAttempts; maxTotalTimeMs; retryableErrors; nonRetryableErrors; constructor(config) { this.maxAttempts = config.maxAttempts ?? 3; this.maxTotalTimeMs = config.maxTotalTimeMs ?? 60000; // 1 minute this.retryableErrors = new Set(config.retryableErrors ?? [ "ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EPIPE", "ENOTFOUND", "ENETUNREACH", "EAI_AGAIN", "429", // Rate limit "500", // Internal server error "502", // Bad gateway "503", // Service unavailable "504", // Gateway timeout ]); this.nonRetryableErrors = new Set(config.nonRetryableErrors ?? [ "400", // Bad request "401", // Unauthorized "403", // Forbidden "404", // Not found "422", // Unprocessable entity ]); } isRetryableError(error) { const errorCode = this.extractErrorCode(error); // Check if explicitly non-retryable if (this.nonRetryableErrors.has(errorCode)) { return false; } // Check if explicitly retryable if (this.retryableErrors.has(errorCode)) { return true; } // Default: retry on network-like errors return (error.message.includes("timeout") || error.message.includes("ECONNRESET") || error.message.includes("network")); } extractErrorCode(error) { // Check for HTTP status code in error const httpMatch = error.message.match(/(\d{3})/); if (httpMatch) { return httpMatch[1]; } // Check for Node.js error code const nodeError = error; if (nodeError.code) { return nodeError.code; } return "UNKNOWN"; } } /** * Exponential backoff retry policy * Delay increases exponentially with each attempt */ export class ExponentialBackoffPolicy extends BaseRetryPolicy { name = "exponential-backoff"; baseDelayMs; maxDelayMs; jitterFactor; constructor(config) { super(config ?? {}); this.baseDelayMs = config?.baseDelayMs ?? 1000; this.maxDelayMs = config?.maxDelayMs ?? 30000; this.jitterFactor = config?.jitterFactor ?? 0.1; } shouldRetry(context) { // Check max attempts if (context.attempt >= this.maxAttempts) { return { shouldRetry: false, delayMs: 0, reason: `Max attempts (${this.maxAttempts}) exceeded`, }; } // Check max total time if (context.elapsedMs >= this.maxTotalTimeMs) { return { shouldRetry: false, delayMs: 0, reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`, }; } // Check if error is retryable if (!this.isRetryableError(context.error)) { return { shouldRetry: false, delayMs: 0, reason: `Error not retryable: ${context.error.message}`, }; } // Calculate delay with exponential backoff const exponentialDelay = this.baseDelayMs * 2 ** context.attempt; const cappedDelay = Math.min(exponentialDelay, this.maxDelayMs); // Add jitter const jitter = cappedDelay * this.jitterFactor * (Math.random() * 2 - 1); const delayMs = Math.max(0, Math.round(cappedDelay + jitter)); return { shouldRetry: true, delayMs, reason: `Retrying after ${delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`, }; } } /** * Linear backoff retry policy * Delay increases linearly with each attempt */ export class LinearBackoffPolicy extends BaseRetryPolicy { name = "linear-backoff"; delayIncrementMs; initialDelayMs; constructor(config) { super(config ?? {}); this.initialDelayMs = config?.initialDelayMs ?? 1000; this.delayIncrementMs = config?.delayIncrementMs ?? 1000; } shouldRetry(context) { if (context.attempt >= this.maxAttempts) { return { shouldRetry: false, delayMs: 0, reason: `Max attempts (${this.maxAttempts}) exceeded`, }; } if (context.elapsedMs >= this.maxTotalTimeMs) { return { shouldRetry: false, delayMs: 0, reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`, }; } if (!this.isRetryableError(context.error)) { return { shouldRetry: false, delayMs: 0, reason: `Error not retryable: ${context.error.message}`, }; } const delayMs = this.initialDelayMs + context.attempt * this.delayIncrementMs; return { shouldRetry: true, delayMs, reason: `Retrying after ${delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`, }; } } /** * Fixed delay retry policy * Same delay for all retry attempts */ export class FixedDelayPolicy extends BaseRetryPolicy { name = "fixed-delay"; delayMs; constructor(config) { super(config ?? {}); this.delayMs = config?.delayMs ?? 1000; } shouldRetry(context) { if (context.attempt >= this.maxAttempts) { return { shouldRetry: false, delayMs: 0, reason: `Max attempts (${this.maxAttempts}) exceeded`, }; } if (context.elapsedMs >= this.maxTotalTimeMs) { return { shouldRetry: false, delayMs: 0, reason: `Max total time (${this.maxTotalTimeMs}ms) exceeded`, }; } if (!this.isRetryableError(context.error)) { return { shouldRetry: false, delayMs: 0, reason: `Error not retryable: ${context.error.message}`, }; } return { shouldRetry: true, delayMs: this.delayMs, reason: `Retrying after ${this.delayMs}ms (attempt ${context.attempt + 1}/${this.maxAttempts})`, }; } } /** * No retry policy - never retries */ export class NoRetryPolicy { name = "no-retry"; maxAttempts = 1; maxTotalTimeMs = 0; shouldRetry(_context) { return { shouldRetry: false, delayMs: 0, reason: "Retry disabled", }; } } /** * Circuit breaker aware retry policy * Works with circuit breaker state to prevent retries when circuit is open */ export class CircuitBreakerAwarePolicy extends BaseRetryPolicy { name = "circuit-breaker-aware"; innerPolicy; failures = 0; lastFailure = 0; circuitState = "closed"; failureThreshold; resetTimeoutMs; constructor(config) { super(config ?? {}); this.innerPolicy = config?.innerPolicy ?? new ExponentialBackoffPolicy(); this.failureThreshold = config?.failureThreshold ?? 5; this.resetTimeoutMs = config?.resetTimeoutMs ?? 30000; } shouldRetry(context) { // Check circuit state if (this.circuitState === "open") { if (Date.now() - this.lastFailure > this.resetTimeoutMs) { this.circuitState = "half-open"; logger.info(`[${this.name}] Circuit half-open, allowing probe request`); } else { return { shouldRetry: false, delayMs: 0, reason: "Circuit breaker open", }; } } // Delegate to inner policy const decision = this.innerPolicy.shouldRetry(context); // Record result for circuit breaker if (!decision.shouldRetry) { this.recordFailure(); } return decision; } recordFailure() { this.failures++; this.lastFailure = Date.now(); if (this.failures >= this.failureThreshold) { this.circuitState = "open"; logger.warn(`[${this.name}] Circuit opened after ${this.failures} failures`); } } recordSuccess() { if (this.circuitState === "half-open") { logger.info(`[${this.name}] Circuit closed after successful request`); } this.failures = 0; this.circuitState = "closed"; } getCircuitState() { return this.circuitState; } reset() { this.failures = 0; this.circuitState = "closed"; this.lastFailure = 0; } } /** * Retry executor - executes operations with retry policy */ export class RetryExecutor { policy; constructor(policy) { this.policy = policy; } /** * Execute an operation with retry * @param operation - The async operation to execute * @param operationName - Name for logging * @returns The result of the operation * @throws The last error if all retries fail */ async execute(operation, operationName) { const startTime = Date.now(); let lastError; let attempt = 0; while (attempt < this.policy.maxAttempts) { try { const result = await operation(); // Record success if policy supports it if (this.policy instanceof CircuitBreakerAwarePolicy) { this.policy.recordSuccess(); } return result; } catch (error) { lastError = error; const elapsed = Date.now() - startTime; const decision = this.policy.shouldRetry({ attempt, error: lastError, elapsedMs: elapsed, operationName, }); if (!decision.shouldRetry) { logger.warn(`[RetryExecutor] ${operationName}: ${decision.reason}`); break; } logger.debug(`[RetryExecutor] ${operationName}: ${decision.reason}`); await this.delay(decision.delayMs); attempt++; } } throw (lastError ?? new Error(`${operationName} failed after ${attempt} attempts`)); } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } /** * Factory for creating retry policies */ export class RetryPolicyFactory { /** * Create a default policy for production use */ static createDefault() { return new ExponentialBackoffPolicy({ maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, }); } /** * Create an aggressive policy for critical operations */ static createAggressive() { return new ExponentialBackoffPolicy({ maxAttempts: 5, maxTotalTimeMs: 120000, baseDelayMs: 500, maxDelayMs: 60000, }); } /** * Create a conservative policy for non-critical operations */ static createConservative() { return new LinearBackoffPolicy({ maxAttempts: 2, maxTotalTimeMs: 10000, initialDelayMs: 500, delayIncrementMs: 500, }); } /** * Create a circuit breaker aware policy */ static createWithCircuitBreaker(config) { return new CircuitBreakerAwarePolicy({ innerPolicy: RetryPolicyFactory.createDefault(), failureThreshold: config?.failureThreshold ?? 5, resetTimeoutMs: config?.resetTimeoutMs ?? 30000, }); } }