Solving tips
- Backoff grows the wait between attempts (base * 2**attempt) so you stop hammering a service that is already struggling.
- Inject the sleep function so tests stay fast and deterministic — never call real time.sleep in gradeable code.
- max_retries is the number of RETRIES, so the function calls fn up to max_retries + 1 times before giving up.
Build a retry wrapper with exponential backoff — the single most common reliability primitive around a flaky call (an LLM API, a tool HTTP request, a rate-limited endpoint). The interviewer wants to see correct attempt counting, a growing backoff, and a sleep you can inject so the behavior is deterministic and testable.
How backoff works
You call fn(). If it succeeds, you return its result. If it raises, you wait a bit and try again — and each wait is longer than the last so you back off a service that is under pressure.
For retry attempt attempt (0-indexed: the first retry is attempt = 0), the wait is:
delay = base_delay * 2**attempt + jitter
jitter = 0.1 * base_delay * attempt
The jitter here is deterministic (a fixed function of attempt) so the test can assert exact delays. In production you would randomize it to avoid thundering-herd sync, but a gradeable exercise keeps it reproducible.
You sleep by calling the injected sleep(delay) — not time.sleep — so tests run instantly and can record every delay.
Task
Complete retry(fn, max_retries, base_delay, sleep):
- Attempt
fn(). On success, return its result immediately. - On exception, if any retries remain, compute
delay = base_delay * 2**attempt + 0.1 * base_delay * attempt, callsleep(delay), then try again. - After the final attempt fails (a total of
max_retries + 1calls tofn), re-raise the last exception.
Example
calls = []
delays = []
def flaky():
calls.append(1)
if len(calls) < 3: # fail twice, then succeed
raise ValueError("boom")
return "ok"
def sleep(s):
delays.append(s)
retry(flaky, max_retries=3, base_delay=1.0, sleep=sleep) # -> "ok"
# len(calls) == 3
# delays == [1.0, 2.1] # attempt 0: 1*1 + 0; attempt 1: 1*2 + 0.1
Constraints
- Do not call
time.sleepor any real timer — use the injectedsleep. max_retriescounts retries, not total attempts:fnis invoked at mostmax_retries + 1times.- If
max_retries == 0, callfnexactly once and let any exception propagate (no sleep). - Do not swallow the final failure — re-raise the exception from the last attempt unchanged.