github.com/hashicorp/vault/sdk@v0.11.0/helper/backoff/backoff.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package backoff 5 6 import ( 7 "errors" 8 "math" 9 "math/rand" 10 "time" 11 ) 12 13 var ErrMaxRetry = errors.New("exceeded maximum number of retries") 14 15 const maxJitter = 0.25 16 17 // Backoff is used to do capped exponential backoff with jitter, with a maximum number of retries. 18 // Generally, use this struct by calling Next() or NextSleep() after a failure. 19 // If configured for N max retries, Next() and NextSleep() will return an error on the call N+1. 20 // The jitter is set to 25%, so values returned will have up to 25% less than twice the previous value. 21 // The min value will also include jitter, so the first call will almost always be less than the requested minimum value. 22 // Backoff is not thread-safe. 23 type Backoff struct { 24 currentAttempt int 25 maxRetries int 26 min time.Duration 27 max time.Duration 28 current time.Duration 29 } 30 31 // NewBackoff creates a new exponential backoff with the given number of maximum retries and min/max durations. 32 func NewBackoff(maxRetries int, min, max time.Duration) *Backoff { 33 b := &Backoff{ 34 maxRetries: maxRetries, 35 max: max, 36 min: min, 37 } 38 b.Reset() 39 return b 40 } 41 42 // Current returns the next time that will be returned by Next() (or slept in NextSleep()). 43 func (b *Backoff) Current() time.Duration { 44 return b.current 45 } 46 47 // Next determines the next backoff duration that is roughly twice 48 // the current value, capped to a max value, with a measure of randomness. 49 // It returns an error if there are no more retries left. 50 func (b *Backoff) Next() (time.Duration, error) { 51 if b.currentAttempt >= b.maxRetries { 52 return time.Duration(-1), ErrMaxRetry 53 } 54 defer func() { 55 b.currentAttempt += 1 56 }() 57 if b.currentAttempt == 0 { 58 return b.current, nil 59 } 60 next := 2 * b.current 61 if next > b.max { 62 next = b.max 63 } 64 next = jitter(next) 65 b.current = next 66 return next, nil 67 } 68 69 // NextSleep will synchronously sleep the next backoff amount (see Next()). 70 // It returns an error if there are no more retries left. 71 func (b *Backoff) NextSleep() error { 72 next, err := b.Next() 73 if err != nil { 74 return err 75 } 76 time.Sleep(next) 77 return nil 78 } 79 80 // Reset resets the state to the initial backoff amount and 0 retries. 81 func (b *Backoff) Reset() { 82 b.current = b.min 83 b.current = jitter(b.current) 84 b.currentAttempt = 0 85 } 86 87 func jitter(t time.Duration) time.Duration { 88 f := float64(t) * (1.0 - maxJitter*rand.Float64()) 89 return time.Duration(math.Floor(f)) 90 } 91 92 // Retry calls the given function until it does not return an error, at least once and up to max_retries + 1 times. 93 // If the number of retries is exceeded, Retry() will return the last error seen joined with ErrMaxRetry. 94 func (b *Backoff) Retry(f func() error) error { 95 for { 96 err := f() 97 if err == nil { 98 return nil 99 } 100 101 maxRetryErr := b.NextSleep() 102 if maxRetryErr != nil { 103 return errors.Join(maxRetryErr, err) 104 } 105 } 106 return nil // unreachable 107 }