@spaik/mcp-server-roi
Version:
MCP server for AI ROI prediction and tracking with Monte Carlo simulations
303 lines • 9.86 kB
JavaScript
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