first commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# circuit-breaker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `CircuitBreaker`, a state-machine implementation of the circuit-breaker pattern for protecting async functions against cascading failures. The circuit starts `closed` (normal operation) and opens after a configurable number of consecutive failures, temporarily blocking calls to give the underlying resource time to recover. After a timeout, the circuit enters `half-open` state to probe whether the resource has recovered, and closes again on sufficient consecutive successes.
|
||||
|
||||
State-change events are emitted via an internal `EventBus` from `@suaveplan/events`, enabling observability integrations, metrics, and alerting without polling.
|
||||
|
||||
## Features
|
||||
|
||||
- Three states: `closed`, `open`, `half-open` with deterministic transitions
|
||||
- Configurable `failureThreshold`, `successThreshold`, and `timeout` (open → half-open delay)
|
||||
- Optional `halfOpenProbes` limit to bound probe calls in half-open
|
||||
- Clock injection for deterministic testing and wall-time independence
|
||||
- `stateChange` event emitted on every transition via `EventBus`
|
||||
- `reset()` for forced closure (e.g., operator action or integration tests)
|
||||
- `AbortSignal` passed through to wrapped function
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { CircuitBreaker } from "@suaveplan/retry";
|
||||
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 5,
|
||||
successThreshold: 2,
|
||||
timeout: 30_000, // ms before half-open
|
||||
});
|
||||
|
||||
// Use breaker.call() instead of calling fn directly
|
||||
const data = await breaker.call((signal) => fetchData(url, { signal }));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { CircuitBreaker, withRetry, createExponentialBackoff } from "@suaveplan/retry";
|
||||
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
successThreshold: 2,
|
||||
timeout: 10_000,
|
||||
clock: () => performance.now(),
|
||||
});
|
||||
|
||||
// Subscribe to state changes for metrics
|
||||
breaker.on("stateChange", ({ from, to }) => {
|
||||
metrics.increment("circuit_breaker.transition", { from, to });
|
||||
});
|
||||
|
||||
// Combine with retry — circuit rejects fast while open
|
||||
const delay = createExponentialBackoff({ initial: 200, max: 5_000 });
|
||||
|
||||
const result = await withRetry(
|
||||
(signal) => breaker.call(() => callDownstreamService(signal)),
|
||||
{
|
||||
maxAttempts: 6,
|
||||
delay,
|
||||
shouldRetry: (err) => !(err instanceof CircuitOpenError),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new CircuitBreaker(config)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `config.failureThreshold` | `number` | Consecutive failures to open the circuit. |
|
||||
| `config.successThreshold` | `number` | Consecutive successes in half-open to close. |
|
||||
| `config.timeout` | `number` | Ms before open transitions to half-open. |
|
||||
| `config.halfOpenProbes` | `number?` | Max probes in half-open (default: `successThreshold`). |
|
||||
| `config.clock` | `Clock?` | Time source. Defaults to `Date.now`. |
|
||||
|
||||
### `breaker.state`
|
||||
|
||||
`CircuitState` — one of `"closed"`, `"open"`, `"half-open"`.
|
||||
|
||||
### `breaker.on(event, handler)`
|
||||
|
||||
Subscribe to `"stateChange"` events. Returns an unsubscribe function.
|
||||
|
||||
### `breaker.call(fn, signal?)`
|
||||
|
||||
Execute `fn` through the circuit. Throws `CircuitOpenError` if open.
|
||||
|
||||
### `breaker.reset()`
|
||||
|
||||
Force circuit to `closed` and reset all counters. Emits `stateChange` unless already closed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `checkHalfOpen` method is called lazily at the start of every `call()` — this avoids background timers and keeps the circuit breaker self-contained. The `openedAt` timestamp is set using the injected `clock` so deterministic tests can advance time by mutating the clock reference rather than using real timers. The `EventBus` is internal and not exposed as a public property to prevent external mutation. Listeners receive typed payloads without requiring any casting.
|
||||
Reference in New Issue
Block a user