common/backoff/retrypolicy.go (149 lines of code) (raw):
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package backoff
import (
"math"
"math/rand"
"time"
)
const (
// NoInterval represents Maximim interval
NoInterval = 0
done time.Duration = -1
noMaximumAttempts = 0
defaultBackoffCoefficient = 2.0
defaultMaximumInterval = 10 * time.Second
defaultExpirationInterval = time.Minute
defaultMaximumAttempts = noMaximumAttempts
)
type (
// RetryPolicy is the API which needs to be implemented by various retry policy implementations
RetryPolicy interface {
ComputeNextDelay(elapsedTime time.Duration, numAttempts int) time.Duration
}
// Retrier manages the state of retry operation
Retrier interface {
NextBackOff() time.Duration
Reset()
}
// Clock used by ExponentialRetryPolicy implementation to get the current time. Mainly used for unit testing
Clock interface {
Now() time.Time
}
// ExponentialRetryPolicy provides the implementation for retry policy using a coefficient to compute the next delay.
// Formula used to compute the next delay is: initialInterval * math.Pow(backoffCoefficient, currentAttempt)
ExponentialRetryPolicy struct {
initialInterval time.Duration
backoffCoefficient float64
maximumInterval time.Duration
expirationInterval time.Duration
maximumAttempts int
}
// MultiPhasesRetryPolicy implements a policy that first use one policy to get next delay,
// and once expired use the next policy for the following retry.
// It can achieve fast retries in first phase then slowly retires in second phase.
// The supported retry policy is ExponentialRetryPolicy.
// To have the correct next delay, set the maximumAttempts in the non-final policy.
MultiPhasesRetryPolicy struct {
policies []*ExponentialRetryPolicy
}
systemClock struct{}
retrierImpl struct {
policy RetryPolicy
clock Clock
currentAttempt int
startTime time.Time
}
)
// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}
// NewExponentialRetryPolicy returns an instance of ExponentialRetryPolicy using the provided initialInterval
func NewExponentialRetryPolicy(initialInterval time.Duration) *ExponentialRetryPolicy {
p := &ExponentialRetryPolicy{
initialInterval: initialInterval,
backoffCoefficient: defaultBackoffCoefficient,
maximumInterval: defaultMaximumInterval,
expirationInterval: defaultExpirationInterval,
maximumAttempts: defaultMaximumAttempts,
}
return p
}
// NewMultiPhasesRetryPolicy creates MultiPhasesRetryPolicy
func NewMultiPhasesRetryPolicy(policies ...*ExponentialRetryPolicy) *MultiPhasesRetryPolicy {
for i := 0; i < len(policies)-1; i++ {
if policies[i].maximumAttempts == noMaximumAttempts {
panic("Non final retry policy in MultiPhasesRetryPolicy need to set maximum attempts")
}
}
return &MultiPhasesRetryPolicy{
policies: policies,
}
}
// NewRetrier is used for creating a new instance of Retrier
func NewRetrier(policy RetryPolicy, clock Clock) Retrier {
if policy == nil {
panic("Retry policy cannot be nil.")
}
if clock == nil {
panic("Retry clock cannot be nil.")
}
return &retrierImpl{
policy: policy,
clock: clock,
startTime: clock.Now(),
currentAttempt: 0,
}
}
// SetInitialInterval sets the initial interval used by ExponentialRetryPolicy for the very first retry
// All later retries are computed using the following formula:
// initialInterval * math.Pow(backoffCoefficient, currentAttempt)
func (p *ExponentialRetryPolicy) SetInitialInterval(initialInterval time.Duration) {
p.initialInterval = initialInterval
}
// SetBackoffCoefficient sets the coefficient used by ExponentialRetryPolicy to compute next delay for each retry
// All retries are computed using the following formula:
// initialInterval * math.Pow(backoffCoefficient, currentAttempt)
func (p *ExponentialRetryPolicy) SetBackoffCoefficient(backoffCoefficient float64) {
p.backoffCoefficient = backoffCoefficient
}
// SetMaximumInterval sets the maximum interval for each retry
func (p *ExponentialRetryPolicy) SetMaximumInterval(maximumInterval time.Duration) {
p.maximumInterval = maximumInterval
}
// SetExpirationInterval sets the absolute expiration interval for all retries
func (p *ExponentialRetryPolicy) SetExpirationInterval(expirationInterval time.Duration) {
p.expirationInterval = expirationInterval
}
// SetMaximumAttempts sets the maximum number of retry attempts
func (p *ExponentialRetryPolicy) SetMaximumAttempts(maximumAttempts int) {
p.maximumAttempts = maximumAttempts
}
// ComputeNextDelay returns the next delay interval. This is used by Retrier to delay calling the operation again
func (p *ExponentialRetryPolicy) ComputeNextDelay(elapsedTime time.Duration, numAttempts int) time.Duration {
// Check to see if we ran out of maximum number of attempts
if p.maximumAttempts != noMaximumAttempts && numAttempts >= p.maximumAttempts {
return done
}
// Stop retrying after expiration interval is elapsed
if p.expirationInterval != NoInterval && elapsedTime > p.expirationInterval {
return done
}
nextInterval := float64(p.initialInterval) * math.Pow(p.backoffCoefficient, float64(numAttempts))
// Disallow retries if initialInterval is negative or nextInterval overflows
if nextInterval <= 0 {
return done
}
if p.maximumInterval != NoInterval {
nextInterval = math.Min(nextInterval, float64(p.maximumInterval))
}
if p.expirationInterval != NoInterval {
remainingTime := float64(math.Max(0, float64(p.expirationInterval-elapsedTime)))
nextInterval = math.Min(remainingTime, nextInterval)
}
// Bail out if the next interval is smaller than initial retry interval
nextDuration := time.Duration(nextInterval)
if nextDuration < p.initialInterval {
return done
}
// add jitter to avoid global synchronization
jitterPortion := int(0.2 * nextInterval)
// Prevent overflow
if jitterPortion < 1 {
jitterPortion = 1
}
nextInterval = nextInterval*0.8 + float64(rand.Intn(jitterPortion))
return time.Duration(nextInterval)
}
// ComputeNextDelay returns the next delay interval.
func (tp MultiPhasesRetryPolicy) ComputeNextDelay(elapsedTime time.Duration, numAttempts int) time.Duration {
previousStageRetryCount := 0
for _, policy := range tp.policies {
nextInterval := policy.ComputeNextDelay(elapsedTime, numAttempts-previousStageRetryCount)
if nextInterval != done {
return nextInterval
}
previousStageRetryCount += policy.maximumAttempts
}
return done
}
// Now returns the current time using the system clock
func (t systemClock) Now() time.Time {
return time.Now()
}
// Reset will set the Retrier into initial state
func (r *retrierImpl) Reset() {
r.startTime = r.clock.Now()
r.currentAttempt = 0
}
// NextBackOff returns the next delay interval. This is used by Retry to delay calling the operation again
func (r *retrierImpl) NextBackOff() time.Duration {
nextInterval := r.policy.ComputeNextDelay(r.getElapsedTime(), r.currentAttempt)
// Now increment the current attempt
r.currentAttempt++
return nextInterval
}
func (r *retrierImpl) getElapsedTime() time.Duration {
return r.clock.Now().Sub(r.startTime)
}