Nobody has started this yet — be first.
Business impact
Today the only way to bound how long a Do call can run in total is the caller's own ctx deadline, set independently, or MaxAttempts combined with a worst-case backoff calculation the caller has to do by hand every time a policy changes. For a request-handling path with its own SLA (e.g. an HTTP handler that must respond within N seconds regardless of how many attempts a downstream needs), that mismatch is a real risk: a policy tuned for "retry aggressively" can end up running far longer in the worst case than any caller actually intended, holding a connection/goroutine open the whole time. A Policy-level overall deadline makes "this whole retry operation, no matter how it's configured, will never run past N" an explicit, self-contained guarantee that travels with the policy itself.
Problem
A field, OverallTimeout time.Duration, has been added as scaffolding in pkg/retry/policy.go ("bounds the total wall-clock time of an entire Do/DoWithResult call... independent of whatever deadline the caller's ctx already carries"). Nothing enforces it. A policy with a large MaxAttempts and a long BaseDelay/MaxDelay combination can run for as long as the arithmetic works out to, regardless of OverallTimeout.
Current behavior
A Policy with MaxAttempts: 30 at a constant 50ms backoff and OverallTimeout: 150ms still runs for the full ~1.45s the unbounded arithmetic implies, instead of stopping at ~150ms.
Expected behavior
When Policy.OverallTimeout > 0, DoWithResult bounds the entire call (every attempt and every backoff sleep, combined) to that duration, independent of and in addition to whatever deadline the caller's own ctx carries -- whichever deadline is reached first wins.
Steps to reproduce
policy := retry.Policy{MaxAttempts: 30, BaseDelay: 50 * time.Millisecond, Multiplier: 1, OverallTimeout: 150 * time.Millisecond} result := retry.Do(context.Background(), alwaysFailingFn, policy) // got: Do runs for ~1.458s (the full MaxAttempts * BaseDelay worst case), ignoring OverallTimeout entirely // want: Do returns at approximately the 150ms OverallTimeout mark
Why this matters
MaxAttempts alone doesn't bound wall-clock time in any way that's easy to reason about at a call site far from where the policy was defined -- someone tuning BaseDelay/Multiplier/MaxDelay for better backoff characteristics can accidentally blow past a latency budget nobody re-checked. An explicit OverallTimeout makes the bound a first-class, self-documenting part of the policy.
Suggested approach
DoWithResult already respects ctx cancellation via ctx.Err() checks and the backoff-sleep select. context.WithTimeout composes cleanly with an existing context -- wrapping the caller's ctx with one more layer near the top of DoWithResult (only when policy.OverallTimeout > 0) should make every existing cancellation check "just work" for the new deadline too, with no changes needed to the loop body itself. Don't forget to call the returned cancel function.
Acceptance criteria
Verification
go test -race ./test/ -run TestTask11 -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/overall-timeoutFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/overall-timeoutSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8080POST /v1/chargesTo run against Redis (the multi-replica-safe backend) with zero local setup:
docker compose up --build # app + redis, hot-reloading on save
Work the tickets in tasks/ (ordered easy → hard); each names one Go test in test/:
make test-race # go test -race -count=1 ./...
go test ./test/ -run TestTask01 -v # a single ticket
Level 2
Implement a feature
Extend the system within its own patterns.