/** * Circuit breaker states */ export enum CircuitState { /** Circuit is closed, requests flow normally */ CLOSED = 'closed', /** Circuit is open, requests are rejected immediately */ OPEN = 'open', /** Circuit is half-open, testing if service recovered */ HALF_OPEN = 'half_open', } /** * Circuit breaker configuration */ export interface CircuitBreakerConfig { /** Number of failures before opening circuit */ failureThreshold: number; /** Number of successes in half-open state before closing */ successThreshold: number; /** Time in milliseconds circuit stays open before attempting half-open */ timeout: number; /** Time window in milliseconds for counting failures */ rollingWindow: number; /** Whether circuit breaker is enabled */ enabled: boolean; } /** * Default circuit breaker configuration */ export const DEFAULT_CIRCUIT_BREAKER_CONFIG: CircuitBreakerConfig = { failureThreshold: 5, successThreshold: 2, timeout: 60000, // 1 minute rollingWindow: 10000, // 10 seconds enabled: true, }; /** * Circuit breaker error thrown when circuit is open */ export class CircuitBreakerOpenError extends Error { constructor( public readonly circuitName: string, public readonly nextAttemptTime: Date ) { super( `Circuit breaker "${circuitName}" is OPEN. Next attempt at ${nextAttemptTime.toISOString()}` ); this.name = 'CircuitBreakerOpenError'; } } /** * Failure record for tracking */ interface FailureRecord { timestamp: number; error: Error; } /** * Circuit breaker for preventing cascading failures * * @example * ```typescript * const breaker = new CircuitBreaker('elasticsearch', { * failureThreshold: 5, * timeout: 60000, * }); * * try { * const result = await breaker.execute(async () => { * return await someElasticsearchOperation(); * }); * } catch (error) { * if (error instanceof CircuitBreakerOpenError) { * // Circuit is open, handle gracefully * } * } * ``` */ export class CircuitBreaker { private config: CircuitBreakerConfig; private state: CircuitState = CircuitState.CLOSED; private failures: FailureRecord[] = []; private successCount = 0; private openedAt?: number; private nextAttemptTime?: number; constructor( private name: string, config: Partial = {} ) { this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config }; } /** * Execute an operation through the circuit breaker */ async execute(operation: () => Promise): Promise { if (!this.config.enabled) { return operation(); } // Check circuit state this.updateState(); if (this.state === CircuitState.OPEN) { const nextAttempt = this.nextAttemptTime ? new Date(this.nextAttemptTime) : new Date(Date.now() + this.config.timeout); throw new CircuitBreakerOpenError(this.name, nextAttempt); } try { const result = await operation(); this.onSuccess(); return result; } catch (error) { this.onFailure(error as Error); throw error; } } /** * Handle successful operation */ private onSuccess(): void { this.removeOldFailures(); if (this.state === CircuitState.HALF_OPEN) { this.successCount++; if (this.successCount >= this.config.successThreshold) { this.transitionTo(CircuitState.CLOSED); } } } /** * Handle failed operation */ private onFailure(error: Error): void { this.failures.push({ timestamp: Date.now(), error, }); this.removeOldFailures(); if (this.state === CircuitState.HALF_OPEN) { // Any failure in half-open state opens the circuit immediately this.transitionTo(CircuitState.OPEN); } else if (this.state === CircuitState.CLOSED) { // Check if we've exceeded failure threshold if (this.failures.length >= this.config.failureThreshold) { this.transitionTo(CircuitState.OPEN); } } } /** * Update circuit state based on time */ private updateState(): void { if (this.state === CircuitState.OPEN && this.nextAttemptTime) { if (Date.now() >= this.nextAttemptTime) { this.transitionTo(CircuitState.HALF_OPEN); } } } /** * Transition to a new state */ private transitionTo(newState: CircuitState): void { const previousState = this.state; this.state = newState; switch (newState) { case CircuitState.OPEN: this.openedAt = Date.now(); this.nextAttemptTime = Date.now() + this.config.timeout; this.successCount = 0; break; case CircuitState.HALF_OPEN: this.successCount = 0; break; case CircuitState.CLOSED: this.failures = []; this.successCount = 0; this.openedAt = undefined; this.nextAttemptTime = undefined; break; } if (previousState !== newState) { this.onStateChange(previousState, newState); } } /** * Remove failures outside the rolling window */ private removeOldFailures(): void { const cutoff = Date.now() - this.config.rollingWindow; this.failures = this.failures.filter((f) => f.timestamp >= cutoff); } /** * Callback when state changes (can be overridden) */ protected onStateChange(from: CircuitState, to: CircuitState): void { // Override in subclass or use getState() to monitor console.log(`Circuit breaker "${this.name}" transitioned from ${from} to ${to}`); } /** * Get current circuit state */ getState(): CircuitState { this.updateState(); return this.state; } /** * Get circuit statistics */ getStats(): { state: CircuitState; failureCount: number; successCount: number; openedAt?: Date; nextAttemptTime?: Date; } { this.removeOldFailures(); this.updateState(); return { state: this.state, failureCount: this.failures.length, successCount: this.successCount, ...(this.openedAt && { openedAt: new Date(this.openedAt) }), ...(this.nextAttemptTime && { nextAttemptTime: new Date(this.nextAttemptTime) }), }; } /** * Manually open the circuit */ open(): void { this.transitionTo(CircuitState.OPEN); } /** * Manually close the circuit */ close(): void { this.transitionTo(CircuitState.CLOSED); } /** * Reset the circuit breaker */ reset(): void { this.failures = []; this.successCount = 0; this.openedAt = undefined; this.nextAttemptTime = undefined; this.state = CircuitState.CLOSED; } /** * Check if circuit is open */ isOpen(): boolean { this.updateState(); return this.state === CircuitState.OPEN; } /** * Check if circuit is closed */ isClosed(): boolean { this.updateState(); return this.state === CircuitState.CLOSED; } /** * Check if circuit is half-open */ isHalfOpen(): boolean { this.updateState(); return this.state === CircuitState.HALF_OPEN; } }