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:
TransientError (and its subclass RateLimited), TimeoutError and ConnectionError. Anything else, such as PermanentError for a 4xx, is re-raised at once with no sleep (Day 4, section 3.2).RateLimited carries retry_after seconds from the server's Retry-After header: sleep exactly that long instead of the backoff (the 429 exception in section 3.2).sleep. Draw randomness only from the injected rng (rng.uniform(a, b) or rng.random()), so the tests can predict every delay.In JavaScript and TypeScript the functions are backoffDelay(attempt, base, cap, rng) and retry(operation, { maxAttempts, base, cap, sleep, rng }), the keyword arguments becoming one options object. JavaScript has no built-in TimeoutError or ConnectionError for this, so the given code defines both, and RateLimited has retryAfter.
Write your code, then run 3 example tests and 10 hidden tests. Each run is limited to 8 seconds: an infinite loop gets stopped and the runtime restarts, your code stays.