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.
73 lines
1.8 KiB
Go
73 lines
1.8 KiB
Go
package retry
|
|
|
|
import (
|
|
"time"
|
|
|
|
"git.codelab.vc/pkg/httpx/internal/clock"
|
|
)
|
|
|
|
type options struct {
|
|
maxAttempts int // default 3
|
|
backoff Backoff // default ExponentialBackoff(100ms, 5s, true)
|
|
policy Policy // default: defaultPolicy (retry on 5xx and network errors)
|
|
retryAfter bool // default true, respect Retry-After header
|
|
clk clock.Clock // time source for backoff delays (real by default)
|
|
}
|
|
|
|
// Option configures the retry transport.
|
|
type Option func(*options)
|
|
|
|
func defaults() options {
|
|
return options{
|
|
maxAttempts: 3,
|
|
backoff: ExponentialBackoff(100*time.Millisecond, 5*time.Second, true),
|
|
policy: defaultPolicy{},
|
|
retryAfter: true,
|
|
clk: clock.System(),
|
|
}
|
|
}
|
|
|
|
// withClock sets the clock used for inter-attempt delays. Unexported; for
|
|
// deterministic tests.
|
|
func withClock(c clock.Clock) Option {
|
|
return func(o *options) {
|
|
if c != nil {
|
|
o.clk = c
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithMaxAttempts sets the maximum number of attempts (including the first).
|
|
// Values less than 1 are treated as 1 (no retries).
|
|
func WithMaxAttempts(n int) Option {
|
|
return func(o *options) {
|
|
if n < 1 {
|
|
n = 1
|
|
}
|
|
o.maxAttempts = n
|
|
}
|
|
}
|
|
|
|
// WithBackoff sets the backoff strategy used to compute delays between retries.
|
|
func WithBackoff(b Backoff) Option {
|
|
return func(o *options) {
|
|
o.backoff = b
|
|
}
|
|
}
|
|
|
|
// WithPolicy sets the retry policy that decides whether to retry a request.
|
|
func WithPolicy(p Policy) Option {
|
|
return func(o *options) {
|
|
o.policy = p
|
|
}
|
|
}
|
|
|
|
// WithRetryAfter controls whether the Retry-After response header is respected.
|
|
// When enabled and present, the Retry-After delay is used if it exceeds the
|
|
// backoff delay.
|
|
func WithRetryAfter(enable bool) Option {
|
|
return func(o *options) {
|
|
o.retryAfter = enable
|
|
}
|
|
}
|