/** * Circuit Breaker Pattern Implementation * * Prevents cascading failures by monitoring operation failures * and temporarily blocking requests when failure rate is too high */ export interface CircuitBreakerConfig { /** Failure threshold to open circuit (default: 5) */ failureThreshold: number; /** Success threshold to close circuit (default: 2) */ successThreshold: number; /** Timeout in milliseconds before attempting to recover (default: 60000) */ timeout: number; /** Monitor window in milliseconds (default: 60000) */ monitoringWindow: number; /** Expected failure types that should trigger circuit breaker */ expectedErrors?: Array; /** Unexpected failure types that should not trigger circuit breaker */ ignoredErrors?: Array; } export enum CircuitState { CLOSED = 'closed', // Normal operation OPEN = 'open', // Circuit is open, failing fast HALF_OPEN = 'half-open' // Testing if service recovered } interface CircuitBreakerMetrics { failures: number; successes: number; requests: number; lastFailureTime: number | null; lastSuccessTime: number | null; } export interface CircuitBreakerEvents { stateChange: (newState: CircuitState, oldState: CircuitState) => void; failure: (error: Error) => void; success: () => void; reject: (error: Error) => void; } /** * Circuit Breaker implementation with configurable failure detection */ export class CircuitBreaker { private state: CircuitState = CircuitState.CLOSED; private readonly config: Required; private metrics: CircuitBreakerMetrics = { failures: 0, successes: 0, requests: 0, lastFailureTime: null, lastSuccessTime: null }; private nextAttempt: number = 0; private readonly eventHandlers: Partial = {}; constructor(config: Partial = {}) { this.config = { failureThreshold: config.failureThreshold ?? 5, successThreshold: config.successThreshold ?? 2, timeout: config.timeout ?? 60000, monitoringWindow: config.monitoringWindow ?? 60000, expectedErrors: config.expectedErrors ?? [], ignoredErrors: config.ignoredErrors ?? [] }; } /** * Register event handler */ public on( event: K, handler: CircuitBreakerEvents[K] ): void { this.eventHandlers[event] = handler; } /** * Execute operation with circuit breaker protection */ public async execute(operation: () => Promise): Promise { if (this.state === CircuitState.OPEN) { if (this.shouldAttemptReset()) { this.setState(CircuitState.HALF_OPEN); } else { const error = new Error( `Circuit breaker is OPEN. Next attempt allowed at ${new Date(this.nextAttempt).toISOString()}` ); this.emit('reject', error); throw error; } } this.metrics.requests++; try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(error as Error); throw error; } } /** * Get current circuit state */ public getState(): CircuitState { return this.state; } /** * Get current metrics */ public getMetrics(): Readonly { this.cleanupOldMetrics(); return { ...this.metrics }; } /** * Reset circuit breaker to initial state */ public reset(): void { this.setState(CircuitState.CLOSED); this.metrics = { failures: 0, successes: 0, requests: 0, lastFailureTime: null, lastSuccessTime: null }; this.nextAttempt = 0; } /** * Manually open circuit (for testing or forced maintenance) */ public open(): void { this.setState(CircuitState.OPEN); this.nextAttempt = Date.now() + this.config.timeout; } /** * Check if circuit is currently available for requests */ public isAvailable(): boolean { return this.state === CircuitState.CLOSED || (this.state === CircuitState.HALF_OPEN) || (this.state === CircuitState.OPEN && this.shouldAttemptReset()); } /** * Handle successful operation */ private onSuccess(): void { this.metrics.successes++; this.metrics.lastSuccessTime = Date.now(); this.emit('success'); if (this.state === CircuitState.HALF_OPEN) { if (this.metrics.successes >= this.config.successThreshold) { this.setState(CircuitState.CLOSED); this.resetSuccessCount(); } } } /** * Handle failed operation */ private onFailure(error: Error): void { // Check if this error should be ignored if (this.shouldIgnoreError(error)) { return; } this.metrics.failures++; this.metrics.lastFailureTime = Date.now(); this.emit('failure', error); if (this.state === CircuitState.HALF_OPEN) { // Any failure in half-open state should open the circuit this.setState(CircuitState.OPEN); this.nextAttempt = Date.now() + this.config.timeout; } else if (this.state === CircuitState.CLOSED) { if (this.shouldOpenCircuit()) { this.setState(CircuitState.OPEN); this.nextAttempt = Date.now() + this.config.timeout; } } } /** * Check if circuit should be opened based on failure rate */ private shouldOpenCircuit(): boolean { this.cleanupOldMetrics(); // Need minimum number of requests to determine failure rate if (this.metrics.requests < this.config.failureThreshold) { return false; } const failureRate = this.metrics.failures / this.metrics.requests; const threshold = this.config.failureThreshold / this.metrics.requests; return failureRate >= threshold; } /** * Check if we should attempt to reset from open state */ private shouldAttemptReset(): boolean { return Date.now() >= this.nextAttempt; } /** * Check if error should be ignored by circuit breaker */ private shouldIgnoreError(error: Error): boolean { const errorMessage = error.message; // Check ignored errors first for (const ignoredError of this.config.ignoredErrors) { if (ignoredError instanceof RegExp) { if (ignoredError.test(errorMessage)) { return true; } } else if (errorMessage.includes(ignoredError)) { return true; } } // If expected errors are configured, only count those if (this.config.expectedErrors.length > 0) { for (const expectedError of this.config.expectedErrors) { if (expectedError instanceof RegExp) { if (expectedError.test(errorMessage)) { return false; // This is an expected error, should count } } else if (errorMessage.includes(expectedError)) { return false; // This is an expected error, should count } } return true; // Not in expected errors list, ignore it } return false; // Count all errors by default } /** * Clean up old metrics outside monitoring window */ private cleanupOldMetrics(): void { const now = Date.now(); const windowStart = now - this.config.monitoringWindow; // If last failure/success was outside window, reset counters const lastActivity = Math.max( this.metrics.lastFailureTime ?? 0, this.metrics.lastSuccessTime ?? 0 ); if (lastActivity > 0 && lastActivity < windowStart) { this.metrics.failures = 0; this.metrics.successes = 0; this.metrics.requests = 0; } } /** * Reset success count (called when transitioning from half-open to closed) */ private resetSuccessCount(): void { this.metrics.successes = 0; this.metrics.failures = 0; this.metrics.requests = 0; } /** * Set circuit state and emit event */ private setState(newState: CircuitState): void { if (this.state !== newState) { const oldState = this.state; this.state = newState; this.emit('stateChange', newState, oldState); } } /** * Emit event to registered handlers */ private emit( event: K, ...args: Parameters ): void { const handler = this.eventHandlers[event]; if (handler) { // @ts-expect-error - Dynamic event handling handler(...args); } } } /** * Decorator for automatic circuit breaker protection */ export function circuitBreaker(config?: Partial) { const breaker = new CircuitBreaker(config); return function Promise>( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { return breaker.execute(() => originalMethod.apply(this, args)); }; // Expose circuit breaker instance for monitoring Object.defineProperty(target, `${propertyKey}CircuitBreaker`, { value: breaker, writable: false, enumerable: false }); return descriptor; }; }