first commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# `circuit-breaker`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`circuit-breaker.ts` exposes `CircuitBreaker`, a per-worker failure-
|
||||
isolating state machine the base manager uses to short-circuit
|
||||
`sendRequest` calls to workers that have failed `failureThreshold`
|
||||
consecutive times. The breaker transitions `closed → open` after the
|
||||
failure budget is exhausted, `open → half-open` once `resetMs` has
|
||||
elapsed, and either `closed` (on success) or back to `open` (on failure)
|
||||
from half-open.
|
||||
|
||||
Adding a breaker bounds the blast radius of a flaky worker: instead of
|
||||
every request waiting for the full timeout, the breaker rejects them
|
||||
synchronously with `WEB_WORKERS.CIRCUIT_OPEN` and lets the cool-down do
|
||||
its job. The base manager calls `beforeRequest` / `recordSuccess` /
|
||||
`recordFailure` at the right inflection points so consumers get the
|
||||
behaviour for free by adding `circuitBreaker: { ... }` to their
|
||||
`WorkerConfig`.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-state state machine: `closed`, `open`, `half-open`.
|
||||
- Validates configuration (`failureThreshold > 0`, `resetMs >= 0`).
|
||||
- `beforeRequest` throws `WebWorkersError(CIRCUIT_OPEN)` when open and
|
||||
the cool-down hasn't elapsed; transitions to half-open when it has.
|
||||
- `recordSuccess` closes a half-open breaker and resets the consecutive
|
||||
failure counter.
|
||||
- `recordFailure` re-opens a half-open breaker immediately, or opens a
|
||||
closed breaker once the threshold is reached.
|
||||
- Injectable `Clock` for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WorkerManager } from "@suaveplan/web-workers";
|
||||
|
||||
const manager = new WorkerManager({ factory });
|
||||
manager.create({
|
||||
name: "flaky",
|
||||
url: WORKER_URL,
|
||||
circuitBreaker: { failureThreshold: 3, resetMs: 30_000 },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CircuitBreaker } from "@suaveplan/web-workers";
|
||||
|
||||
class GuardedTransport {
|
||||
private readonly breaker = new CircuitBreaker(
|
||||
{ failureThreshold: 5, resetMs: 60_000 },
|
||||
() => Date.now()
|
||||
);
|
||||
async call(op: () => Promise<unknown>): Promise<unknown> {
|
||||
this.breaker.beforeRequest();
|
||||
try {
|
||||
const result = await op();
|
||||
this.breaker.recordSuccess();
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.breaker.recordFailure();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class CircuitBreaker`.
|
||||
- `constructor(config: CircuitBreakerConfig, clock: Clock)`.
|
||||
- `get current: CircuitState`.
|
||||
- `beforeRequest(): void` — throws `WebWorkersError(CIRCUIT_OPEN)` when
|
||||
open and cool-down not elapsed.
|
||||
- `recordSuccess(): void`.
|
||||
- `recordFailure(): void`.
|
||||
- `type CircuitState = "closed" | "open" | "half-open"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The breaker is single-instance per worker — the base manager creates it
|
||||
in `create()` and holds it on the `ManagedEntry`. Failures only count
|
||||
when consecutive, so a successful request mid-failure-window resets the
|
||||
counter. The half-open state allows exactly one probe request through;
|
||||
the next call to `recordFailure` re-opens the breaker AND resets the
|
||||
cool-down timer so the next probe is `resetMs` away. `Clock` is injected
|
||||
(rather than reading `Date.now` directly) so tests can advance time
|
||||
deterministically without `vi.useFakeTimers()` machinery.
|
||||
Reference in New Issue
Block a user