Litestar¶
The interlock-cb[litestar] extra (Litestar ≥ 2.23) protects a route's
outgoing dependency with a shared Registry and turns a tripped breaker into
a clean 503 Service Unavailable response with a Retry-After header.
Usage¶
Litestar wires exception handlers and dependencies at construction time —
declare both on the app (or a router / controller) and annotate the handler
parameter with NamedDependency:
from litestar import Litestar, get
from litestar.di import NamedDependency
from interlock import CircuitBreaker, CircuitOpenError, Registry
from interlock.integrations.litestar import breaker_dependency, circuit_open_handler
registry = Registry()
@get('/orders')
async def orders(breaker: NamedDependency[CircuitBreaker]) -> list[dict]:
return await breaker.call(fetch_orders)
app = Litestar(
route_handlers=[orders],
dependencies={'breaker': breaker_dependency('orders-db', registry=registry)},
exception_handlers={CircuitOpenError: circuit_open_handler},
)
When fetch_orders fails often enough, the breaker opens. The next request is
rejected with CircuitOpenError before fetch_orders runs, and the handler
converts it into:
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/json
{"detail": "Circuit 'orders-db' is open"}
How it works¶
breaker_dependency(name, *, registry)returns a LitestarProvidethat yields the named breaker from the sharedRegistry. The breaker is created lazily on first use and reused on every later request, so all requests sharing the dependency share one breaker (and one view of the downstream's health). Declare it at any layer — app, router, controller or handler.circuit_open_handlermapsCircuitOpenErrorto503and setsRetry-Afterto the breaker'sretry_afterestimate, rounded up to whole seconds (per RFC 7231). The header is omitted when there is no estimate (for example afterforce_open()).
You protect the outgoing call (breaker.call(...)) rather than the route
itself: only the dependency you wrap counts toward the breaker, and the
breaker's own admission logic (probes, half-open) keeps working.
Sharing breakers across routes¶
Reuse the same name (and the same registry) wherever routes depend on the
same downstream — one dependencies={...} declaration on the app covers them
all, and every route sees the same circuit state.
Pass config, clock, classifier or listener to the Registry to tune
every breaker it creates, or override per name via
registry.get(name, config=...).
Custom responses¶
For a different response shape, register your own handler instead: