Composition¶
Policies are small on purpose; their value comes from stacking them. Stack decorators directly (the one closest to the function runs innermost), or name a stack once with compose (the first argument is outermost):
from nopanic import CircuitOpen, backoff, circuit_breaker, compose, fallback, retry, timeout
llm = circuit_breaker(failure_threshold=0.5, min_calls=8, reset_timeout=20.0, name="llm")
resilient = compose(
fallback(lambda exc: "Sorry, try again later.", on=(ConnectionError, CircuitOpen, TimeoutError)),
retry(attempts=3, on=(ConnectionError, TimeoutError), backoff=backoff.full_jitter(0.2)),
llm,
timeout(30.0),
)
@resilient
async def ask(prompt: str) -> str: ...
The recommended order¶
Outermost to innermost:
fallback: sees only what nothing else could absorb.deadline: one budget for everything below.retry(with aRetryBudget): retries whole guarded attempts.circuit_breaker: every attempt's outcome feeds it; while open, attempts fail instantly.rate_limit/adaptive_rate_limit/bulkhead: admission control per attempt.timeout: bounds each attempt.
A few interactions worth knowing:
timeoutinsideretrygives each attempt a fresh limit, usually the intent. Outsideretry, it bounds all attempts together (preferdeadlinefor that).retryand an open breaker:CircuitOpenis retried only if you include it inon. With it included,retrywaits out the breaker'sretry_after(capped byretry_after_cap); without it, an open breaker fails fast.retryand a rate limiter withmax_wait:RateLimitedcarriesretry_after, so including it inonmakesretrywait exactly long enough.adaptive_rate_limitand a breaker: the limiter's defaultlooks_throttledignoresCircuitOpen, so a tripped breaker never collapses the adaptive rate.
One-off calls¶
Every policy and every compose stack runs a call without decorating anything:
user = resilient.call(client.get_user, user_id)
user = await resilient.acall(async_client.get_user, user_id)
acall accepts anything that returns an awaitable: an async def, a bound method of an async client, or a lambda. On a stack, call / acall build the wrapper chain per call (a few microseconds); decorate once on hot paths.
Blocks of code¶
Stateful policies guard blocks as well as functions:
with payments_breaker.guard(), payments_bulkhead.guard():
...
async with timeout(5.0).guard():
...