Retry with exponential backoff and full jitter.
Retry only what can succeed later, wait base × 2^attempt capped, pick a random point below it, and honour Retry-After.
Ten servers that retry a failed call immediately, three times each, turn one blip into a stampede on a service that is already struggling (Week 2 preview, Concept 3). The fixes: wait longer after each failure (exponential backoff), add randomness so clients don't retry in lockstep (jitter), give up after a budget, and don't retry what can't succeed.
This lab uses full jitter: instead of adding a small random amount to the backoff, pick the whole delay at random below it.
ceiling = min(cap, base * 2 ** attempt) # attempt = 0 for the first retry
delay = rng.uniform(0, ceiling)Implement two functions:
| Function | Does |
|---|---|
backoff_delay(attempt, base, cap, rng) |
the full-jitter delay for retry number attempt (0-based) |
retry(operation, *, max_attempts, base, cap, sleep, rng) |
calls operation() up to max_attempts times in total and returns its first result. Between attempts it calls sleep(delay) |
Rules the tests check:
- Retry only
TransientError(and its subclassRateLimited),TimeoutErrorandConnectionError. Anything else, such asPermanentErrorfor a 4xx, is re-raised at once with no sleep (Day 4, section 3.2). RateLimitedcarriesretry_afterseconds from the server'sRetry-Afterheader: sleep exactly that long instead of the backoff (the 429 exception in section 3.2).- After the last attempt fails, re-raise that exception, not a new one.
- Never sleep for real: call the injected
sleep. Draw randomness only from the injectedrng(rng.uniform(a, b)orrng.random()), so the tests can predict every delay.
Write your code, then run 2 example tests and 10 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and Python restarts, your code stays.