UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and

374 lines (373 loc) 12.9 kB
/** * MCP Circuit Breaker * Implements circuit breaker pattern for external MCP operations * Provides fault tolerance and prevents cascading failures */ import { EventEmitter } from "events"; import { mcpLogger } from "../utils/logger.js"; /** * MCPCircuitBreaker * Implements circuit breaker pattern for fault tolerance */ export class MCPCircuitBreaker extends EventEmitter { name; state = "closed"; config; callHistory = []; lastFailureTime = 0; halfOpenCalls = 0; lastStateChange = new Date(); // Store the cleanup timer reference for proper cleanup cleanupTimer; constructor(name, config = {}) { super(); this.name = name; // Set default configuration this.config = { failureThreshold: config.failureThreshold ?? 5, resetTimeout: config.resetTimeout ?? 60000, halfOpenMaxCalls: config.halfOpenMaxCalls ?? 3, operationTimeout: config.operationTimeout ?? 30000, minimumCallsBeforeCalculation: config.minimumCallsBeforeCalculation ?? 10, statisticsWindowSize: config.statisticsWindowSize ?? 300000, // 5 minutes }; // Clean up old call records periodically - now storing the timer reference this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000); } /** * Execute an operation with circuit breaker protection */ async execute(operation) { const startTime = Date.now(); try { // Check if circuit is open if (this.state === "open") { if (Date.now() - this.lastFailureTime < this.config.resetTimeout) { throw new Error(`Circuit breaker '${this.name}' is open. Next retry at ${new Date(this.lastFailureTime + this.config.resetTimeout)}`); } // Transition to half-open this.changeState("half-open", "Reset timeout reached"); } // Check half-open call limit if (this.state === "half-open" && this.halfOpenCalls >= this.config.halfOpenMaxCalls) { throw new Error(`Circuit breaker '${this.name}' is half-open but call limit reached`); } // Execute operation with timeout const result = await Promise.race([ operation(), this.timeoutPromise(this.config.operationTimeout), ]); // Record successful call this.recordCall(true, Date.now() - startTime); // Handle half-open success if (this.state === "half-open") { this.halfOpenCalls++; // If enough successful calls in half-open, close the circuit if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) { this.changeState("closed", "Half-open test successful"); } } return result; } catch (error) { // Record failed call const duration = Date.now() - startTime; this.recordCall(false, duration); const errorMessage = error instanceof Error ? error.message : String(error); // Emit failure event this.emit("callFailure", { error: errorMessage, duration, timestamp: new Date(), }); // Handle state transitions on failure if (this.state === "half-open") { // Failure in half-open immediately opens circuit this.changeState("open", `Half-open test failed: ${errorMessage}`); } else if (this.state === "closed") { // Check if we should open the circuit this.checkFailureThreshold(); } throw error; } } /** * Record a call in the history */ recordCall(success, duration) { const now = Date.now(); this.callHistory.push({ timestamp: now, success, duration, }); // Emit success event if (success) { this.emit("callSuccess", { duration, timestamp: new Date(), }); } // Update failure time if (!success) { this.lastFailureTime = now; } } /** * Check if failure threshold is exceeded */ checkFailureThreshold() { const windowStart = Date.now() - this.config.statisticsWindowSize; const windowCalls = this.callHistory.filter((call) => call.timestamp >= windowStart); // Need minimum calls before calculating failure rate if (windowCalls.length < this.config.minimumCallsBeforeCalculation) { return; } const failedCalls = windowCalls.filter((call) => !call.success).length; const failureRate = failedCalls / windowCalls.length; mcpLogger.debug(`[CircuitBreaker:${this.name}] Failure rate: ${(failureRate * 100).toFixed(1)}% (${failedCalls}/${windowCalls.length})`); // Open circuit if failure rate exceeds threshold if (failedCalls >= this.config.failureThreshold) { this.changeState("open", `Failure threshold exceeded: ${failedCalls} failures`); this.emit("circuitOpen", { failureRate, totalCalls: windowCalls.length, timestamp: new Date(), }); } } /** * Change circuit breaker state */ changeState(newState, reason) { const oldState = this.state; this.state = newState; this.lastStateChange = new Date(); // Reset counters based on state if (newState === "half-open") { this.halfOpenCalls = 0; this.emit("circuitHalfOpen", { timestamp: new Date(), }); } else if (newState === "closed") { this.halfOpenCalls = 0; this.emit("circuitClosed", { timestamp: new Date(), }); } mcpLogger.info(`[CircuitBreaker:${this.name}] State changed: ${oldState} -> ${newState} (${reason})`); // Emit state change event this.emit("stateChange", { oldState, newState, reason, timestamp: new Date(), }); } /** * Create a timeout promise */ timeoutPromise(timeout) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Operation timed out after ${timeout}ms`)); }, timeout); }); } /** * Clean up old call records */ cleanupCallHistory() { const cutoffTime = Date.now() - this.config.statisticsWindowSize; const originalLength = this.callHistory.length; this.callHistory = this.callHistory.filter((call) => call.timestamp >= cutoffTime); const removed = originalLength - this.callHistory.length; if (removed > 0) { mcpLogger.debug(`[CircuitBreaker:${this.name}] Cleaned up ${removed} old call records`); } } /** * Get current statistics */ getStats() { const windowStart = Date.now() - this.config.statisticsWindowSize; const windowCalls = this.callHistory.filter((call) => call.timestamp >= windowStart); const successfulCalls = windowCalls.filter((call) => call.success).length; const failedCalls = windowCalls.length - successfulCalls; const failureRate = windowCalls.length > 0 ? failedCalls / windowCalls.length : 0; return { state: this.state, totalCalls: this.callHistory.length, successfulCalls: this.callHistory.filter((call) => call.success).length, failedCalls: this.callHistory.filter((call) => !call.success).length, failureRate, windowCalls: windowCalls.length, lastStateChange: this.lastStateChange, nextRetryTime: this.state === "open" ? new Date(this.lastFailureTime + this.config.resetTimeout) : undefined, halfOpenCalls: this.halfOpenCalls, }; } /** * Manually reset the circuit breaker */ reset() { this.changeState("closed", "Manual reset"); this.callHistory = []; this.lastFailureTime = 0; this.halfOpenCalls = 0; } /** * Force open the circuit breaker */ forceOpen(reason = "Manual force open") { this.changeState("open", reason); this.lastFailureTime = Date.now(); } /** * Get circuit breaker name */ getName() { return this.name; } /** * Check if circuit is open */ isOpen() { return this.state === "open"; } /** * Check if circuit is closed */ isClosed() { return this.state === "closed"; } /** * Check if circuit is half-open */ isHalfOpen() { return this.state === "half-open"; } /** * Destroy the circuit breaker and clean up resources * This method should be called when the circuit breaker is no longer needed * to prevent memory leaks from the cleanup timer */ destroy() { // Clear the interval timer to prevent memory leaks if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; mcpLogger.debug(`[CircuitBreaker:${this.name}] Cleanup timer cleared`); } // Clear any remaining event listeners this.removeAllListeners(); // Clear call history to free memory this.callHistory = []; mcpLogger.debug(`[CircuitBreaker:${this.name}] Destroyed and cleaned up`); } } /** * Circuit breaker manager for multiple circuit breakers */ export class CircuitBreakerManager { breakers = new Map(); /** * Get or create a circuit breaker */ getBreaker(name, config) { if (!this.breakers.has(name)) { const breaker = new MCPCircuitBreaker(name, config); this.breakers.set(name, breaker); mcpLogger.debug(`[CircuitBreakerManager] Created circuit breaker: ${name}`); } return this.breakers.get(name); } /** * Remove a circuit breaker and clean up its resources */ removeBreaker(name) { const breaker = this.breakers.get(name); if (breaker) { // Destroy the breaker to clean up its timer and resources breaker.destroy(); this.breakers.delete(name); mcpLogger.debug(`[CircuitBreakerManager] Removed and cleaned up circuit breaker: ${name}`); return true; } return false; } /** * Get all circuit breaker names */ getBreakerNames() { return Array.from(this.breakers.keys()); } /** * Get statistics for all circuit 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(); } mcpLogger.info("[CircuitBreakerManager] Reset all circuit breakers"); } /** * Get health summary */ getHealthSummary() { let closedBreakers = 0; let openBreakers = 0; let halfOpenBreakers = 0; const unhealthyBreakers = []; for (const [name, breaker] of this.breakers) { const stats = breaker.getStats(); switch (stats.state) { case "closed": closedBreakers++; break; case "open": openBreakers++; unhealthyBreakers.push(name); break; case "half-open": halfOpenBreakers++; break; } } return { totalBreakers: this.breakers.size, closedBreakers, openBreakers, halfOpenBreakers, unhealthyBreakers, }; } /** * Destroy all circuit breakers and clean up their resources * This should be called during application shutdown to prevent memory leaks */ destroyAll() { for (const breaker of this.breakers.values()) { breaker.destroy(); } this.breakers.clear(); mcpLogger.info("[CircuitBreakerManager] Destroyed all circuit breakers"); } } // Global circuit breaker manager instance export const globalCircuitBreakerManager = new CircuitBreakerManager();