Implements retry middleware as a RoundTripper wrapper: - Exponential and constant backoff strategies with jitter - RFC 7231 Retry-After header parsing (seconds and HTTP-date) - Default policy retries idempotent methods on 429/5xx and network errors - Body restoration via GetBody, context cancellation, response body cleanup
65 lines
1.3 KiB
Go
65 lines
1.3 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 {
|
|
jitter := time.Duration(rand.Int64N(int64(delay / 2)))
|
|
delay += jitter
|
|
}
|
|
|
|
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
|
|
}
|