Bounded Retry Policy
Part of AI Engineering: Implement a bounded retry policy
Bounded Retry Policy
An AI feature calls a model provider that sometimes fails. Before the application retries a failed call, it asks a pure policy function what to do next. The function never sleeps, reads a clock, or generates random numbers: the caller passes the time left before the task's deadline and a random value for jitter, so every decision can be tested exactly.
Implement decideRetry(failure, completedAttempts, remainingMs, random, policy).
Inputs
failure:{ transient, safeToRetry, retryAfterMs? }.transientistruewhen the failure is temporary, such as a rate limit or a brief outage.safeToRetryistruewhen the request can be replayed without repeating a side effect.retryAfterMs, when present, is the provider's minimum wait, already parsed into milliseconds.completedAttempts: attempts made so far, including the one that just failed.remainingMs: milliseconds left before the logical task's deadline.random: a number in [0, 1) chosen by the caller.policy:{ maxAttempts, baseDelayMs, maxDelayMs, minimumAttemptMs }.maxAttemptscounts total attempts, not retries.minimumAttemptMsis the time a new attempt needs to be worth starting.
Return { retry: true, delayMs } or { retry: false, reason }, with exactly those keys.
Rules, in order
- Validate first. Throw an
ErrorwhenremainingMs,baseDelayMs,maxDelayMs,minimumAttemptMs, or a presentretryAfterMsis not a finite number of at least 0; whencompletedAttemptsormaxAttemptsis not an integer of at least 1; whenrandomis not a finite number in [0, 1); or whenminimumAttemptMsis 0. Invalid input throws even when the failure would not be retried. - If
safeToRetryisfalse, return reason"unsafe". - If
transientisfalse, return reason"permanent". - If
completedAttempts >= maxAttempts, return reason"attempts". - Compute the delay with capped exponential backoff, full jitter, and retry-after as a minimum:
ceiling = min(maxDelayMs, baseDelayMs * 2 ** (completedAttempts - 1))jittered = floor(random * ceiling)delayMs = max(jittered, retryAfterMs), where a missing retry-after counts as 0
- If
delayMs + minimumAttemptMs > remainingMs, return reason"deadline". Otherwise return{ retry: true, delayMs }.
Fixtures
Every row uses policy = { maxAttempts: 3, baseDelayMs: 200, maxDelayMs: 1000, minimumAttemptMs: 300 } and random = 0.5. A safe failure has safeToRetry: true.
| Failure and state | Remaining time | Expected decision |
|---|---|---|
| Transient, safe, 1 completed attempt | 2,000 ms | { retry: true, delayMs: 100 } |
| Transient, safe, 2 completed attempts | 2,000 ms | { retry: true, delayMs: 200 } |
| Transient, safe, 3 completed attempts | 2,000 ms | { retry: false, reason: "attempts" } |
| Permanent, safe, 1 completed attempt | 2,000 ms | { retry: false, reason: "permanent" } |
| Transient, unsafe, 1 completed attempt | 2,000 ms | { retry: false, reason: "unsafe" } |
| Transient, safe, retry-after 700 ms, 1 completed attempt | 2,000 ms | { retry: true, delayMs: 700 } |
| Same retry-after case | 900 ms | { retry: false, reason: "deadline" } |
| Transient, safe, 1 completed attempt (needs 100 + 300 ms) | 399 ms | { retry: false, reason: "deadline" } |
Notes
- Retry-after can exceed
maxDelayMs. The cap limits the randomized backoff, not the provider's minimum wait; the deadline still decides whether to wait at all. - Remaining time exactly equal to
delayMs + minimumAttemptMsis enough to retry. - A
randomof 0 gives a jittered delay of 0.
In Python, implement decide_retry(failure, completed_attempts, remaining_ms, random_value, policy) with dict keys transient, safe_to_retry, and retry_after_ms (missing or None means no retry-after), and policy keys max_attempts, base_delay_ms, max_delay_ms, and minimum_attempt_ms. Return {"retry": True, "delay_ms": ...} or {"retry": False, "reason": ...}, and raise ValueError for invalid input.
0 views
Solution
Hints
