Retries convert a transient failure into another chance to do useful work. They also create new load. A saturated dependency may get worse when every caller immediately repeats its request. A retry policy therefore needs an error classification, a total attempt allowance, a delay policy and a time budget. A loop that catches every exception and repeats forever has none of those boundaries.
The build counts attempts from one: attempt one is the initial execution, not the first retry. With max_attempts=3, at most two retries can follow it. A permanent failure stops immediately even if attempts remain. A timeout is not proof that a remote effect failed; the next lesson handles duplicates rather than assuming every retry is harmless.
For the just-failed attempt number a, delay is min(cap, base * 2**(a-1)). Base two and cap five produce delays two, four, five, five. Once the cap is reached, further doubling is unnecessary. This matters for large counters: compute a saturated delay instead of constructing an enormous integer power only to discard it.
base, cap = 2, 5
delay = base
for attempt in range(1, 5):
print(attempt, delay)
delay = min(cap, 2 * delay)The deadline in this exercise is an exclusive next-attempt start budget. At tick eight, a delay two cannot start a retry before deadline ten. Return None; do not promise to squeeze execution into a budget that has already ended. This does not reserve time for the operation to finish. A production design may need an execution timeout and remaining-budget calculation as separate decisions.
Scheduling and releasing use different comparisons. A new retry needs due < deadline, while an existing scheduled item is ready when due <= now. Stable input-order ties make simultaneous jobs deterministic. due_jobs accepts a one-pass iterable, validates every due value, and returns a new list; it does not remove entries from a scheduler. Its caller must record dispatch so repeated inspection does not imply repeated processing.
Fake time is an input, not a slow test. At tick two a due-three item waits; at tick three it appears. This lets public and hidden fixtures test exact boundaries instantly. No sleeping, wall clock, network requests or performance benchmark helps establish these policies. Keep time units consistent even though the integer ticks need not represent seconds.
Real callers commonly add jitter so many retries do not synchronize at the same cap. The primary reading explains that motivation. This build deliberately omits randomness to isolate the budget and delay rules; an extension can inject a deterministic random source and test allowed jitter bounds. The exercises reject one extra attempt, a retry at deadline equality, bad configuration and unstable tie handling. Passing them demonstrates a bounded local planning function, not a reliable broker or an outage-tested client.