95 lines
2.4 KiB
Go
95 lines
2.4 KiB
Go
|
|
// Package backoff implements exponential backoff with jitter for retry logic.
|
||
|
|
package backoff
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"math"
|
||
|
|
"math/rand/v2"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Config holds the configuration for a Backoff instance.
|
||
|
|
type Config struct {
|
||
|
|
// Initial is the starting upper bound for the backoff duration.
|
||
|
|
// Must be > 0.
|
||
|
|
Initial time.Duration `yaml:"initial"`
|
||
|
|
// Max is the absolute maximum upper bound for the backoff duration.
|
||
|
|
// Must be >= Initial.
|
||
|
|
Max time.Duration `yaml:"max"`
|
||
|
|
// RNG is an optional source of randomness.
|
||
|
|
// If nil, the global math/rand/v2 source is used.
|
||
|
|
RNG *rand.Rand `yaml:"-"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate checks that the configuration values are sensible.
|
||
|
|
func (cfg Config) Validate() error {
|
||
|
|
if cfg.Initial <= 0 {
|
||
|
|
return fmt.Errorf("invalid initial backoff: %v", cfg.Initial)
|
||
|
|
}
|
||
|
|
if cfg.Max < cfg.Initial {
|
||
|
|
return fmt.Errorf("invalid max duration: %v", cfg.Max)
|
||
|
|
}
|
||
|
|
// MaxInt64 will overflow when calculating jitter.
|
||
|
|
if cfg.Max == math.MaxInt64 {
|
||
|
|
return fmt.Errorf("max duration cannot be MaxInt64")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Backoff implements exponential backoff with full jitter.
|
||
|
|
// It is NOT safe for concurrent use.
|
||
|
|
type Backoff struct {
|
||
|
|
initial time.Duration
|
||
|
|
max time.Duration
|
||
|
|
current time.Duration
|
||
|
|
rng *rand.Rand
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a new Backoff instance with the provided configuration.
|
||
|
|
func New(cfg Config) (*Backoff, error) {
|
||
|
|
if err := cfg.Validate(); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid config: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &Backoff{
|
||
|
|
initial: cfg.Initial,
|
||
|
|
max: cfg.Max,
|
||
|
|
current: cfg.Initial,
|
||
|
|
rng: cfg.RNG,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Next returns the next backoff delay.
|
||
|
|
func (b *Backoff) Next() time.Duration {
|
||
|
|
limit := b.current
|
||
|
|
|
||
|
|
// Update state for the next call.
|
||
|
|
if b.current >= b.max {
|
||
|
|
b.current = b.max
|
||
|
|
} else if b.current > b.max/2 {
|
||
|
|
// If doubling would exceed max, just clamp to max.
|
||
|
|
b.current = b.max
|
||
|
|
} else {
|
||
|
|
b.current *= 2
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate jitter; return random in [0, limit].
|
||
|
|
// Int64N(n) returns values in [0, n).
|
||
|
|
// For [0, limit], we use limit + 1.
|
||
|
|
var jitter int64
|
||
|
|
if b.rng != nil {
|
||
|
|
jitter = b.rng.Int64N(int64(limit) + 1)
|
||
|
|
} else {
|
||
|
|
jitter = rand.Int64N(int64(limit) + 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
return time.Duration(jitter)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset resets the current backoff cap to the initial value.
|
||
|
|
// This should be called after a successful operation to restart
|
||
|
|
// the backoff sequence for future retries.
|
||
|
|
func (b *Backoff) Reset() {
|
||
|
|
b.current = b.initial
|
||
|
|
}
|