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.
68 lines
1.5 KiB
Go
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
|
|
}
|