Files
httpx/retry/backoff.go
Aleksey Shakhmatov 43d3ecfba1 Fix retry body replay and jitter panic; drive delays via internal/clock
Gate the retry decision on body rewindability: an idempotent request whose
body cannot be replayed (no GetBody) is now returned as-is instead of looping
with an empty body or surfacing a stale, already-drained response. Guard
ExponentialBackoff against rand.Int64N panicking when delay/2 rounds to zero.
Use internal/clock for inter-attempt delays so retry timing is consistent with
the rest of the codebase and testable without real sleeps.
2026-05-23 13:47:18 +03:00

68 lines
1.5 KiB
Go

package retry
import (
"math/rand/v2"
"time"
)
// Backoff computes the delay before the next retry attempt.
type Backoff interface {
// Delay returns the wait duration for the given attempt number (zero-based).
Delay(attempt int) time.Duration
}
// ExponentialBackoff returns a Backoff that doubles the delay on each attempt.
// The delay is calculated as base * 2^attempt, capped at max. When withJitter
// is true, a random duration in [0, delay*0.5) is added.
func ExponentialBackoff(base, max time.Duration, withJitter bool) Backoff {
return &exponentialBackoff{
base: base,
max: max,
withJitter: withJitter,
}
}
// ConstantBackoff returns a Backoff that always returns the same delay.
func ConstantBackoff(d time.Duration) Backoff {
return constantBackoff{delay: d}
}
type exponentialBackoff struct {
base time.Duration
max time.Duration
withJitter bool
}
func (b *exponentialBackoff) Delay(attempt int) time.Duration {
delay := b.base
for range attempt {
delay *= 2
if delay >= b.max {
delay = b.max
break
}
}
if b.withJitter {
// Guard against rand.Int64N panicking on a non-positive argument when
// delay is small enough that delay/2 rounds to zero.
if half := int64(delay / 2); half > 0 {
delay += time.Duration(rand.Int64N(half))
}
}
if delay > b.max {
delay = b.max
}
return delay
}
type constantBackoff struct {
delay time.Duration
}
func (b constantBackoff) Delay(_ int) time.Duration {
return b.delay
}