@houmak/minerva-mcp-server
Version:
Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations
150 lines (149 loc) • 4.92 kB
JavaScript
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;
}
}