@codai/memorai-core
Version:
Simplified advanced memory engine - no tiers, just powerful semantic search with persistence
261 lines (260 loc) • 8.93 kB
JavaScript
/**
* 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();