/** * Circuit Breaker pattern implementation * * Prevents cascading failures by temporarily blocking requests * to a failing service, giving it time to recover. * * @example * ```typescript * const breaker = new CircuitBreaker({ failureThreshold: 5 }); * * try { * const result = await breaker.execute(async () => { * return await fetch('https://api.example.com'); * }); * } catch (error) { * if (error instanceof CircuitBreakerError) { * console.log('Circuit is open, service unavailable'); * } * } * ``` */ /** * Circuit breaker states */ export enum CircuitState { /** Circuit is closed - requests pass through normally */ CLOSED = 'CLOSED', /** Circuit is open - all requests are immediately rejected */ OPEN = 'OPEN', /** Circuit is half-open - one test request is allowed */ HALF_OPEN = 'HALF_OPEN', } /** * Configuration options for circuit breaker */ export interface CircuitBreakerOptions { /** Number of consecutive failures before opening circuit (default: 5) */ failureThreshold?: number; /** Time in ms to wait before attempting recovery (default: 60000) */ resetTimeoutMs?: number; /** Time window in ms for monitoring failures (default: 120000) */ monitoringWindowMs?: number; } /** * Custom error thrown when circuit is open */ export class CircuitBreakerError extends Error { constructor( message: string, public readonly state: CircuitState ) { super(message); this.name = 'CircuitBreakerError'; } } /** * Tracks failure events with timestamps */ interface FailureRecord { timestamp: number; error: Error; } /** * Circuit Breaker implementation * * Monitors operation failures and automatically opens the circuit * when failure threshold is exceeded, preventing further attempts * until a reset timeout has elapsed. */ export class CircuitBreaker { private state: CircuitState = CircuitState.CLOSED; private failures: FailureRecord[] = []; private openedAt: number | null = null; private successCount = 0; private failureCount = 0; private readonly failureThreshold: number; private readonly resetTimeoutMs: number; private readonly monitoringWindowMs: number; constructor(options?: CircuitBreakerOptions) { this.failureThreshold = options?.failureThreshold ?? 5; this.resetTimeoutMs = options?.resetTimeoutMs ?? 60000; this.monitoringWindowMs = options?.monitoringWindowMs ?? 120000; console.log('[CircuitBreaker] Initialized', { failureThreshold: this.failureThreshold, resetTimeoutMs: this.resetTimeoutMs, monitoringWindowMs: this.monitoringWindowMs, }); } /** * Get current circuit state */ getState(): CircuitState { return this.state; } /** * Get circuit statistics */ getStats() { const lastFailure = this.failures.length > 0 ? this.failures[this.failures.length - 1] : null; return { state: this.state, failures: this.failures.length, lastFailureTime: lastFailure ? new Date(lastFailure.timestamp) : undefined, stats: { totalRequests: this.successCount + this.failureCount, totalFailures: this.failureCount, totalSuccesses: this.successCount, }, config: { failureThreshold: this.failureThreshold, resetTimeoutMs: this.resetTimeoutMs, monitoringWindowMs: this.monitoringWindowMs, }, }; } /** * Manually reset the circuit to closed state */ reset(): void { console.log('[CircuitBreaker] Manual reset'); this.state = CircuitState.CLOSED; this.failures = []; this.openedAt = null; this.successCount = 0; this.failureCount = 0; } /** * Remove old failure records outside monitoring window */ private cleanupOldFailures(): void { const now = Date.now(); const cutoff = now - this.monitoringWindowMs; this.failures = this.failures.filter( record => record.timestamp > cutoff ); } /** * Check if circuit should transition to half-open state */ private checkResetTimeout(): void { if (this.state === CircuitState.OPEN && this.openedAt !== null) { const now = Date.now(); const elapsed = now - this.openedAt; if (elapsed >= this.resetTimeoutMs) { console.log('[CircuitBreaker] Reset timeout reached, transitioning to HALF_OPEN'); this.state = CircuitState.HALF_OPEN; } } } /** * Record a successful operation */ private onSuccess(): void { this.successCount++; if (this.state === CircuitState.HALF_OPEN) { console.log('[CircuitBreaker] Half-open test succeeded, closing circuit'); this.state = CircuitState.CLOSED; this.failures = []; this.openedAt = null; } } /** * Record a failed operation */ private onFailure(error: Error): void { this.failureCount++; const now = Date.now(); this.failures.push({ timestamp: now, error }); // Clean up old failures this.cleanupOldFailures(); // If in half-open state, one failure reopens the circuit if (this.state === CircuitState.HALF_OPEN) { console.log('[CircuitBreaker] Half-open test failed, reopening circuit'); this.state = CircuitState.OPEN; this.openedAt = now; return; } // Check if we should open the circuit if (this.state === CircuitState.CLOSED) { if (this.failures.length >= this.failureThreshold) { console.log( `[CircuitBreaker] Failure threshold (${this.failureThreshold}) exceeded, opening circuit` ); this.state = CircuitState.OPEN; this.openedAt = now; } } } /** * Execute a function through the circuit breaker * * @param fn - Async function to execute * @returns Promise resolving to the function's result * @throws CircuitBreakerError if circuit is open * @throws Original error if function fails */ async execute(fn: () => Promise): Promise { // Check if we should transition to half-open this.checkResetTimeout(); // If circuit is open, reject immediately if (this.state === CircuitState.OPEN) { const error = new CircuitBreakerError( 'Circuit breaker is open - service unavailable', this.state ); console.log('[CircuitBreaker] Request blocked - circuit is OPEN'); throw error; } try { // Execute the function const result = await fn(); // Record success this.onSuccess(); return result; } catch (error) { // Record failure const err = error instanceof Error ? error : new Error(String(error)); this.onFailure(err); // Log failure console.error( `[CircuitBreaker] Operation failed (${this.failures.length}/${this.failureThreshold} failures):`, err.message ); // Re-throw the original error throw err; } } }