first commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# MeshCircuitBreaker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Per-path circuit-breaker FSM that walks `CLOSED → OPEN → HALF_OPEN → CLOSED`. Stops repeated wire-level retries against a known-dead path so retries don't cascade into healthy paths. Each `(localHostId, nextHopHostId)` pair gets its own breaker instance, identified by `pathId`.
|
||||
|
||||
## Features
|
||||
|
||||
- Three-state FSM with deterministic transitions.
|
||||
- Configurable `failureThreshold` (default 5), `successThreshold` (default 3), `openDurationMs` (default 30 s), `halfOpenMaxCalls` (default 3).
|
||||
- Auto-transition `OPEN → HALF_OPEN` after the cooldown elapses (lazy-evaluated on `getState`/`canExecute`).
|
||||
- Throws `INVALID_TRANSITION` on `recordSuccess()` from `OPEN` to surface programmer errors loudly.
|
||||
- Bus-free: state-change listeners installed via `on(handler)` for downstream observability without coupling to a specific bus.
|
||||
- Injectable `clock` for deterministic tests.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MeshCircuitBreaker } from "@suaveplan/network-plugin-circuit-breaker";
|
||||
|
||||
const cb = new MeshCircuitBreaker({ pathId: "meshA:host1→meshB:host2" });
|
||||
|
||||
if (cb.canExecute()) {
|
||||
try {
|
||||
await sendThroughPath();
|
||||
cb.recordSuccess();
|
||||
} catch (err) {
|
||||
cb.recordFailure();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MeshCircuitBreaker } from "@suaveplan/network-plugin-circuit-breaker";
|
||||
|
||||
let now = 0;
|
||||
const cb = new MeshCircuitBreaker({
|
||||
pathId: "p",
|
||||
failureThreshold: 2,
|
||||
successThreshold: 1,
|
||||
openDurationMs: 100,
|
||||
halfOpenMaxCalls: 1,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
cb.on((state) => {
|
||||
console.log("breaker", cb.pathId, "→", state);
|
||||
});
|
||||
|
||||
cb.recordFailure();
|
||||
cb.recordFailure(); // OPEN
|
||||
now += 200;
|
||||
// canExecute lazily transitions to HALF_OPEN
|
||||
if (cb.canExecute()) {
|
||||
cb.recordSuccess(); // CLOSED
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type CircuitBreakerState = "CLOSED" | "OPEN" | "HALF_OPEN"`.
|
||||
- `interface MeshCircuitBreakerOptions { pathId, failureThreshold?, successThreshold?, openDurationMs?, halfOpenMaxCalls?, clock? }`.
|
||||
- `getState(): CircuitBreakerState` — lazy `OPEN → HALF_OPEN` if cooled down.
|
||||
- `canExecute(): boolean` — gating check.
|
||||
- `recordSuccess()` / `recordFailure()` — feedback hooks.
|
||||
- `on(handler) → () => void` — state-change subscription.
|
||||
- `pathId: string` — read-only public path identifier.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The auto-transition from `OPEN → HALF_OPEN` is lazy because we don't run a clock-driven timer inside the breaker — every `getState`/`canExecute` call is the moment we recompute the state if cooldown has elapsed. This avoids leaking timers and keeps the breaker fully synchronous from the caller's perspective. `recordSuccess()` from `OPEN` is a programmer error (the FSM should be `HALF_OPEN` first), so the breaker throws `INVALID_TRANSITION` to surface the bug rather than silently accepting it. In `HALF_OPEN`, `halfOpenMaxCalls` is the cap on trial executions; once exceeded, `canExecute` returns false until the next successful close.
|
||||
Reference in New Issue
Block a user