Skip to main content
Glama
CircuitBreaker.ts15.2 kB
/** * Circuit Breaker Pattern Implementation * * Provides fault tolerance for external service calls by preventing * cascading failures and allowing systems to recover gracefully. * * States: * - CLOSED: Normal operation, requests pass through * - OPEN: Failures exceeded threshold, requests fail fast * - HALF_OPEN: Testing if service recovered, limited requests allowed * * @example * ```typescript * const breaker = new CircuitBreaker({ * failureThreshold: 5, * resetTimeout: 30000, * name: 'wordpress-api' * }); * * try { * const result = await breaker.execute(() => apiCall()); * } catch (error) { * if (error instanceof CircuitBreakerOpenError) { * // Circuit is open, use fallback * } * } * ``` */ import { createLogger } from "./logger.js"; /** * Circuit breaker states */ /** * Circuit state values */ export const CircuitState = { CLOSED: "CLOSED", OPEN: "OPEN", HALF_OPEN: "HALF_OPEN", } as const; /** * Circuit state type */ export type CircuitStateType = (typeof CircuitState)[keyof typeof CircuitState]; /** * Circuit breaker configuration options */ export interface CircuitBreakerOptions { /** Name for logging and identification */ name: string; /** Number of failures before opening circuit (default: 5) */ failureThreshold?: number; /** Time in ms before attempting to close circuit (default: 30000) */ resetTimeout?: number; /** Number of successful calls needed to close circuit from half-open (default: 2) */ successThreshold?: number; /** Time window in ms to count failures (default: 60000) */ failureWindow?: number; /** Timeout for individual operations in ms (default: 30000) */ timeout?: number; /** Function to determine if an error should trip the breaker */ isFailure?: (error: Error) => boolean; /** Callback when circuit opens */ onOpen?: (name: string, failures: number) => void; /** Callback when circuit closes */ onClose?: (name: string) => void; /** Callback when circuit enters half-open state */ onHalfOpen?: (name: string) => void; } /** * Circuit breaker statistics */ export interface CircuitBreakerStats { state: CircuitStateType; failures: number; successes: number; lastFailure: Date | null; lastSuccess: Date | null; totalRequests: number; failedRequests: number; successfulRequests: number; rejectedRequests: number; timeInCurrentState: number; } /** * Error thrown when circuit breaker is open */ export class CircuitBreakerOpenError extends Error { constructor( public readonly circuitName: string, public readonly resetTime: number, ) { super(`Circuit breaker '${circuitName}' is open. Retry after ${Math.ceil(resetTime / 1000)}s`); this.name = "CircuitBreakerOpenError"; } } /** * Error thrown when operation times out */ export class CircuitBreakerTimeoutError extends Error { constructor( public readonly circuitName: string, public readonly timeout: number, ) { super(`Circuit breaker '${circuitName}' operation timed out after ${timeout}ms`); this.name = "CircuitBreakerTimeoutError"; } } /** * Failure record for tracking failures within time window */ interface FailureRecord { timestamp: number; error: Error; } /** * Circuit Breaker Implementation */ export class CircuitBreaker { private state: CircuitStateType = CircuitState.CLOSED; private failures: FailureRecord[] = []; private successes: number = 0; private lastStateChange: number = Date.now(); private lastFailure: Date | null = null; private lastSuccess: Date | null = null; // Statistics private totalRequests: number = 0; private failedRequests: number = 0; private successfulRequests: number = 0; private rejectedRequests: number = 0; // Configuration with defaults private readonly name: string; private readonly failureThreshold: number; private readonly resetTimeout: number; private readonly successThreshold: number; private readonly failureWindow: number; private readonly timeout: number; private readonly isFailure: (error: Error) => boolean; private readonly onOpen: ((name: string, failures: number) => void) | undefined; private readonly onClose: ((name: string) => void) | undefined; private readonly onHalfOpen: ((name: string) => void) | undefined; private readonly logger = createLogger("CircuitBreaker"); constructor(options: CircuitBreakerOptions) { this.name = options.name; this.failureThreshold = options.failureThreshold ?? 5; this.resetTimeout = options.resetTimeout ?? 30000; this.successThreshold = options.successThreshold ?? 2; this.failureWindow = options.failureWindow ?? 60000; this.timeout = options.timeout ?? 30000; this.isFailure = options.isFailure ?? this.defaultIsFailure; this.onOpen = options.onOpen; this.onClose = options.onClose; this.onHalfOpen = options.onHalfOpen; this.logger.debug(`Circuit breaker '${this.name}' initialized`, { failureThreshold: this.failureThreshold, resetTimeout: this.resetTimeout, successThreshold: this.successThreshold, }); } /** * Execute an operation through the circuit breaker */ async execute<T>(operation: () => Promise<T>): Promise<T> { this.totalRequests++; // Check if circuit should transition from OPEN to HALF_OPEN if (this.state === CircuitState.OPEN) { if (this.shouldAttemptReset()) { this.transitionTo(CircuitState.HALF_OPEN); } else { this.rejectedRequests++; const remainingTime = this.getRemainingResetTime(); throw new CircuitBreakerOpenError(this.name, remainingTime); } } try { const result = await this.executeWithTimeout(operation); this.onSuccess(); return result; } catch (error) { this.onError(error as Error); throw error; } } /** * Execute operation with timeout */ private async executeWithTimeout<T>(operation: () => Promise<T>): Promise<T> { return new Promise<T>((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new CircuitBreakerTimeoutError(this.name, this.timeout)); }, this.timeout); operation() .then((result) => { clearTimeout(timeoutId); resolve(result); }) .catch((error) => { clearTimeout(timeoutId); reject(error); }); }); } /** * Handle successful operation */ private onSuccess(): void { this.successfulRequests++; this.lastSuccess = new Date(); this.successes++; if (this.state === CircuitState.HALF_OPEN) { if (this.successes >= this.successThreshold) { this.transitionTo(CircuitState.CLOSED); } } // Clear old failures when successful in closed state if (this.state === CircuitState.CLOSED) { this.cleanOldFailures(); } } /** * Handle operation error */ private onError(error: Error): void { // Check if this error should count as a failure if (!this.isFailure(error)) { return; } this.failedRequests++; this.lastFailure = new Date(); if (this.state === CircuitState.HALF_OPEN) { // Any failure in half-open state reopens the circuit this.transitionTo(CircuitState.OPEN); return; } if (this.state === CircuitState.CLOSED) { this.recordFailure(error); // Clean old failures and check threshold this.cleanOldFailures(); if (this.failures.length >= this.failureThreshold) { this.transitionTo(CircuitState.OPEN); } } } /** * Record a failure */ private recordFailure(error: Error): void { this.failures.push({ timestamp: Date.now(), error, }); } /** * Remove failures outside the time window */ private cleanOldFailures(): void { const cutoff = Date.now() - this.failureWindow; this.failures = this.failures.filter((f) => f.timestamp > cutoff); } /** * Check if we should attempt to reset (transition to half-open) */ private shouldAttemptReset(): boolean { return Date.now() - this.lastStateChange >= this.resetTimeout; } /** * Get remaining time until reset attempt */ private getRemainingResetTime(): number { const elapsed = Date.now() - this.lastStateChange; return Math.max(0, this.resetTimeout - elapsed); } /** * Transition to a new state */ private transitionTo(newState: CircuitStateType): void { const oldState = this.state; this.state = newState; this.lastStateChange = Date.now(); this.logger.info(`Circuit '${this.name}' state change: ${oldState} -> ${newState}`); // Reset counters on state change if (newState === CircuitState.CLOSED) { this.failures = []; this.successes = 0; this.onClose?.(this.name); } else if (newState === CircuitState.OPEN) { this.successes = 0; this.onOpen?.(this.name, this.failures.length); } else if (newState === CircuitState.HALF_OPEN) { this.successes = 0; this.onHalfOpen?.(this.name); } } /** * Default failure detection - all errors are failures except timeouts and circuit breaker errors */ private defaultIsFailure(error: Error): boolean { // Don't count circuit breaker's own errors as failures if (error instanceof CircuitBreakerOpenError || error instanceof CircuitBreakerTimeoutError) { return false; } // Count all other errors as failures return true; } /** * Get current circuit breaker statistics */ getStats(): CircuitBreakerStats { this.cleanOldFailures(); return { state: this.state, failures: this.failures.length, successes: this.successes, lastFailure: this.lastFailure, lastSuccess: this.lastSuccess, totalRequests: this.totalRequests, failedRequests: this.failedRequests, successfulRequests: this.successfulRequests, rejectedRequests: this.rejectedRequests, timeInCurrentState: Date.now() - this.lastStateChange, }; } /** * Get current state */ getState(): CircuitStateType { // Check for automatic transition to half-open if (this.state === CircuitState.OPEN && this.shouldAttemptReset()) { this.transitionTo(CircuitState.HALF_OPEN); } return this.state; } /** * Check if circuit is allowing requests */ isAvailable(): boolean { const state = this.getState(); return state === CircuitState.CLOSED || state === CircuitState.HALF_OPEN; } /** * Force circuit to open state (for testing or manual intervention) */ forceOpen(): void { this.logger.warn(`Circuit '${this.name}' forced open`); this.transitionTo(CircuitState.OPEN); } /** * Force circuit to closed state (for testing or manual intervention) */ forceClose(): void { this.logger.warn(`Circuit '${this.name}' forced closed`); this.transitionTo(CircuitState.CLOSED); } /** * Reset circuit breaker to initial state */ reset(): void { this.state = CircuitState.CLOSED; this.failures = []; this.successes = 0; this.lastStateChange = Date.now(); this.totalRequests = 0; this.failedRequests = 0; this.successfulRequests = 0; this.rejectedRequests = 0; this.lastFailure = null; this.lastSuccess = null; this.logger.info(`Circuit '${this.name}' reset`); } } /** * Circuit Breaker Registry * Manages multiple circuit breakers for different services */ export class CircuitBreakerRegistry { private static instance: CircuitBreakerRegistry; private breakers: Map<string, CircuitBreaker> = new Map(); private readonly logger = createLogger("CircuitBreakerRegistry"); private constructor() {} /** * Get singleton instance */ static getInstance(): CircuitBreakerRegistry { if (!CircuitBreakerRegistry.instance) { CircuitBreakerRegistry.instance = new CircuitBreakerRegistry(); } return CircuitBreakerRegistry.instance; } /** * Get or create a circuit breaker */ getBreaker(options: CircuitBreakerOptions): CircuitBreaker { let breaker = this.breakers.get(options.name); if (!breaker) { breaker = new CircuitBreaker(options); this.breakers.set(options.name, breaker); this.logger.debug(`Created circuit breaker: ${options.name}`); } return breaker; } /** * Get existing circuit breaker by name */ get(name: string): CircuitBreaker | undefined { return this.breakers.get(name); } /** * Get all circuit breaker statistics */ getAllStats(): Record<string, CircuitBreakerStats> { const stats: Record<string, CircuitBreakerStats> = {}; for (const [name, breaker] of this.breakers) { stats[name] = breaker.getStats(); } return stats; } /** * Get health summary of all circuit breakers */ getHealthSummary(): { total: number; closed: number; open: number; halfOpen: number; healthy: boolean; } { let closed = 0; let open = 0; let halfOpen = 0; for (const breaker of this.breakers.values()) { switch (breaker.getState()) { case CircuitState.CLOSED: closed++; break; case CircuitState.OPEN: open++; break; case CircuitState.HALF_OPEN: halfOpen++; break; } } return { total: this.breakers.size, closed, open, halfOpen, healthy: open === 0, }; } /** * Reset all circuit breakers */ resetAll(): void { for (const breaker of this.breakers.values()) { breaker.reset(); } this.logger.info("All circuit breakers reset"); } /** * Remove a circuit breaker */ remove(name: string): boolean { return this.breakers.delete(name); } /** * Clear all circuit breakers */ clear(): void { this.breakers.clear(); this.logger.info("All circuit breakers cleared"); } } /** * Create a circuit breaker with default WordPress API settings */ export function createWordPressCircuitBreaker( siteId: string, options?: Partial<CircuitBreakerOptions>, ): CircuitBreaker { return CircuitBreakerRegistry.getInstance().getBreaker({ name: `wordpress-api-${siteId}`, failureThreshold: 5, resetTimeout: 30000, successThreshold: 2, failureWindow: 60000, timeout: 30000, isFailure: (error: Error) => { // Don't trip on client errors (4xx) except rate limiting const message = error.message.toLowerCase(); if (message.includes("401") || message.includes("403")) { return false; // Auth errors shouldn't trip the breaker } if (message.includes("404")) { return false; // Not found is a valid response } if (message.includes("429")) { return true; // Rate limiting should trip the breaker } // Server errors (5xx) and network errors should trip return ( message.includes("5") || message.includes("timeout") || message.includes("network") || message.includes("econnrefused") || message.includes("econnreset") ); }, ...options, }); }

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/docdyhr/mcp-wordpress'

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