157 lines
5.4 KiB
Go
157 lines
5.4 KiB
Go
|
package backoff
|
||
|
|
||
|
import (
|
||
|
"math/rand"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
/*
|
||
|
ExponentialBackOff is a backoff implementation that increases the backoff
|
||
|
period for each retry attempt using a randomization function that grows exponentially.
|
||
|
|
||
|
NextBackOff() is calculated using the following formula:
|
||
|
|
||
|
randomized interval =
|
||
|
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
|
||
|
|
||
|
In other words NextBackOff() will range between the randomization factor
|
||
|
percentage below and above the retry interval.
|
||
|
|
||
|
For example, given the following parameters:
|
||
|
|
||
|
RetryInterval = 2
|
||
|
RandomizationFactor = 0.5
|
||
|
Multiplier = 2
|
||
|
|
||
|
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
|
||
|
multiplied by the exponential, that is, between 2 and 6 seconds.
|
||
|
|
||
|
Note: MaxInterval caps the RetryInterval and not the randomized interval.
|
||
|
|
||
|
If the time elapsed since an ExponentialBackOff instance is created goes past the
|
||
|
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
|
||
|
|
||
|
The elapsed time can be reset by calling Reset().
|
||
|
|
||
|
Example: Given the following default arguments, for 10 tries the sequence will be,
|
||
|
and assuming we go over the MaxElapsedTime on the 10th try:
|
||
|
|
||
|
Request # RetryInterval (seconds) Randomized Interval (seconds)
|
||
|
|
||
|
1 0.5 [0.25, 0.75]
|
||
|
2 0.75 [0.375, 1.125]
|
||
|
3 1.125 [0.562, 1.687]
|
||
|
4 1.687 [0.8435, 2.53]
|
||
|
5 2.53 [1.265, 3.795]
|
||
|
6 3.795 [1.897, 5.692]
|
||
|
7 5.692 [2.846, 8.538]
|
||
|
8 8.538 [4.269, 12.807]
|
||
|
9 12.807 [6.403, 19.210]
|
||
|
10 19.210 backoff.Stop
|
||
|
|
||
|
Note: Implementation is not thread-safe.
|
||
|
*/
|
||
|
type ExponentialBackOff struct {
|
||
|
InitialInterval time.Duration
|
||
|
RandomizationFactor float64
|
||
|
Multiplier float64
|
||
|
MaxInterval time.Duration
|
||
|
// After MaxElapsedTime the ExponentialBackOff stops.
|
||
|
// It never stops if MaxElapsedTime == 0.
|
||
|
MaxElapsedTime time.Duration
|
||
|
Clock Clock
|
||
|
|
||
|
currentInterval time.Duration
|
||
|
startTime time.Time
|
||
|
}
|
||
|
|
||
|
// Clock is an interface that returns current time for BackOff.
|
||
|
type Clock interface {
|
||
|
Now() time.Time
|
||
|
}
|
||
|
|
||
|
// Default values for ExponentialBackOff.
|
||
|
const (
|
||
|
DefaultInitialInterval = 500 * time.Millisecond
|
||
|
DefaultRandomizationFactor = 0.5
|
||
|
DefaultMultiplier = 1.5
|
||
|
DefaultMaxInterval = 60 * time.Second
|
||
|
DefaultMaxElapsedTime = 15 * time.Minute
|
||
|
)
|
||
|
|
||
|
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
|
||
|
func NewExponentialBackOff() *ExponentialBackOff {
|
||
|
b := &ExponentialBackOff{
|
||
|
InitialInterval: DefaultInitialInterval,
|
||
|
RandomizationFactor: DefaultRandomizationFactor,
|
||
|
Multiplier: DefaultMultiplier,
|
||
|
MaxInterval: DefaultMaxInterval,
|
||
|
MaxElapsedTime: DefaultMaxElapsedTime,
|
||
|
Clock: SystemClock,
|
||
|
}
|
||
|
if b.RandomizationFactor < 0 {
|
||
|
b.RandomizationFactor = 0
|
||
|
} else if b.RandomizationFactor > 1 {
|
||
|
b.RandomizationFactor = 1
|
||
|
}
|
||
|
b.Reset()
|
||
|
return b
|
||
|
}
|
||
|
|
||
|
type systemClock struct{}
|
||
|
|
||
|
func (t systemClock) Now() time.Time {
|
||
|
return time.Now()
|
||
|
}
|
||
|
|
||
|
// SystemClock implements Clock interface that uses time.Now().
|
||
|
var SystemClock = systemClock{}
|
||
|
|
||
|
// Reset the interval back to the initial retry interval and restarts the timer.
|
||
|
func (b *ExponentialBackOff) Reset() {
|
||
|
b.currentInterval = b.InitialInterval
|
||
|
b.startTime = b.Clock.Now()
|
||
|
}
|
||
|
|
||
|
// NextBackOff calculates the next backoff interval using the formula:
|
||
|
// Randomized interval = RetryInterval +/- (RandomizationFactor * RetryInterval)
|
||
|
func (b *ExponentialBackOff) NextBackOff() time.Duration {
|
||
|
// Make sure we have not gone over the maximum elapsed time.
|
||
|
if b.MaxElapsedTime != 0 && b.GetElapsedTime() > b.MaxElapsedTime {
|
||
|
return Stop
|
||
|
}
|
||
|
defer b.incrementCurrentInterval()
|
||
|
return getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
|
||
|
}
|
||
|
|
||
|
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
|
||
|
// is created and is reset when Reset() is called.
|
||
|
//
|
||
|
// The elapsed time is computed using time.Now().UnixNano().
|
||
|
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
|
||
|
return b.Clock.Now().Sub(b.startTime)
|
||
|
}
|
||
|
|
||
|
// Increments the current interval by multiplying it with the multiplier.
|
||
|
func (b *ExponentialBackOff) incrementCurrentInterval() {
|
||
|
// Check for overflow, if overflow is detected set the current interval to the max interval.
|
||
|
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
|
||
|
b.currentInterval = b.MaxInterval
|
||
|
} else {
|
||
|
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Returns a random value from the following interval:
|
||
|
// [randomizationFactor * currentInterval, randomizationFactor * currentInterval].
|
||
|
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
|
||
|
var delta = randomizationFactor * float64(currentInterval)
|
||
|
var minInterval = float64(currentInterval) - delta
|
||
|
var maxInterval = float64(currentInterval) + delta
|
||
|
|
||
|
// Get a random value from the range [minInterval, maxInterval].
|
||
|
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
|
||
|
// we want a 33% chance for selecting either 1, 2 or 3.
|
||
|
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
|
||
|
}
|