UNPKG

@codai/memorai-core

Version:

Simplified advanced memory engine - no tiers, just powerful semantic search with persistence

261 lines (260 loc) 8.93 kB
/** * Advanced error handling and resilience patterns for Memorai * Includes retry mechanisms, circuit breakers, and graceful degradation */ import { logger } from '../utils/logger.js'; export var CircuitBreakerState; (function (CircuitBreakerState) { CircuitBreakerState["CLOSED"] = "CLOSED"; CircuitBreakerState["OPEN"] = "OPEN"; CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN"; })(CircuitBreakerState || (CircuitBreakerState = {})); export class RetryManager { constructor() { this.defaultOptions = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 10000, exponentialBackoff: true, jitter: true, // Don't specify retryableErrors to allow all errors by default }; } /** * Execute a function with retry logic */ async executeWithRetry(operation, options = {}) { const opts = { ...this.defaultOptions, ...options }; let lastError; for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; // Don't retry on the last attempt if (attempt === opts.maxAttempts) { break; } // Check if error is retryable if (!this.isRetryableError(error, opts.retryableErrors)) { throw error; } // Calculate delay const delay = this.calculateDelay(attempt, opts); // Log retry attempt logger.warn(`Attempt ${attempt} failed, retrying in ${delay}ms:`, error); await this.sleep(delay); } } throw lastError; } isRetryableError(error, retryableErrors) { if (!retryableErrors) { return true; } return retryableErrors.some(retryableError => error.message.includes(retryableError) || error.name.includes(retryableError)); } calculateDelay(attempt, options) { let delay = options.baseDelayMs; if (options.exponentialBackoff) { delay = Math.min(options.baseDelayMs * Math.pow(2, attempt - 1), options.maxDelayMs); } if (options.jitter) { // Add random jitter to prevent thundering herd delay += Math.random() * delay * 0.1; } return Math.floor(delay); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } export class CircuitBreaker { constructor(name, options) { this.name = name; this.options = options; this.state = CircuitBreakerState.CLOSED; this.failures = 0; this.lastFailureTime = 0; this.nextAttemptTime = 0; this.recentCalls = []; } /** * Execute a function with circuit breaker protection */ async execute(operation) { if (this.shouldRejectCall()) { throw new Error(`Circuit breaker '${this.name}' is OPEN - rejecting call`); } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } /** * Get current circuit breaker status */ getStatus() { const successRate = this.calculateSuccessRate(); const result = { state: this.state, failures: this.failures, successRate, }; if (this.nextAttemptTime > 0) { result.nextAttemptTime = new Date(this.nextAttemptTime); } return result; } shouldRejectCall() { const now = Date.now(); switch (this.state) { case CircuitBreakerState.CLOSED: return false; case CircuitBreakerState.OPEN: if (now >= this.nextAttemptTime) { this.state = CircuitBreakerState.HALF_OPEN; return false; } return true; case CircuitBreakerState.HALF_OPEN: return false; default: return false; } } onSuccess() { this.recordCall(true); this.failures = 0; if (this.state === CircuitBreakerState.HALF_OPEN) { this.state = CircuitBreakerState.CLOSED; } } onFailure() { this.recordCall(false); this.failures++; this.lastFailureTime = Date.now(); if (this.shouldOpenCircuit()) { this.state = CircuitBreakerState.OPEN; this.nextAttemptTime = Date.now() + this.options.resetTimeoutMs; } } shouldOpenCircuit() { if (this.state === CircuitBreakerState.HALF_OPEN) { return true; } const recentCallsCount = this.getRecentCallsCount(); if (recentCallsCount < this.options.minimumCalls) { return false; } const successRate = this.calculateSuccessRate(); const failureRate = 1 - successRate; return failureRate >= this.options.failureThreshold / 100; } recordCall(success) { const now = Date.now(); this.recentCalls.push({ timestamp: now, success }); // Clean up old calls outside the monitoring window const cutoff = now - this.options.monitoringWindowMs; this.recentCalls = this.recentCalls.filter(call => call.timestamp >= cutoff); } getRecentCallsCount() { const now = Date.now(); const cutoff = now - this.options.monitoringWindowMs; return this.recentCalls.filter(call => call.timestamp >= cutoff).length; } calculateSuccessRate() { const recentCalls = this.getRecentCallsInWindow(); if (recentCalls.length === 0) { return 1.0; } const successfulCalls = recentCalls.filter(call => call.success).length; return successfulCalls / recentCalls.length; } getRecentCallsInWindow() { const now = Date.now(); const cutoff = now - this.options.monitoringWindowMs; return this.recentCalls.filter(call => call.timestamp >= cutoff); } } export class ResilienceManager { constructor() { this.retryManager = new RetryManager(); this.circuitBreakers = new Map(); } /** * Execute operation with both retry and circuit breaker protection */ async executeResilient(operationName, operation, options = {}) { // Get or create circuit breaker const circuitBreaker = this.getOrCreateCircuitBreaker(operationName, options.circuitBreaker); // Execute with circuit breaker protection and retry logic return circuitBreaker.execute(async () => { return this.retryManager.executeWithRetry(operation, options.retry); }); } /** * Execute with graceful degradation fallback */ async executeWithFallback(operation, fallback, operationName, options) { try { if (operationName && options) { return await this.executeResilient(operationName, operation, options); } else { return await operation(); } } catch (error) { logger.warn(`Primary operation failed, falling back:`, error); return fallback(); } } /** * Get status of all circuit breakers */ getAllCircuitBreakerStatus() { const status = {}; for (const [name, circuitBreaker] of this.circuitBreakers) { status[name] = circuitBreaker.getStatus(); } return status; } /** * Reset a specific circuit breaker */ resetCircuitBreaker(name) { const circuitBreaker = this.circuitBreakers.get(name); if (circuitBreaker) { // Create a new circuit breaker to reset state this.circuitBreakers.delete(name); return true; } return false; } getOrCreateCircuitBreaker(name, options) { let circuitBreaker = this.circuitBreakers.get(name); if (!circuitBreaker) { const defaultOptions = { failureThreshold: 50, // 50% failure rate resetTimeoutMs: 60000, // 1 minute monitoringWindowMs: 60000, // 1 minute window minimumCalls: 10, // Minimum calls before circuit can open }; circuitBreaker = new CircuitBreaker(name, { ...defaultOptions, ...options, }); this.circuitBreakers.set(name, circuitBreaker); } return circuitBreaker; } } // Global singleton instance export const resilienceManager = new ResilienceManager();