UNPKG

@houmak/minerva-mcp-server

Version:

Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations

150 lines (149 loc) 4.92 kB
import { logger } from '../logger.js'; export class CircuitBreaker { state; config; name; constructor(name, config) { this.name = name; this.config = config; this.state = { status: 'closed', failureCount: 0, lastFailureTime: 0, nextAttemptTime: 0 }; } async execute(operation) { if (!this.config.enabled) { return await operation(); } if (this.state.status === 'open') { if (Date.now() < this.state.nextAttemptTime) { throw new Error(`Circuit breaker '${this.name}' is open. Retry after ${new Date(this.state.nextAttemptTime).toISOString()}`); } this.state.status = 'half-open'; logger.info(`Circuit breaker '${this.name}' moved to half-open state`); } try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Operation timeout after ${this.config.timeout}ms`)), this.config.timeout); }); const result = await Promise.race([operation(), timeoutPromise]); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.state.failureCount = 0; this.state.status = 'closed'; logger.info(`Circuit breaker '${this.name}' reset to closed state`); } onFailure() { this.state.failureCount++; this.state.lastFailureTime = Date.now(); if (this.state.failureCount >= this.config.failureThreshold) { this.state.status = 'open'; this.state.nextAttemptTime = Date.now() + this.config.recoveryTimeout; logger.warn(`Circuit breaker '${this.name}' opened after ${this.state.failureCount} failures. Next attempt at ${new Date(this.state.nextAttemptTime).toISOString()}`); } } getState() { return { ...this.state }; } forceOpen() { this.state.status = 'open'; this.state.nextAttemptTime = Date.now() + this.config.recoveryTimeout; logger.warn(`Circuit breaker '${this.name}' forced open`); } forceClose() { this.state.status = 'closed'; this.state.failureCount = 0; logger.info(`Circuit breaker '${this.name}' forced closed`); } isOpen() { return this.state.status === 'open' && Date.now() < this.state.nextAttemptTime; } isHalfOpen() { return this.state.status === 'half-open'; } isClosed() { return this.state.status === 'closed'; } } export class CircuitBreakerManager { breakers = new Map(); configs = new Map(); constructor() { this.loadConfigs(); } loadConfigs() { try { // Configuration par défaut const defaultConfigs = { graph: { failureThreshold: 5, recoveryTimeout: 30000, timeout: 10000, enabled: true }, pnp: { failureThreshold: 3, recoveryTimeout: 60000, timeout: 15000, enabled: true }, cli: { failureThreshold: 2, recoveryTimeout: 45000, timeout: 20000, enabled: true } }; Object.entries(defaultConfigs).forEach(([name, config]) => { this.configs.set(name, config); }); logger.info('Circuit breaker configurations loaded'); } catch (error) { logger.error('Error loading circuit breaker configs:', error); } } getBreaker(name) { if (!this.breakers.has(name)) { const config = this.configs.get(name) || { failureThreshold: 3, recoveryTimeout: 30000, timeout: 10000, enabled: true }; this.breakers.set(name, new CircuitBreaker(name, config)); } return this.breakers.get(name); } async executeWithBreaker(name, operation) { const breaker = this.getBreaker(name); return await breaker.execute(operation); } getStatus() { const status = {}; this.breakers.forEach((breaker, name) => { status[name] = breaker.getState(); }); return status; } resetAll() { this.breakers.forEach(breaker => breaker.forceClose()); logger.info('All circuit breakers reset'); } getConfigs() { const configs = {}; this.configs.forEach((config, name) => { configs[name] = config; }); return configs; } }