Files
raven/internal/backoff/backoff.go

95 lines
2.4 KiB
Go
Raw Permalink Normal View History

// 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
}