err-retry-exponential-backoff
The implementation is in backoff/backoff.go — a fully generic (T any) exponential backoff retry library in Go 1.26.
// Retry a function with exponential backoff.
result, err := backoff.Retry(ctx, config, fn)
// Mark an error as non-retryable (short-circuits the loop).
return "", backoff.Permanent(errors.New("fatal error"))
ctx := context.Background()
cfg := backoff.DefaultConfig()
cfg.MaxRetries = 3
users, err := backoff.Retry(ctx, cfg, func(ctx context.Context) ([]User, error) {
return db.QueryUsers(ctx)
})
| Feature | Implementation |
|---|---|
| Max retries | Config field, initial attempt not counted |
| Initial delay | Base delay before first retry |
| Max delay | Caps exponential growth via if delay > maxDelay { delay = maxDelay } |
| Multiplier | delay = time.Duration(float64(delay) * config.Multiplier) |
| Jitter | rand.Int63n(int64(delay) + 1) — uniform [0, delay) |
| Retryable predicate | IsRetryable func(error) bool — returns immediately if false |
| Permanent errors | backoff.Permanent(err) — errors.As unwrapping, loop bail |
| Context cancellation | Checked before sleep and inside sleepWithContext via select on ctx.Done() |
| Callback | OnRetry(attempt, delay, err) invoked before each retry |
All config parameters are validated in Config.Validate() before the retry loop begins.
All 20 tests pass (including 6 sub-tests): ```text === RUN TestRetry_FirstAttemptSuccess --- PASS === RUN TestRetry_EventuallySucceeds --- PASS (3 attempts → success) === RUN TestRetry_ExhaustsRetries --- PASS (returns last error) === RUN TestRetry_NonRetryableError --- PASS (1 attempt, predicate stops it) === RUN TestRetry_ContextCancelledDuringBackoff --- PASS (cancellation during sleep) === RUN TestRetry_ContextCancelledDuringCall --- PASS (cancellation inside fn) === RUN TestRetry_DeadlineExceeded --- PASS (context deadline) === RUN TestRetry_PermanentError --- PASS (1 attempt, Permanent short-circuits) === RUN TestPermanent_Nil --- PASS === RUN TestRetry_OnRetryCallback --- PASS (callback invoked per attempt) === RUN TestRetry_JitterVariability --- PASS (non-trivial elapsed time) === RUN TestRetry_MaxDelayCap --- PASS (delay capped at 25ms) === RUN TestRetry_ZeroMaxRetries --- PASS (no retries, immediate fail) === RUN TestRetry_RetryablePredicateSelective --- PASS (retry vs stop on different errors) === RUN TestConfig_Validation --- PASS (6 sub-cases) === RUN TestRetry_ManyRetries --- PASS (20 retries, no issues) === RUN TestRetry_ContextValue --- PASS === RUN TestPermanentError_Unwrap --- PASS === RUN TestRetry_ZeroDelay --- PASS === RUN ExampleRetry --- PASS PASS ``` **Edge cases tested:** 1. **Success on first attempt** — no unnecessary delay 2. **Exhausts all retries** — returns the last error from the loop 3. **Non-retryable predicate** — returns immediately without retrying 4. **Context cancellation during backoff sleep** — respects cancellation within ~10ms 5. **Context cancellation from inside the function** — passes `ctx.Err()` through 6. **Deadline exceeded** — returns `context.DeadlineExceeded` 7. **Permanent error** — `backoff.Permanent()` short-circuits in one attempt 8. **Zero max retries** — only the initial attempt runs 9. **Zero initial delay** — no sleep, works correctly 10. **Max delay cap** — exponential growth bounded to 25ms 11. **Jitter** — randomized delays produce varying total time 12. **Large retry count** — 20 retries with jitter and max delay handle gracefully 13. **Nil Permanent** — returns nil safely Benchmark: `BenchmarkRetry_FastFailure-16` — **37,743 ns/op** (2 calls with 1μs delay). ---
{"model": "gpt-4o", "problem_class": "err-retry-exponential-backoff", "result": "passed", "tests": 20}