UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

303 lines 9.86 kB
import { EventEmitter } from 'events'; import { createLogger } from './logger.js'; import { structuredLogger } from './structured-logger.js'; export var CircuitState; (function (CircuitState) { CircuitState["CLOSED"] = "CLOSED"; CircuitState["OPEN"] = "OPEN"; CircuitState["HALF_OPEN"] = "HALF_OPEN"; // Testing if service recovered })(CircuitState || (CircuitState = {})); /** * Circuit Breaker implementation for fault tolerance */ export class CircuitBreaker extends EventEmitter { state = CircuitState.CLOSED; failures = 0; successes = 0; consecutiveSuccesses = 0; consecutiveFailures = 0; lastFailureTime; lastSuccessTime; totalRequests = 0; rejectedRequests = 0; timeoutRequests = 0; fallbackRequests = 0; resetTimer; failureTimestamps = []; options; logger; constructor(options = {}) { super(); this.options = { failureThreshold: options.failureThreshold || 5, failureWindow: options.failureWindow || 60000, // 1 minute resetTimeout: options.resetTimeout || 60000, // 1 minute successThreshold: options.successThreshold || 3, timeout: options.timeout || 30000, // 30 seconds name: options.name || 'CircuitBreaker', errorFilter: options.errorFilter || (() => true), fallback: options.fallback || (() => Promise.reject(new Error('Circuit breaker is OPEN'))) }; this.logger = createLogger({ component: `CircuitBreaker:${this.options.name}` }); } /** * Execute a function with circuit breaker protection */ async execute(fn, correlationId) { this.totalRequests++; const logger = correlationId ? structuredLogger.createCorrelated(`circuit-breaker.${this.options.name}`, correlationId) : this.logger; // Check circuit state if (this.state === CircuitState.OPEN) { this.rejectedRequests++; this.fallbackRequests++; logger.warn('Circuit is OPEN, using fallback', { failures: this.failures, lastFailure: this.lastFailureTime }); this.emit('rejected', { state: this.state, stats: this.getStats() }); // Use fallback if available return this.options.fallback(); } // Create timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timed out after ${this.options.timeout}ms`)); }, this.options.timeout); }); try { // Execute the function with timeout const result = await Promise.race([ fn(), timeoutPromise ]); this.onSuccess(); logger.debug('Operation succeeded', { state: this.state, consecutiveSuccesses: this.consecutiveSuccesses }); return result; } catch (error) { const err = error; // Check if this is a timeout if (err.message.includes('timed out')) { this.timeoutRequests++; } // Check if error should trip the breaker if (this.options.errorFilter(err)) { this.onFailure(); logger.error('Operation failed', err, { state: this.state, consecutiveFailures: this.consecutiveFailures }); } else { logger.debug('Error ignored by filter', { error: err.message }); } throw error; } } /** * Handle successful execution */ onSuccess() { this.successes++; this.consecutiveSuccesses++; this.consecutiveFailures = 0; this.lastSuccessTime = new Date(); if (this.state === CircuitState.HALF_OPEN) { if (this.consecutiveSuccesses >= this.options.successThreshold) { this.close(); } } this.emit('success', { state: this.state, stats: this.getStats() }); } /** * Handle failed execution */ onFailure() { this.failures++; this.consecutiveFailures++; this.consecutiveSuccesses = 0; this.lastFailureTime = new Date(); // Add timestamp to sliding window const now = Date.now(); this.failureTimestamps.push(now); // Remove old timestamps outside the window const cutoff = now - this.options.failureWindow; this.failureTimestamps = this.failureTimestamps.filter(ts => ts > cutoff); // Check if we should open the circuit if (this.state === CircuitState.CLOSED) { if (this.failureTimestamps.length >= this.options.failureThreshold) { this.open(); } } else if (this.state === CircuitState.HALF_OPEN) { // Single failure in half-open state reopens the circuit this.open(); } this.emit('failure', { state: this.state, stats: this.getStats() }); } /** * Open the circuit */ open() { this.state = CircuitState.OPEN; this.logger.warn('Circuit opened', { failures: this.failures, threshold: this.options.failureThreshold, window: this.options.failureWindow }); // Clear any existing timer if (this.resetTimer) { clearTimeout(this.resetTimer); } // Set timer to try half-open this.resetTimer = setTimeout(() => { this.halfOpen(); }, this.options.resetTimeout); this.emit('open', { stats: this.getStats() }); } /** * Move to half-open state */ halfOpen() { this.state = CircuitState.HALF_OPEN; this.consecutiveSuccesses = 0; this.consecutiveFailures = 0; this.logger.info('Circuit half-open, testing recovery'); this.emit('half-open', { stats: this.getStats() }); } /** * Close the circuit */ close() { this.state = CircuitState.CLOSED; this.failures = 0; this.failureTimestamps = []; this.logger.info('Circuit closed, service recovered'); this.emit('close', { stats: this.getStats() }); } /** * Get current statistics */ getStats() { return { state: this.state, failures: this.failures, successes: this.successes, consecutiveSuccesses: this.consecutiveSuccesses, consecutiveFailures: this.consecutiveFailures, lastFailureTime: this.lastFailureTime, lastSuccessTime: this.lastSuccessTime, totalRequests: this.totalRequests, rejectedRequests: this.rejectedRequests, timeoutRequests: this.timeoutRequests, fallbackRequests: this.fallbackRequests }; } /** * Reset the circuit breaker */ reset() { this.state = CircuitState.CLOSED; this.failures = 0; this.successes = 0; this.consecutiveSuccesses = 0; this.consecutiveFailures = 0; this.failureTimestamps = []; this.lastFailureTime = undefined; this.lastSuccessTime = undefined; if (this.resetTimer) { clearTimeout(this.resetTimer); this.resetTimer = undefined; } this.logger.info('Circuit breaker reset'); this.emit('reset', { stats: this.getStats() }); } /** * Force the circuit to open */ forceOpen() { this.open(); } /** * Force the circuit to close */ forceClose() { this.close(); } /** * Check if circuit is available */ isAvailable() { return this.state !== CircuitState.OPEN; } } /** * Factory function to create circuit breakers for different services */ export function createCircuitBreaker(serviceName, options) { return new CircuitBreaker({ name: serviceName, ...options }); } /** * Circuit breaker manager for managing multiple breakers */ export class CircuitBreakerManager { breakers = new Map(); logger = createLogger({ component: 'CircuitBreakerManager' }); /** * Get or create a circuit breaker for a service */ getBreaker(serviceName, options) { if (!this.breakers.has(serviceName)) { const breaker = createCircuitBreaker(serviceName, options); this.breakers.set(serviceName, breaker); // Log circuit breaker events breaker.on('open', ({ stats }) => { this.logger.warn(`Circuit opened for ${serviceName}`, stats); }); breaker.on('close', ({ stats }) => { this.logger.info(`Circuit closed for ${serviceName}`, stats); }); } return this.breakers.get(serviceName); } /** * Get statistics for all breakers */ getAllStats() { const stats = {}; for (const [name, breaker] of this.breakers) { stats[name] = breaker.getStats(); } return stats; } /** * Reset all circuit breakers */ resetAll() { for (const breaker of this.breakers.values()) { breaker.reset(); } } /** * Check overall health */ isHealthy() { for (const breaker of this.breakers.values()) { if (!breaker.isAvailable()) { return false; } } return true; } } // Export singleton manager export const circuitBreakerManager = new CircuitBreakerManager(); //# sourceMappingURL=circuit-breaker.js.map