@beethovn/circuit-breaker
v1.0.0
Published
Circuit breaker pattern implementation with failure tracking and automatic recovery
Maintainers
Readme
@beethovn/circuit-breaker
Circuit breaker pattern implementation with retry policies and failure tracking for the Beethovn monorepo.
Features
- Circuit Breaker Pattern: Prevent cascading failures with automatic circuit opening/closing
- State Machine: CLOSED → OPEN → HALF_OPEN state transitions
- Failure Tracking: Rolling window-based failure rate calculation
- Retry Policies: Exponential backoff with multiple jitter strategies
- Error Filtering: Configurable error filtering for fine-grained control
- Event Emission: Listen to state change events
- Resilient Executor: Combine circuit breaker and retry in one wrapper
Installation
pnpm add @beethovn/circuit-breakerQuick Start
Basic Circuit Breaker
import { CircuitBreaker } from '@beethovn/circuit-breaker';
const breaker = new CircuitBreaker({
name: 'payment-api',
failureThreshold: 5, // Open after 5 failures
successThreshold: 2, // Close after 2 successes in half-open
timeout: 60000, // Try half-open after 60s
rollingWindowSize: 10000, // 10s rolling window
minimumRequests: 5, // Minimum requests before evaluation
});
try {
const result = await breaker.execute(() => paymentApi.charge(amount));
console.log('Success:', result);
} catch (error) {
console.error('Failed:', error);
}Retry Policy
import { RetryPolicy, RetryPolicyPresets } from '@beethovn/circuit-breaker';
//Use preset
const policy = RetryPolicyPresets.conservative();
// Or custom configuration
const customPolicy = new RetryPolicy({
maxAttempts: 3,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
useJitter: true,
});
const result = await policy.execute(() => fetchData());Resilient Executor (Combined)
import { ResilientExecutor } from '@beethovn/circuit-breaker';
const executor = new ResilientExecutor({
circuitBreaker: {
name: 'external-api',
failureThreshold: 5,
timeout: 60000,
},
retry: {
maxAttempts: 3,
initialDelay: 1000,
},
});
const result = await executor.execute(() => externalApi.call());Circuit Breaker Pattern
States
CLOSED (Normal operation)
↓ (failures ≥ threshold)
OPEN (Reject immediately)
↓ (after timeout)
HALF_OPEN (Test recovery)
↓ (successes ≥ threshold) ↓ (any failure)
CLOSED OPENState Transitions
| From | To | Trigger | |------|------------|---------| | CLOSED | OPEN | Failure threshold exceeded | | OPEN | HALF_OPEN | Timeout period elapsed | | HALF_OPEN | CLOSED | Success threshold met | | HALF_OPEN | OPEN | Any failure occurs |
Usage Examples
Monitor State Changes
const breaker = new CircuitBreaker({
name: 'database',
failureThreshold: 3,
timeout: 30000,
});
breaker.onStateChange((event) => {
console.log(`Circuit ${event.name}: ${event.from} → ${event.to}`);
console.log('Stats:', event.stats);
// Alert if circuit opened
if (event.to === CircuitState.OPEN) {
alerting.sendAlert(`Circuit ${event.name} opened!`);
}
});Custom Error Filtering
const breaker = new CircuitBreaker({
name: 'api',
failureThreshold: 5,
errorFilter: (error) => {
// Only count 5xx errors, ignore 4xx
if (error instanceof HttpError) {
return error.status >= 500;
}
return true;
},
});Retry with Custom Filter
const policy = new RetryPolicy({
maxAttempts: 3,
retryableErrorFilter: (error) => {
// Don't retry on 4xx errors
if (error instanceof HttpError) {
return error.status >= 500;
}
return true;
},
});Get Circuit Statistics
const stats = breaker.getStats();
console.log({
state: stats.state,
totalSuccesses: stats.totalSuccesses,
totalFailures: stats.totalFailures,
totalRejections: stats.totalRejections,
failureRate: stats.failureRate,
timeUntilRetry: stats.timeUntilRetry,
});Jitter Strategies
import { JitterStrategy } from '@beethovn/circuit-breaker';
const policy = new RetryPolicy({ maxAttempts: 3 });
// Full jitter: random(0, delay)
policy.setJitterStrategy(JitterStrategy.FULL);
// Equal jitter: delay/2 + random(0, delay/2)
policy.setJitterStrategy(JitterStrategy.EQUAL);
// No jitter
policy.setJitterStrategy(JitterStrategy.NONE);Retry Presets
// Conservative: 3 attempts, 1s initial, 10s max
const conservative = RetryPolicyPresets.conservative();
// Aggressive: 5 attempts, 500ms initial, 5s max
const aggressive = RetryPolicyPresets.aggressive();
// Patient: 4 attempts, 2s initial, 30s max
const patient = RetryPolicyPresets.patient();
// Quick: 2 attempts, 100ms initial, 1s max
const quick = RetryPolicyPresets.quick();Create Resilient Function
import { createResilient } from '@beethovn/circuit-breaker';
const resilientFetch = createResilient(
() => fetch(url),
{
circuitBreaker: {
name: 'fetch-api',
failureThreshold: 5,
timeout: 60000,
},
retry: {
maxAttempts: 3,
initialDelay: 1000,
},
}
);
// Use like a normal function
const data = await resilientFetch();API Reference
CircuitBreaker
class CircuitBreaker {
constructor(config: CircuitBreakerConfig);
async execute<T>(fn: () => Promise<T>, options?: ExecuteOptions): Promise<T>;
getStats(): CircuitBreakerStats;
getState(): CircuitState;
isOpen(): boolean;
reset(): void;
onStateChange(listener: (event: CircuitStateChangeEvent) => void): void;
removeStateChangeListener(listener: (event: CircuitStateChangeEvent) => void): void;
}CircuitBreakerConfig
interface CircuitBreakerConfig {
name: string;
failureThreshold: number; // default: 5
successThreshold: number; // default: 2
timeout: number; // default: 60000 (60s)
rollingWindowSize: number; // default: 10000 (10s)
minimumRequests: number; // default: 5
errorFilter?: (error: unknown) => boolean;
enableLogging?: boolean; // default: true
}RetryPolicy
class RetryPolicy {
constructor(config?: Partial<RetryPolicyConfig>);
async execute<T>(fn: () => Promise<T>): Promise<T>;
setJitterStrategy(strategy: JitterStrategy): void;
getConfig(): Required<RetryPolicyConfig>;
}RetryPolicyConfig
interface RetryPolicyConfig {
maxAttempts: number; // default: 3
initialDelay: number; // default: 1000 (1s)
maxDelay: number; // default: 30000 (30s)
backoffMultiplier: number; // default: 2
useJitter: boolean; // default: true
retryableErrorFilter?: (error: unknown) => boolean;
}ResilientExecutor
class ResilientExecutor {
constructor(config: ResilienceConfig);
async execute<T>(fn: () => Promise<T>): Promise<T>;
getCircuitBreaker(): CircuitBreaker;
getRetryPolicy(): RetryPolicy | undefined;
}Integration with Other Packages
With @beethovn/errors
import { ErrorFactory } from '@beethovn/errors';
import { CircuitBreaker } from '@beethovn/circuit-breaker';
const breaker = new CircuitBreaker({ name: 'api', failureThreshold: 5 });
try {
await breaker.execute(() => apiCall());
} catch (error: unknown) {
const err = ErrorFactory.fromUnknown(error);
logger.error('Circuit breaker execution failed', err);
}With @beethovn/logging
import { Logger } from '@beethovn/logging';
import { CircuitBreaker, CircuitState } from '@beethovn/circuit-breaker';
const logger = new Logger({ service: 'payment-service' });
const breaker = new CircuitBreaker({ name: 'payment-api', failureThreshold: 5 });
breaker.onStateChange((event) => {
if (event.to === CircuitState.OPEN) {
logger.error('Circuit breaker opened', {
circuit: event.name,
stats: event.stats,
});
} else if (event.to === CircuitState.CLOSED) {
logger.info('Circuit breaker closed', {
circuit: event.name,
});
}
});Best Practices
1. Choose Appropriate Thresholds
// ✅ Good: Balanced thresholds
const breaker = new CircuitBreaker({
name: 'external-api',
failureThreshold: 5, // Open after 5 failures
successThreshold: 2, // Close after 2 successes
timeout: 60000, // Wait 60s before retry
minimumRequests: 5, // Need 5 requests min
});
// ❌ Bad: Too sensitive
const badBreaker = new CircuitBreaker({
name: 'api',
failureThreshold: 1, // Opens after single failure
successThreshold: 10, // Needs 10 successes
});2. Use Error Filters
// ✅ Good: Only count real failures
const breaker = new CircuitBreaker({
name: 'api',
failureThreshold: 5,
errorFilter: (error) => {
// Don't count validation errors (4xx)
if (error instanceof ValidationError) return false;
if (error instanceof NotFoundError) return false;
return true;
},
});3. Monitor State Changes
// ✅ Good: Monitor and alert
breaker.onStateChange((event) => {
metrics.record(`circuit.${event.name}.state`, event.to);
if (event.to === CircuitState.OPEN) {
alerting.critical(`Circuit ${event.name} opened`);
}
});4. Combine with Retry Wisely
// ✅ Good: Circuit breaker wraps retry
const executor = new ResilientExecutor({
circuitBreaker: {
name: 'api',
failureThreshold: 5, // Circuit level
},
retry: {
maxAttempts: 2, // Few retries
initialDelay: 100, // Quick retries
},
});
// ❌ Bad: Too many retries can trigger circuit
const badExecutor = new ResilientExecutor({
circuitBreaker: {
failureThreshold: 3,
},
retry: {
maxAttempts: 10, // Will trigger circuit quickly
},
});5. Use Jitter in Production
// ✅ Good: Jitter prevents thundering herd
const policy = new RetryPolicy({
maxAttempts: 3,
useJitter: true, // Randomize delays
});
// ❌ Bad: No jitter in distributed system
const badPolicy = new RetryPolicy({
maxAttempts: 3,
useJitter: false, // All clients retry at same time
});Exponential Backoff Calculation
Delay Formula
baseDelay = initialDelay × (backoffMultiplier ^ (attempt - 1))
finalDelay = min(baseDelay, maxDelay)Example (initialDelay=1000, multiplier=2, maxDelay=30000)
| Attempt | Base Delay | With Jitter (Full) | |---------|------------|-------------------| | 1 | 1000ms | random(0, 1000) | | 2 | 2000ms | random(0, 2000) | | 3 | 4000ms | random(0, 4000) | | 4 | 8000ms | random(0, 8000) | | 5 | 16000ms | random(0, 16000) | | 6 | 30000ms (capped) | random(0, 30000) |
Error Handling
import { CircuitBreakerOpenError } from '@beethovn/circuit-breaker';
try {
await breaker.execute(() => operation());
} catch (error) {
if (error instanceof CircuitBreakerOpenError) {
// Circuit is open, service is down
console.log('Service unavailable, try again in:', error.metadata?.timeUntilRetry);
} else {
// Operation failed
console.error('Operation error:', error);
}
}Performance Considerations
- Rolling Window: Old requests are cleaned automatically, minimal memory footprint
- State Checks: O(1) complexity for state checks
- Failure Tracking: O(n) where n = requests in window (typically small)
- Logging: Disable with
enableLogging: falsefor high-throughput scenarios
Testing
import { describe, it, expect, vi } from 'vitest';
import { CircuitBreaker, CircuitState } from '@beethovn/circuit-breaker';
describe('MyService', () => {
it('should handle circuit breaker', async () => {
const breaker = new CircuitBreaker({
name: 'test',
failureThreshold: 2,
});
const fn = vi.fn().mockRejectedValue(new Error('fail'));
// Trigger failures
try { await breaker.execute(fn); } catch {}
try { await breaker.execute(fn); } catch {}
expect(breaker.getState()).toBe(CircuitState.OPEN);
});
});Migration Guide
From Manual Retry Logic
// Before: Manual retry
async function fetchWithRetry() {
let attempt = 0;
while (attempt < 3) {
try {
return await fetch(url);
} catch (error) {
attempt++;
if (attempt >= 3) throw error;
await sleep(1000 * Math.pow(2, attempt));
}
}
}
// After: RetryPolicy
const policy = new RetryPolicy({ maxAttempts: 3 });
const result = await policy.execute(() => fetch(url));From Simple Circuit Breaker
// Before: Manual circuit breaker
let failures = 0;
let isOpen = false;
async function callApi() {
if (isOpen) throw new Error('Circuit open');
try {
const result = await api.call();
failures = 0;
return result;
} catch (error) {
failures++;
if (failures >= 5) isOpen = true;
throw error;
}
}
// After: CircuitBreaker
const breaker = new CircuitBreaker({
name: 'api',
failureThreshold: 5,
});
const result = await breaker.execute(() => api.call());TypeScript Support
Full TypeScript support with strict types:
import type {
CircuitBreakerConfig,
CircuitBreakerStats,
CircuitStateChangeEvent,
RetryPolicyConfig,
ResilienceConfig,
} from '@beethovn/circuit-breaker';License
MIT
Package Version: 1.0.0
Dependencies: @beethovn/errors, @beethovn/logging
Node Version: >= 18.0.0
