We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency Without Regret step 21 of 38
Deadline: one budget, many nested scopes
Give a call graph one budget instead of a dozen unrelated timeouts.
class Deadline:
def __init__(self, budget: float) -> None: ...
def remaining(self) -> float: ...
def expired(self) -> bool: ...
def scope(self, limit: float | None = None) -> asyncio.Timeout: ...
async def call_with_deadline[T](
deadline: Deadline, op: Callable[[], Awaitable[T]], *, retries: int
) -> T
-
Deadline(budget)fixes an absolute deadline atbudgetseconds from now, onloop.time()— the same clockasyncio.timeout_atreads. -
scope()returns a timeout scope expiring at that deadline.scope(limit)expires afterlimitseconds or at the deadline, whichever comes first. Nesting can only tighten, never extend. -
call_with_deadlinerunsopinside the deadline, retryingRetryableErrorup toretriestimes. It must never start an attempt once the deadline has expired — raiseTimeoutErrorinstead. Any other exception propagates immediately.
Why the budget exists
Per-call timeouts do not compose. A one-second SLA, a two-second downstream timeout and five retries gives you a twelve-second request in which every individual call was “within timeout”. Nothing in that code is wrong line by line; what is missing is a single absolute point that every level shares.
What the report proves
-
attempts— the number of timesopwas entered. The budget case is the interesting one: an operation that consumes the whole budget on its first try must produce exactly one attempt, however many retries were configured. Retrying past an expired deadline shows up here immediately. -
outcome/value/message— a fast success returns straight away; an exhausted retry count surfaces the lastRetryableError; an expired budget raisesTimeoutError. -
expired— the deadline’s own view of itself afterwards. -
level— for the two structural scenarios:-
"outside"provesTimeoutErroris raised outside theasync with.asyncio.timeoutcancels the current task and converts theCancelledErrorintoTimeoutErrorin__aexit__, so atry/except TimeoutErrorinside the block never fires. This trips up nearly everyone once. -
"inner"/"outer"proves nested scopes attribute the expiry to the level that actually blew, which is whatTask.cancelling()/uncancel()bookkeeping buys you.
-
Where the type system earns its keep
scope() returns asyncio.Timeout — a real type, so async with deadline.scope(): type-checks and a scope() that forgot to return anything
does not. call_with_deadline is generic in the operation’s result, so the
retry wrapper is transparent to the caller’s types. And limit: float | None
with None meaning “the whole remaining budget” is the honest signature: a
sentinel 0.0 would be indistinguishable from a caller asking for no time at
all.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.