Circuit breaker
Stop calling what keeps failing: around an outgoing call, or per route.
Needs inkan 0.8
@inkanjs/circuit-breaker needs inkan 0.8.0 or newer, below 1.0.
npm i @inkanjs/circuit-breakerA service that is down gets time to come back instead of a queue of retries, and your callers get an answer at once instead of waiting for the next timeout. No dependencies.
closed calls go through; `threshold` failures within `window` ms open it
open calls fail at once for `cooldown` ms
half-open one call goes through as a probe: success closes it, failure opens it againOutgoing calls: circuit(fn)
The common case: a service you call that sometimes falls over. Wrap the call once, and use the wrapped function as before.
import { } from "@vxnsin/inkan";
import { , } from "@inkanjs/circuit-breaker";
const = ((: string) => (`https://rates.example/${}`, { : .(2000) }), {
: "rates",
: 5,
: 30_000,
});
.("/prices/:currency", async () => {
try {
const = await (..);
if (!.ok) throw (502, "rates-failed");
return await .json();
} catch () {
if ( instanceof ) throw new (503, "rates-unavailable", , {}, { "retry-after": (.retryAfter) });
throw ;
}
});- What the function throws counts as a failure, and so does a returned value whose
statusis 500 or more (a fetchResponse); that response is still handed back. - While open, the wrapped function rejects at once with
CircuitOpenError(code: "circuit-open",retryAfterin seconds,keythename), without calling. rates.stateis"closed","open"or"half-open";rates.reset()closes it.- Give every call a timeout of its own (
AbortSignal.timeout): a call that hangs is not a failure until it ends.
Incoming routes: breaker()
import { } from "@inkanjs/circuit-breaker";
.(({ : 5, : 10_000, : 30_000 }));GET /reports → 500, 500, 500, 500, 500
GET /reports → 503 circuit-open, retry-after: 30 (the handler does not run)
… 30 s later
GET /reports → the probe: 200 closes it, a 5xx opens it for another 30 sA shared plugin: a circuit per route of the scope it is registered in. While a route's
circuit is open, its requests get a 503 problem circuit-open with retry-after, in an
onRequest hook, before the body is read or the handler runs. It protects what is behind a
route (a database, an upstream) from being hammered while it is down. A failure is an answer
of 500 or more; a 4xx problem is an answer, not a failure.
Behaviour
- The state lives in this process. With
workerseach process counts its own failures, which is mostly what you want: each finds out on its own that a service is down. - In half-open exactly one probe goes through; the others fail at once with
retryAfter1. A probe that never settles is given up after anothercooldown. keyruns for every request of the scope: keep it cheap. Requests no route takes are left alone.Circuitis exported for a breaker of your own:enter()before a call (it throwsCircuitOpenErrorwhile open),settle(token, failed)after it.
Options
| option | default | |
|---|---|---|
threshold | 5 | failures within window that open the circuit |
window | 10000 | ms the failures are counted over |
cooldown | 30000 | ms it stays open before one probe goes through |
isFailure | thrown, or status ≥ 500 | (status, err) => boolean |
now | Date.now | the clock in ms, your own in tests |
onStateChange | none | (to, from, key), for logs and metrics |
name | none | circuit() only: names its errors and changes |
key | the route, "GET /teas/:id" | breaker() only: (ctx) => string, which circuit a request belongs to, such as one per tenant |
max | 10000 | breaker() only: circuits kept, the least recently used dropped first |
OpenAPI
Unchanged.