@rhofkens/mcp-quotes-server-claude-code
Version:
Model Context Protocol (MCP) server for managing and serving quotes
238 lines • 7.21 kB
JavaScript
/**
* Circuit Breaker Pattern Implementation
*
* Provides fault tolerance for external API calls by preventing
* cascading failures and allowing systems to recover
*/
import { APIError, ErrorCode } from './errors.js';
import { logger } from './logger.js';
/**
* Circuit breaker states
*/
export var CircuitState;
(function (CircuitState) {
CircuitState["CLOSED"] = "CLOSED";
CircuitState["OPEN"] = "OPEN";
CircuitState["HALF_OPEN"] = "HALF_OPEN";
})(CircuitState || (CircuitState = {}));
/**
* Circuit breaker implementation
*/
export class CircuitBreaker {
state = CircuitState.CLOSED;
failureCount = 0;
successCount = 0;
lastFailureTime;
lastSuccessTime;
nextAttempt;
config;
stats = {
totalRequests: 0,
rejectedRequests: 0,
fallbacksExecuted: 0,
};
constructor(config = {}) {
this.config = {
failureThreshold: config.failureThreshold || 5,
successThreshold: config.successThreshold || 2,
timeout: config.timeout || 60000, // 1 minute
monitoringPeriod: config.monitoringPeriod || 60000, // 1 minute
fallbackFunction: config.fallbackFunction ||
(() => {
throw new APIError('Circuit breaker is open and no fallback provided', ErrorCode.API_ERROR, 'circuit-breaker');
}),
healthCheckFunction: config.healthCheckFunction || (() => Promise.resolve(true)),
};
}
/**
* Execute a function with circuit breaker protection
*/
execute(fn) {
this.stats.totalRequests++;
// Check if we should reset failure count based on monitoring period
if (this.lastFailureTime && Date.now() - this.lastFailureTime > this.config.monitoringPeriod) {
this.failureCount = 0;
}
switch (this.state) {
case CircuitState.OPEN:
return this.handleOpenState();
case CircuitState.HALF_OPEN:
return this.handleHalfOpenState(fn);
case CircuitState.CLOSED:
default:
return this.handleClosedState(fn);
}
}
/**
* Handle request when circuit is open
*/
async handleOpenState() {
const now = Date.now();
// Check if timeout has passed
if (this.nextAttempt && now >= this.nextAttempt) {
logger.info('Circuit breaker attempting half-open state');
this.state = CircuitState.HALF_OPEN;
// Try health check if configured
try {
const isHealthy = await this.config.healthCheckFunction();
if (isHealthy) {
this.successCount = 0;
logger.info('Health check passed, circuit breaker now half-open');
}
else {
this.open();
throw new Error('Health check failed');
}
}
catch (error) {
this.open();
logger.error('Health check failed', error);
}
}
// Circuit is still open, use fallback
this.stats.rejectedRequests++;
this.stats.fallbacksExecuted++;
logger.warn('Circuit breaker open, using fallback');
return this.config.fallbackFunction();
}
/**
* Handle request when circuit is half-open
*/
async handleHalfOpenState(fn) {
try {
const result = await fn();
this.onSuccess();
// Check if we should close the circuit
if (this.successCount >= this.config.successThreshold) {
this.close();
}
return result;
}
catch (error) {
this.onFailure(error);
throw error;
}
}
/**
* Handle request when circuit is closed
*/
async handleClosedState(fn) {
try {
const result = await fn();
this.onSuccess();
return result;
}
catch (error) {
this.onFailure(error);
// Check if we should open the circuit
if (this.failureCount >= this.config.failureThreshold) {
this.open();
}
throw error;
}
}
/**
* Record successful execution
*/
onSuccess() {
this.failureCount = 0;
this.successCount++;
this.lastSuccessTime = Date.now();
logger.debug('Circuit breaker success recorded', {
state: this.state,
successCount: this.successCount,
});
}
/**
* Record failed execution
*/
onFailure(error) {
this.failureCount++;
this.successCount = 0;
this.lastFailureTime = Date.now();
logger.warn('Circuit breaker failure recorded', {
state: this.state,
failureCount: this.failureCount,
error: error instanceof Error ? error.message : String(error),
});
}
/**
* Open the circuit
*/
open() {
this.state = CircuitState.OPEN;
this.nextAttempt = Date.now() + this.config.timeout;
logger.error('Circuit breaker opened', {
failures: this.failureCount,
nextAttempt: new Date(this.nextAttempt),
});
}
/**
* Close the circuit
*/
close() {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
this.successCount = 0;
this.nextAttempt = undefined;
logger.info('Circuit breaker closed');
}
/**
* Get current state
*/
getState() {
return this.state;
}
/**
* Get circuit breaker statistics
*/
getStats() {
return {
state: this.state,
failures: this.failureCount,
successes: this.successCount,
lastFailureTime: this.lastFailureTime || 0,
lastSuccessTime: this.lastSuccessTime || 0,
totalRequests: this.stats.totalRequests,
rejectedRequests: this.stats.rejectedRequests,
fallbacksExecuted: this.stats.fallbacksExecuted,
};
}
/**
* Reset circuit breaker to initial state
*/
reset() {
this.state = CircuitState.CLOSED;
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = undefined;
this.lastSuccessTime = undefined;
this.nextAttempt = undefined;
this.stats = {
totalRequests: 0,
rejectedRequests: 0,
fallbacksExecuted: 0,
};
logger.info('Circuit breaker reset');
}
/**
* Force circuit to open state (for testing/maintenance)
*/
forceOpen() {
this.open();
}
/**
* Force circuit to closed state (for testing/recovery)
*/
forceClose() {
this.close();
}
}
/**
* Factory function to create configured circuit breakers
*/
export function createCircuitBreaker(name, config) {
logger.info('Creating circuit breaker', { name, config });
return new CircuitBreaker(config);
}
//# sourceMappingURL=circuitBreaker.js.map