/** * @module @dotdo/postgres-shared/circuit-breaker * * Circuit Breaker Pattern for Cross-DO Communication Resilience * * This module implements the circuit breaker pattern to prevent cascading failures * when Durable Objects or external services become unavailable or unresponsive. * It provides a multi-instance tracking implementation that maintains separate * circuit state for each DO instance or service endpoint. * * ## Overview * * The circuit breaker pattern protects your application from repeated failures * by "tripping" after a threshold of consecutive failures, preventing further * requests until the service recovers. * * ## States * * - **CLOSED**: Normal operation, requests flow through to the protected service * - **OPEN**: Failures exceeded threshold, requests fail fast with CircuitOpenError * - **HALF_OPEN**: Testing recovery, limited requests allowed to probe service health * * ## Features * * - Multi-instance state management (track multiple DO instances independently) * - Configurable failure thresholds and time windows * - Exponential backoff for recovery attempts * - Event emission for monitoring and alerting * - Metrics collection integration * - DOCircuitBreakerWrapper for easy DO stub integration * - Passive recovery (state transitions on getState() check) * * ## Quick Start * * @example Basic usage with execute() * ```typescript * import { CircuitBreaker, CircuitOpenError } from '@dotdo/postgres-shared' * * const cb = new CircuitBreaker({ * failureThreshold: 5, * resetTimeoutMs: 30000, * }) * * try { * const result = await cb.execute('tenant-123', async () => { * return await doStub.fetch(request) * }) * } catch (error) { * if (error instanceof CircuitOpenError) { * // Circuit is open - respond with 503 * return new Response('Service temporarily unavailable', { * status: 503, * headers: { 'Retry-After': String(Math.ceil(error.retryAfterMs / 1000)) } * }) * } * throw error * } * ``` * * @example Using DOCircuitBreakerWrapper for DO stubs * ```typescript * import { DOCircuitBreakerWrapper } from '@dotdo/postgres-shared' * * const wrapper = new DOCircuitBreakerWrapper({ * failureThreshold: 3, * resetTimeoutMs: 10000, * fallbackResponse: () => new Response('Service unavailable', { status: 503 }), * }) * * // Wrap a DO stub * const protectedStub = wrapper.wrap(doStub, 'tenant-123') * * // Use the protected stub - circuit breaker handles failures automatically * const response = await protectedStub.fetch(request) * ``` * * @example With metrics collection * ```typescript * import { CircuitBreaker, DefaultMetricsCollector } from '@dotdo/postgres-shared' * * const collector = new DefaultMetricsCollector() * const cb = new CircuitBreaker({ * failureThreshold: 5, * metricsCollector: collector, * }) * * // Later, get metrics summary * const summary = collector.getSummary() * console.log('Open circuits:', summary.openCircuits) * console.log('Total failures:', summary.totalFailures) * ``` * * @see UnifiedCircuitBreaker for a flexible API supporting both single and multi-instance modes * @see packages/shared/src/circuit-breaker-unified.ts for the unified implementation */ /** * Circuit breaker states */ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; /** * Circuit breaker event types for monitoring */ export type CircuitBreakerEvent = { type: 'STATE_CHANGE'; from: CircuitState; to: CircuitState; instanceId: string; timestamp: number; } | { type: 'FAILURE_RECORDED'; instanceId: string; failureCount: number; timestamp: number; } | { type: 'SUCCESS_RECORDED'; instanceId: string; timestamp: number; } | { type: 'REQUEST_REJECTED'; instanceId: string; state: CircuitState; timestamp: number; } | { type: 'HALF_OPEN_TEST'; instanceId: string; success: boolean; timestamp: number; }; /** * Metrics collector interface for aggregating circuit breaker metrics */ export interface CircuitBreakerMetricsCollector { /** Record a state transition */ recordStateTransition(instanceId: string, from: CircuitState, to: CircuitState): void; /** Record a failure */ recordFailure(instanceId: string, failureCount: number): void; /** Record a success */ recordSuccess(instanceId: string): void; /** Record a rejected request */ recordRejection(instanceId: string): void; /** Record current backoff timeout for an instance */ recordBackoffTimeout(instanceId: string, timeoutMs: number): void; /** Get aggregated metrics summary */ getSummary(): CircuitBreakerMetricsSummary; } /** * Summary of aggregated circuit breaker metrics */ export interface CircuitBreakerMetricsSummary { totalInstances: number; openCircuits: number; halfOpenCircuits: number; closedCircuits: number; totalFailures: number; totalSuccesses: number; totalRejections: number; averageBackoffMs: number; } /** * Configuration options for the circuit breaker */ export interface CircuitBreakerConfig { /** Number of consecutive failures before opening the circuit (default: 5) */ failureThreshold?: number; /** Base time in ms to wait before moving from OPEN to HALF_OPEN (default: 30000) */ resetTimeoutMs?: number; /** Number of successful requests in HALF_OPEN to close circuit (default: 3) */ halfOpenSuccessThreshold?: number; /** Time window in ms for counting failures (default: 60000) */ failureWindowMs?: number; /** Optional event handler for circuit breaker events */ onEvent?: (event: CircuitBreakerEvent) => void; /** Optional fallback function when circuit is open */ fallback?: () => Promise; /** Enable exponential backoff for recovery timeouts (default: true) */ enableExponentialBackoff?: boolean; /** Multiplier for exponential backoff (default: 2) */ backoffMultiplier?: number; /** Maximum timeout in ms when using exponential backoff (default: 300000 = 5 minutes) */ maxResetTimeoutMs?: number; /** Optional metrics collector for aggregating circuit breaker metrics */ metricsCollector?: CircuitBreakerMetricsCollector; } /** * Statistics for a circuit breaker instance */ export interface CircuitBreakerStats { state: CircuitState; failureCount: number; successCount: number; lastFailureTime: number | null; lastSuccessTime: number | null; lastStateChangeTime: number; totalRequests: number; totalFailures: number; totalSuccesses: number; totalRejected: number; /** Number of consecutive circuit opens (for exponential backoff tracking) */ consecutiveOpens: number; /** Current reset timeout in ms (may be increased due to exponential backoff) */ currentResetTimeoutMs: number; } export { CircuitOpenError } from './errors.js'; /** * Circuit Breaker implementation * * Tracks failures per DO instance and manages state transitions. * * @example * ```typescript * const cb = new CircuitBreaker({ * failureThreshold: 5, * resetTimeoutMs: 30000, * onEvent: (event) => console.log('Circuit breaker event:', event), * }) * * try { * const result = await cb.execute('tenant-123', async () => { * return await doStub.fetch(request) * }) * } catch (error) { * if (error instanceof CircuitOpenError) { * // Circuit is open, fail fast * return new Response('Service temporarily unavailable', { status: 503 }) * } * throw error * } * ``` */ export declare class CircuitBreaker { private config; private instances; constructor(config?: CircuitBreakerConfig); /** * Get or create state for a DO instance */ private getInstanceState; /** * Calculate the reset timeout with exponential backoff */ private calculateBackoffTimeout; /** * Emit an event if handler is configured */ private emitEvent; /** * Transition to a new state */ private transitionState; /** * Clean up old failures outside the failure window */ private cleanupOldFailures; /** * Get the current state of the circuit for an instance */ getState(instanceId: string): CircuitState; /** * Check if a request is allowed to proceed */ canExecute(instanceId: string): boolean; /** * Record a successful request */ recordSuccess(instanceId: string): void; /** * Record a failed request */ recordFailure(instanceId: string): void; /** * Execute a function with circuit breaker protection */ execute(instanceId: string, fn: () => Promise): Promise; /** * Get statistics for an instance */ getStats(instanceId: string): CircuitBreakerStats; /** * Get statistics for all tracked instances */ getAllStats(): Map; /** * Manually reset the circuit for an instance */ reset(instanceId: string): void; /** * Manually force a circuit to open (useful for maintenance) */ forceOpen(instanceId: string): void; /** * Remove tracking for an instance */ remove(instanceId: string): boolean; /** * Clear all tracked instances */ clear(): void; /** * Get the current configuration */ getConfig(): Required>; /** * Get the current backoff timeout for an instance */ getBackoffTimeout(instanceId: string): number; /** * Check if a request is allowed to proceed (alias for canExecute) * @param instanceId - The instance ID to check (default: 'default') */ isAllowed(instanceId?: string): boolean; /** * Force the circuit to a specific state (for testing/admin) * @param instanceId - The instance ID * @param state - The state to force */ forceState(instanceId: string, state: CircuitState): void; /** * Execute a function with circuit breaker protection and automatic retry * @param instanceId - The instance ID * @param fn - The function to execute * @param options - Retry options */ executeWithRetry(instanceId: string, fn: () => Promise, options?: { maxRetries?: number; delayMs?: number; }): Promise; /** * Sleep helper for retry logic */ private sleep; /** * Cleanup any resources (timers, etc.) * Call this when the circuit breaker is no longer needed. * Note: This only clears timers (if any), not the circuit state. */ destroy(): void; } /** * Default in-memory implementation of CircuitBreakerMetricsCollector * * Provides basic metrics aggregation for monitoring circuit breaker behavior. * * @example * ```typescript * const collector = new DefaultMetricsCollector() * const cb = new CircuitBreaker({ * metricsCollector: collector, * }) * * // Later, get metrics summary * const summary = collector.getSummary() * console.log('Open circuits:', summary.openCircuits) * ``` */ export declare class DefaultMetricsCollector implements CircuitBreakerMetricsCollector { private stateTransitions; private failures; private successes; private rejections; private backoffTimeouts; private currentStates; recordStateTransition(instanceId: string, from: CircuitState, to: CircuitState): void; recordFailure(instanceId: string, _failureCount: number): void; recordSuccess(instanceId: string): void; recordRejection(instanceId: string): void; recordBackoffTimeout(instanceId: string, timeoutMs: number): void; getSummary(): CircuitBreakerMetricsSummary; /** * Get recent state transitions (useful for debugging) */ getRecentTransitions(limit?: number): Array<{ instanceId: string; from: CircuitState; to: CircuitState; timestamp: number; }>; /** * Reset all collected metrics */ reset(): void; } /** * Durable Object stub interface (minimal for wrapping) */ export interface DOStub { fetch(request: Request): Promise; } /** * Configuration for the DO Circuit Breaker Wrapper */ export interface DOCircuitBreakerConfig extends CircuitBreakerConfig { /** Function to get instance ID from a request */ getInstanceId?: (request: Request) => string; /** Function to determine if a response should be treated as a failure */ isFailure?: (response: Response) => boolean; /** Fallback response when circuit is open */ fallbackResponse?: () => Response; } /** * Wrapper for Durable Object stubs with circuit breaker protection * * Wraps DO stub fetch calls with circuit breaker logic. * * @example * ```typescript * const wrapper = new DOCircuitBreakerWrapper({ * failureThreshold: 3, * resetTimeoutMs: 10000, * fallbackResponse: () => new Response('Service unavailable', { status: 503 }), * }) * * // Wrap a DO stub * const protectedStub = wrapper.wrap(doStub, 'tenant-123') * * // Use the protected stub * try { * const response = await protectedStub.fetch(request) * } catch (error) { * if (error instanceof CircuitOpenError) { * // Handle circuit open * } * } * ``` */ export declare class DOCircuitBreakerWrapper { private circuitBreaker; private config; constructor(config?: DOCircuitBreakerConfig); /** * Get instance ID from a request */ private getInstanceId; /** * Check if a response indicates a failure */ private isFailure; /** * Wrap a DO stub with circuit breaker protection */ wrap(stub: DOStub, stubId?: string): DOStub; /** * Execute a fetch with circuit breaker protection (without wrapping) */ fetch(stub: DOStub, request: Request, stubId?: string): Promise; /** * Get the underlying circuit breaker */ getCircuitBreaker(): CircuitBreaker; /** * Get state for a specific instance */ getState(instanceId: string): CircuitState; /** * Get stats for a specific instance */ getStats(instanceId: string): CircuitBreakerStats; /** * Get stats for all tracked instances */ getAllStats(): Map; /** * Reset circuit for an instance */ reset(instanceId: string): void; /** * Force circuit open for an instance */ forceOpen(instanceId: string): void; } /** * Create a new CircuitBreaker instance * * @example * ```typescript * const cb = createCircuitBreaker({ * failureThreshold: 5, * resetTimeoutMs: 30000, * }) * ``` */ export declare function createCircuitBreaker(config?: CircuitBreakerConfig): CircuitBreaker; /** * Create a new DOCircuitBreakerWrapper instance * * @example * ```typescript * const wrapper = createDOCircuitBreakerWrapper({ * failureThreshold: 3, * fallbackResponse: () => new Response('Service unavailable', { status: 503 }), * }) * ``` */ export declare function createDOCircuitBreakerWrapper(config?: DOCircuitBreakerConfig): DOCircuitBreakerWrapper; //# sourceMappingURL=circuit-breaker.d.ts.map