No description
|
Some checks failed
Build and Publish / build-and-publish (push) Failing after 39s
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| .gitignore | ||
| IMPLEMENTATION.md | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsup.config.ts | ||
@lilith/circuit-breaker
Circuit breaker pattern implementation for resilient service calls.
Features
- State Machine: Automatic transitions between CLOSED → OPEN → HALF-OPEN → CLOSED
- Configurable Thresholds: Failure threshold, success threshold, timeout, volume threshold
- Statistics Tracking: Real-time monitoring of failures, successes, consecutive counts, and state transitions
- Thread-Safe: Atomic state transitions with execution locking for concurrent operations
- Event Callbacks: React to state changes and failures
- Type-Safe: Full TypeScript support with generic types
- Zero Dependencies: No external runtime dependencies
Installation
pnpm add @lilith/circuit-breaker
Usage
Basic Example
import { CircuitBreaker } from '@lilith/circuit-breaker';
const breaker = new CircuitBreaker({
failureThreshold: 5, // Open after 5 consecutive failures
successThreshold: 3, // Close after 3 consecutive successes
timeout: 30000, // Wait 30s before trying half-open
volumeThreshold: 10, // Need 10 requests before evaluating
});
// Execute protected function
try {
const user = await breaker.execute(async () => {
return await userService.fetchUser(id);
});
} catch (error) {
if (error instanceof CircuitBreakerOpenError) {
// Circuit is open, use fallback or cached data
return getCachedUser(id);
}
throw error;
}
Monitoring State Changes
breaker.onStateChange((from, to) => {
console.log(`Circuit breaker: ${from} -> ${to}`);
if (to === 'open') {
alerting.notify('Circuit breaker opened for user service');
}
});
breaker.onFailure((error) => {
console.error('Request failed:', error);
});
Accessing Statistics
const stats = breaker.statistics;
console.log({
state: stats.state,
failures: stats.failures,
successes: stats.successes,
totalRequests: stats.totalRequests,
consecutiveFailures: stats.consecutiveFailures,
consecutiveSuccesses: stats.consecutiveSuccesses,
lastFailure: stats.lastFailure,
lastSuccess: stats.lastSuccess,
lastStateChange: stats.lastStateChange,
});
Manual Control
// Force open (useful for maintenance)
breaker.open();
// Force close (after manual intervention)
breaker.close();
// Transition to half-open (test recovery)
breaker.halfOpen();
Circuit States
CLOSED (Normal Operation)
- Requests pass through normally
- Failures and successes are tracked
- Consecutive failures increment on each failure, reset on success
- Opens when
consecutiveFailures >= failureThresholdANDtotalRequests >= volumeThreshold
OPEN (Failing Fast)
- All requests immediately rejected with
CircuitBreakerOpenError - No requests reach the protected service
- After
timeoutmilliseconds, transitions to HALF-OPEN
HALF-OPEN (Testing Recovery)
- Requests allowed through to test recovery
- Consecutive successes increment on each success
- Any failure immediately reopens circuit (transitions back to OPEN)
- After
consecutiveSuccesses >= successThreshold, transitions to CLOSED
Configuration
interface CircuitBreakerOptions {
failureThreshold: number; // Default: 5
successThreshold: number; // Default: 3
timeout: number; // Default: 30000ms
volumeThreshold: number; // Default: 10
}
Configuration Guidelines
- failureThreshold: Set based on expected error rate (higher for noisy services)
- successThreshold: Lower values allow faster recovery (2-3 recommended)
- timeout: Based on expected recovery time (30s default, increase for slow recovery)
- volumeThreshold: Prevents opening on low traffic (10-20 recommended)
TypeScript Support
Full generic type support:
interface User {
id: string;
name: string;
}
const userBreaker = new CircuitBreaker<User>({
failureThreshold: 3,
});
const user: User = await userBreaker.execute(async () => {
return await api.getUser('123');
});
Thread Safety
The circuit breaker uses an execution lock to ensure atomic state transitions when multiple concurrent requests are made:
// Multiple concurrent requests are safe
const promises = Array.from({ length: 10 }, () =>
breaker.execute(async () => {
return await externalService.getData();
})
);
const results = await Promise.all(promises);
State transitions are serialized to prevent race conditions:
- Only one request can transition the state at a time
- Consecutive failure/success counts are accurate
- Volume threshold checks are consistent
Best Practices
1. Use with External Services
class UserService {
private breaker = new CircuitBreaker({ timeout: 60000 });
async getUser(id: string): Promise<User> {
return this.breaker.execute(() => this.httpClient.get(`/users/${id}`));
}
}
2. Implement Fallbacks
async function getUserWithFallback(id: string): Promise<User> {
try {
return await breaker.execute(() => api.getUser(id));
} catch (error) {
if (error instanceof CircuitBreakerOpenError) {
return cache.get(id) ?? getDefaultUser();
}
throw error;
}
}
3. Monitor in Production
breaker.onStateChange((from, to) => {
metrics.increment('circuit_breaker.state_change', {
from,
to,
service: 'user-service',
});
});
breaker.onFailure((error) => {
logger.error('Circuit breaker failure', {
service: 'user-service',
error: error.message,
stats: breaker.statistics,
});
});
Architecture
The circuit breaker follows the classic pattern:
┌─────────┐
│ CLOSED │ ──[failures >= threshold]──> OPEN
└─────────┘ │
▲ │
│ │
│ [timeout elapsed]
│ │
│ ▼
│ ┌───────────┐
└──[successes >= threshold]── │ HALF-OPEN │
└───────────┘
│
[any failure]
│
▼
OPEN
License
UNLICENSED - Lilith Platform
Registry
Published to: http://forge.black.lan/api/packages/lilith/npm/