Return context error when a retry backoff wait is cancelled
CI / ci (push) Successful in 1m35s
CI / ci (pull_request) Successful in 1m35s

Retrying.Generate returned the earlier provider error (e.g. a 429) when
cancellation or the deadline interrupted the backoff wait, so callers and
the ai_runs record saw rate limiting instead of cancellation. Return the
context error from the interrupted wait instead. Retry counts, backoff and
other error semantics are unchanged.

Maestro task: 79447ef7-e55c-4641-b351-37bc22ffa760

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:33:20 -04:00
co-authored by Claude Opus 5.5
parent 72fa311de9
commit e344522c63
2 changed files with 41 additions and 3 deletions
+37
View File
@@ -240,6 +240,43 @@ func TestRetryingStopsWhenContextDone(t *testing.T) {
assertConsumed(t, f, 1)
}
func TestRetryingWaitInterruptedReturnsContextError(t *testing.T) {
t.Run("cancelled", func(t *testing.T) {
r, f, _ := newTestRetrying()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
r.sleep = func(ctx context.Context, d time.Duration) error {
cancel()
return sleep(ctx, d)
}
f.QueueGenerate(fail(NewRateLimitedError(3*time.Second, "")))
_, err := r.Generate(ctx, retryReq)
if !errors.Is(err, context.Canceled) || errors.Is(err, ErrRateLimited) {
t.Fatalf("err = %v, want context.Canceled", err)
}
assertConsumed(t, f, 1)
})
t.Run("deadline", func(t *testing.T) {
r, f, _ := newTestRetrying()
// The fake clock stands still, so the 500ms backoff fits before
// the caller's deadline; the real wait outlasts it.
r.jitter = func(time.Duration) time.Duration { return 0 }
r.sleep = func(ctx context.Context, _ time.Duration) error {
return sleep(ctx, time.Hour)
}
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Millisecond)
defer cancel()
f.QueueGenerate(fail(NewRateLimitedError(0, "")))
_, err := r.Generate(ctx, retryReq)
if !errors.Is(err, context.DeadlineExceeded) || errors.Is(err, ErrRateLimited) {
t.Fatalf("err = %v, want context.DeadlineExceeded", err)
}
assertConsumed(t, f, 1)
})
}
// slowProvider is a Provider whose every attempt takes d on clock c,
// records the deadline of the context it received, and times out.
type slowProvider struct {