Skip to main content
Glama
circuit-breaker.ts11 kB
/** * @fileoverview Circuit breaker implementation for fault tolerance * This module implements the circuit breaker pattern to prevent cascade failures. */ import { createLogger } from '../logging/logger.js'; const logger = createLogger('CircuitBreaker'); /** * Circuit breaker states * @enum * @public */ export enum CircuitState { /** Normal operation - requests pass through */ CLOSED = 'closed', /** Circuit is open - requests fail immediately */ OPEN = 'open', /** Testing if service has recovered */ HALF_OPEN = 'half-open', } /** * Circuit breaker configuration * @interface * @public */ export interface CircuitBreakerConfig { /** Number of failures before opening circuit */ failureThreshold: number; /** Time window for counting failures (ms) */ failureWindow: number; /** Time to wait before attempting recovery (ms) */ recoveryTimeout: number; /** Number of successful requests needed to close from half-open */ successThreshold: number; /** Maximum number of test requests in half-open state */ halfOpenMaxAttempts: number; } /** * Circuit breaker statistics * @interface * @public */ export interface CircuitBreakerStats { /** Current circuit state */ state: CircuitState; /** Number of failures in current window */ failureCount: number; /** Number of successful requests */ successCount: number; /** Total requests processed */ totalRequests: number; /** Time when circuit was last opened */ lastOpenTime?: number; /** Time when circuit was last closed */ lastCloseTime?: number; /** Success rate percentage */ successRate: number; } /** * Default circuit breaker configuration from environment * @private */ const DEFAULT_CONFIG: CircuitBreakerConfig = { failureThreshold: parseInt(process.env.CIRCUIT_BREAKER_THRESHOLD || '5', 10), failureWindow: 60000, // 1 minute recoveryTimeout: parseInt(process.env.CIRCUIT_BREAKER_TIMEOUT_MS || '30000', 10), successThreshold: 3, halfOpenMaxAttempts: 5, }; /** * Circuit breaker implementation * @class * @public */ export class CircuitBreaker { private state: CircuitState = CircuitState.CLOSED; private failures: number[] = []; private successes: number[] = []; private lastStateChange: number = Date.now(); private halfOpenAttempts = 0; private totalRequests = 0; private readonly config: CircuitBreakerConfig; private readonly name: string; /** * Creates a new circuit breaker instance * @param name The name of this circuit breaker (for logging) * @param config Optional configuration overrides */ constructor(name: string, config: Partial<CircuitBreakerConfig> = {}) { this.name = name; this.config = { ...DEFAULT_CONFIG, ...config }; logger.debug('Circuit breaker created', { name, config: this.config }); } /** * Check if a request should be allowed through * @returns True if the request can proceed * @public */ canRequest(): boolean { this.cleanupOldEntries(); switch (this.state) { case CircuitState.CLOSED: return true; case CircuitState.OPEN: { // Check if it's time to transition to half-open const timeSinceOpen = Date.now() - this.lastStateChange; if (timeSinceOpen >= this.config.recoveryTimeout) { this.transitionTo(CircuitState.HALF_OPEN); this.halfOpenAttempts = 1; // Count this request as the first attempt return true; } logger.debug('Circuit breaker is open, rejecting request', { name: this.name, timeSinceOpen, recoveryTimeout: this.config.recoveryTimeout, }); return false; } case CircuitState.HALF_OPEN: // Allow limited test requests if (this.halfOpenAttempts < this.config.halfOpenMaxAttempts) { this.halfOpenAttempts++; return true; } logger.debug('Circuit breaker half-open limit reached', { name: this.name, attempts: this.halfOpenAttempts, max: this.config.halfOpenMaxAttempts, }); return false; default: return false; } } /** * Record a successful request * @public */ recordSuccess(): void { this.totalRequests++; this.successes.push(Date.now()); this.cleanupOldEntries(); switch (this.state) { case CircuitState.HALF_OPEN: { // Check if we have enough successes to close the circuit const recentSuccesses = this.getRecentSuccessCount(); logger.debug('Success in half-open state', { name: this.name, recentSuccesses, threshold: this.config.successThreshold, }); if (recentSuccesses >= this.config.successThreshold) { this.transitionTo(CircuitState.CLOSED); } break; } case CircuitState.CLOSED: // Normal operation, nothing special to do break; case CircuitState.OPEN: // Shouldn't happen, but handle gracefully logger.warn('Success recorded while circuit is open', { name: this.name }); break; default: // Log unexpected state logger.error('Unexpected circuit state in recordSuccess', { name: this.name, state: this.state, }); break; } } /** * Record a failed request * @public */ recordFailure(): void { this.totalRequests++; this.failures.push(Date.now()); this.cleanupOldEntries(); switch (this.state) { case CircuitState.CLOSED: { // Check if we've exceeded the failure threshold const recentFailures = this.getRecentFailureCount(); logger.debug('Failure in closed state', { name: this.name, recentFailures, threshold: this.config.failureThreshold, }); if (recentFailures >= this.config.failureThreshold) { this.transitionTo(CircuitState.OPEN); } break; } case CircuitState.HALF_OPEN: // Any failure in half-open state reopens the circuit logger.debug('Failure in half-open state, reopening circuit', { name: this.name }); this.transitionTo(CircuitState.OPEN); break; case CircuitState.OPEN: // Already open, nothing to do break; default: // Log unexpected state logger.error('Unexpected circuit state in recordFailure', { name: this.name, state: this.state, }); break; } } /** * Get current circuit breaker statistics * @returns The current statistics * @public */ getStats(): CircuitBreakerStats { this.cleanupOldEntries(); const failureCount = this.getRecentFailureCount(); const successCount = this.getRecentSuccessCount(); const successRate = successCount + failureCount > 0 ? (successCount / (successCount + failureCount)) * 100 : 0; const stats: CircuitBreakerStats = { state: this.state, failureCount, successCount, totalRequests: this.totalRequests, successRate: Math.round(successRate * 100) / 100, }; if (this.state === CircuitState.OPEN) { stats.lastOpenTime = this.lastStateChange; } if (this.state === CircuitState.CLOSED) { stats.lastCloseTime = this.lastStateChange; } return stats; } /** * Reset the circuit breaker to closed state * @public */ reset(): void { this.state = CircuitState.CLOSED; this.failures = []; this.successes = []; this.halfOpenAttempts = 0; this.lastStateChange = Date.now(); logger.info('Circuit breaker reset', { name: this.name }); } /** * Get the current state * @returns The current circuit state * @public */ getState(): CircuitState { return this.state; } /** * Transition to a new state * @param newState The new state to transition to * @private */ private transitionTo(newState: CircuitState): void { const oldState = this.state; this.state = newState; this.lastStateChange = Date.now(); if (newState === CircuitState.HALF_OPEN) { this.halfOpenAttempts = 0; } logger.info('Circuit breaker state transition', { name: this.name, from: oldState, to: newState, }); } /** * Clean up old entries outside the time window * @private */ private cleanupOldEntries(): void { const cutoff = Date.now() - this.config.failureWindow; this.failures = this.failures.filter((time) => time > cutoff); this.successes = this.successes.filter((time) => time > cutoff); } /** * Get the count of recent failures within the time window * @returns The number of recent failures * @private */ private getRecentFailureCount(): number { const cutoff = Date.now() - this.config.failureWindow; return this.failures.filter((time) => time > cutoff).length; } /** * Get the count of recent successes within the time window * @returns The number of recent successes * @private */ private getRecentSuccessCount(): number { const cutoff = Date.now() - this.config.failureWindow; return this.successes.filter((time) => time > cutoff).length; } } /** * Circuit breaker manager for managing multiple circuit breakers * @class * @public */ export class CircuitBreakerManager { private static instance: CircuitBreakerManager; private breakers: Map<string, CircuitBreaker> = new Map(); /** * Get the singleton instance * @returns The circuit breaker manager instance * @public */ static getInstance(): CircuitBreakerManager { if (!CircuitBreakerManager.instance) { CircuitBreakerManager.instance = new CircuitBreakerManager(); } return CircuitBreakerManager.instance; } /** * Get or create a circuit breaker for an endpoint * @param endpoint The endpoint name * @param config Optional configuration overrides * @returns The circuit breaker instance * @public */ getBreaker(endpoint: string, config?: Partial<CircuitBreakerConfig>): CircuitBreaker { let breaker = this.breakers.get(endpoint); if (!breaker) { breaker = new CircuitBreaker(endpoint, config); this.breakers.set(endpoint, breaker); } return breaker; } /** * Get statistics for all circuit breakers * @returns Map of endpoint to statistics * @public */ getAllStats(): Map<string, CircuitBreakerStats> { const stats = new Map<string, CircuitBreakerStats>(); for (const [endpoint, breaker] of this.breakers) { stats.set(endpoint, breaker.getStats()); } return stats; } /** * Reset all circuit breakers * @public */ resetAll(): void { for (const breaker of this.breakers.values()) { breaker.reset(); } logger.info('All circuit breakers reset'); } /** * Clear all circuit breakers (for testing) * @public */ clear(): void { this.breakers.clear(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server