185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
// Package backoff contains an implementation of an intelligent backoff
|
|
// strategy. It is based on the approach in the AWS architecture blog
|
|
// article titled "Exponential Backoff And Jitter", which is found at
|
|
// http://www.awsarchitectureblog.com/2015/03/backoff.html.
|
|
//
|
|
// Essentially, the backoff has an interval `time.Duration`; the nth
|
|
// call to backoff will return a `time.Duration` that is 2^n *
|
|
// interval. If jitter is enabled (which is the default behaviour),
|
|
// the duration is a random value between 0 and 2^n * interval. The
|
|
// backoff is configured with a maximum duration that will not be
|
|
// exceeded.
|
|
//
|
|
// This package uses math/rand/v2 for jitter, which is automatically
|
|
// seeded from a cryptographically secure source.
|
|
package backoff
|
|
|
|
import (
|
|
"math"
|
|
"math/rand/v2"
|
|
"time"
|
|
)
|
|
|
|
// DefaultInterval is used when a Backoff is initialised with a
|
|
// zero-value Interval.
|
|
var DefaultInterval = 5 * time.Minute
|
|
|
|
// DefaultMaxDuration is the maximum amount of time that the backoff will
|
|
// delay for.
|
|
var DefaultMaxDuration = 6 * time.Hour
|
|
|
|
// A Backoff contains the information needed to intelligently backoff
|
|
// and retry operations using an exponential backoff algorithm. It should
|
|
// be initialised with a call to `New`.
|
|
//
|
|
// Only use a Backoff from a single goroutine, it is not safe for concurrent
|
|
// access.
|
|
type Backoff struct {
|
|
// maxDuration is the largest possible duration that can be
|
|
// returned from a call to Duration.
|
|
maxDuration time.Duration
|
|
|
|
// interval controls the time step for backing off.
|
|
interval time.Duration
|
|
|
|
// noJitter controls whether to use the "Full Jitter" improvement to attempt
|
|
// to smooth out spikes in a high-contention scenario. If noJitter is set to
|
|
// true, no jitter will be introduced.
|
|
noJitter bool
|
|
|
|
// decay controls the decay of n. If it is non-zero, n is
|
|
// reset if more than the last backoff + decay has elapsed since
|
|
// the last try.
|
|
decay time.Duration
|
|
|
|
n uint64
|
|
lastTry time.Time
|
|
}
|
|
|
|
// New creates a new backoff with the specified maxDuration duration and
|
|
// interval. Zero values may be used to use the default values.
|
|
//
|
|
// Panics if either dMax or interval is negative.
|
|
func New(dMax time.Duration, interval time.Duration) *Backoff {
|
|
if dMax < 0 || interval < 0 {
|
|
panic("backoff: dMax or interval is negative")
|
|
}
|
|
|
|
b := &Backoff{
|
|
maxDuration: dMax,
|
|
interval: interval,
|
|
}
|
|
b.setup()
|
|
return b
|
|
}
|
|
|
|
// NewWithoutJitter works similarly to New, except that the created
|
|
// Backoff will not use jitter.
|
|
func NewWithoutJitter(dMax time.Duration, interval time.Duration) *Backoff {
|
|
b := New(dMax, interval)
|
|
b.noJitter = true
|
|
return b
|
|
}
|
|
|
|
func (b *Backoff) setup() {
|
|
if b.interval == 0 {
|
|
b.interval = DefaultInterval
|
|
}
|
|
|
|
if b.maxDuration == 0 {
|
|
b.maxDuration = DefaultMaxDuration
|
|
}
|
|
}
|
|
|
|
// Duration returns a time.Duration appropriate for the backoff,
|
|
// incrementing the attempt counter.
|
|
func (b *Backoff) Duration() time.Duration {
|
|
b.setup()
|
|
|
|
b.decayN()
|
|
|
|
d := b.duration(b.n)
|
|
|
|
if b.n < math.MaxUint64 {
|
|
b.n++
|
|
}
|
|
|
|
if !b.noJitter {
|
|
d = time.Duration(rand.Int64N(int64(d))) // #nosec G404
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
const maxN uint64 = 63
|
|
|
|
// requires b to be locked.
|
|
func (b *Backoff) duration(n uint64) time.Duration {
|
|
// Use left shift on the underlying integer representation to avoid
|
|
// multiplying time.Duration by time.Duration (which is semantically
|
|
// incorrect and flagged by linters).
|
|
if n >= maxN {
|
|
// Saturate when n would overflow a 64-bit shift or exceed maxDuration.
|
|
return b.maxDuration
|
|
}
|
|
|
|
// Calculate 2^n * interval using a shift. Detect overflow by checking
|
|
// for sign change or monotonicity loss and clamp to maxDuration.
|
|
shifted := b.interval << n
|
|
if shifted < 0 || shifted < b.interval {
|
|
// Overflow occurred during the shift; clamp to maxDuration.
|
|
return b.maxDuration
|
|
}
|
|
|
|
if shifted > b.maxDuration {
|
|
return b.maxDuration
|
|
}
|
|
|
|
return shifted
|
|
}
|
|
|
|
// Reset resets the attempt counter of a backoff.
|
|
//
|
|
// It should be called when the rate-limited action succeeds.
|
|
func (b *Backoff) Reset() {
|
|
b.lastTry = time.Time{}
|
|
b.n = 0
|
|
}
|
|
|
|
// SetDecay sets the duration after which the try counter will be reset.
|
|
// Panics if decay is smaller than 0.
|
|
//
|
|
// The decay only kicks in if at least the last backoff + decay has elapsed
|
|
// since the last try.
|
|
func (b *Backoff) SetDecay(decay time.Duration) {
|
|
if decay < 0 {
|
|
panic("backoff: decay < 0")
|
|
}
|
|
|
|
b.decay = decay
|
|
}
|
|
|
|
// requires b to be locked.
|
|
func (b *Backoff) decayN() {
|
|
if b.decay == 0 {
|
|
return
|
|
}
|
|
|
|
if b.lastTry.IsZero() {
|
|
b.lastTry = time.Now()
|
|
return
|
|
}
|
|
|
|
lastDuration := b.duration(b.n - 1)
|
|
// Reset when the elapsed time is at least the previous backoff plus decay.
|
|
// Using ">=" avoids boundary flakiness in tests and real usage.
|
|
decayed := time.Since(b.lastTry) >= lastDuration+b.decay
|
|
b.lastTry = time.Now()
|
|
|
|
if !decayed {
|
|
return
|
|
}
|
|
|
|
b.n = 0
|
|
}
|