Reliability architecture¶
Reliability in this library is about failing in a way the deployment can see and recover from: a dependency that is down should produce a bounded, observable refusal rather than a queue of stuck requests, and a shutdown should finish the work it accepted.
The failure domains¶
flowchart TB
req["a request"] --> gate["ConcurrencyGate<br/>jdlib.reliability.concurrency"]
gate -->|"admitted"| call["the call"]
gate -->|"over the limit"| refuse["429 / refusal<br/>audited, counted"]
call --> cb["CircuitBreaker<br/>jdlib.reliability.breaker"]
cb -->|"closed"| dep["dependency"]
cb -->|"open"| failfast["immediate refusal<br/>no attempt made"]
dep -->|"transient"| retry["RetryBudget<br/>jdlib.reliability.budget"]
retry -->|"budget left"| call
retry -->|"exhausted"| give["refusal + the budget is spent"]
dep -->|"permanent"| give
stop["ShutdownCoordinator<br/>jdlib.reliability.lifecycle"] -.->|"drain"| gate
stop -.->|"report"| probe["/readyz tells the truth"]
| Primitive | Module | The question it answers |
|---|---|---|
ConcurrencyGate |
jdlib.reliability.concurrency |
how much work is admitted at once, and what happens to the rest |
CircuitBreaker (BreakerPolicy, BreakerState) |
jdlib.reliability.breaker |
when to stop trying, and how it closes again |
RetryBudget |
jdlib.reliability.budget |
how much retrying is allowed in total, so retries cannot amplify an outage |
ShutdownCoordinator (ShutdownReport) |
jdlib.reliability.lifecycle |
how accepted work finishes while new work is refused |
Why a budget rather than per-call retries¶
A retry policy that counts attempts per call multiplies load on a dependency that is already
struggling: every caller retries, and the dependency sees the product. A RetryBudget is a shared
allowance — when it is spent, callers stop retrying and fail fast, which is the behaviour that lets a
degraded dependency recover.
Readiness that tells the truth¶
/healthz answers for the process (it is serving, no dependencies checked); /readyz answers for
the dependencies (SELECT 1 against the control plane). The split is not cosmetic: a probe that
needs a token fails during an identity-provider outage and gets a healthy process restarted for it.
The minimal example asserts both, including /readyz answering 503 DEPENDENCY_UNAVAILABLE when its
database is down, through the library's own envelope.
What is deliberately not here¶
- A job queue.
jdlib.tenancy.job_envelopegives work an authority, a tenant and a lifetime; delivery, retries and the dead-letter store belong to the deployment's queue.jdlib.tenancy.dead_letteris the shape the library hands a queue, not a queue. - A scheduler. Nothing in the library wakes up and does work on its own.
- Cross-process coordination. The breaker, the budget and the gate are per-process. A fleet shares the dependency, not the state — the deployment's metrics are what make that visible.