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  }