@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
JavaScript
/**
* 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();