go.uber.org/yarpc@v1.72.1/internal/backoff/exponential.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package backoff
    22  
    23  import (
    24  	"errors"
    25  	"math/rand"
    26  	"time"
    27  
    28  	"go.uber.org/multierr"
    29  	"go.uber.org/yarpc/api/backoff"
    30  )
    31  
    32  var (
    33  	errInvalidFirst = errors.New("invalid first duration for exponential backoff, need greater than zero")
    34  	errInvalidMax   = errors.New("invalid max for exponential backoff, need greater than or equal to zero")
    35  )
    36  
    37  // ExponentialOption defines options that can be applied to an
    38  // exponential backoff strategy
    39  type ExponentialOption func(*exponentialOptions)
    40  
    41  // exponentialOptions are the configuration options for an exponential backoff
    42  type exponentialOptions struct {
    43  	first, max time.Duration
    44  	newRand    func() *rand.Rand
    45  }
    46  
    47  func (e exponentialOptions) validate() (err error) {
    48  	if e.first <= 0 {
    49  		err = multierr.Append(err, errInvalidFirst)
    50  	}
    51  	if e.max < 0 {
    52  		err = multierr.Append(err, errInvalidMax)
    53  	}
    54  	return err
    55  }
    56  
    57  func newRand() *rand.Rand {
    58  	return rand.New(rand.NewSource(time.Now().UnixNano()))
    59  }
    60  
    61  var defaultExponentialOpts = exponentialOptions{
    62  	first:   10 * time.Millisecond,
    63  	max:     time.Minute,
    64  	newRand: newRand,
    65  }
    66  
    67  // DefaultExponential is an exponential backoff.Strategy with full jitter.
    68  // The first attempt has a range of 0 to 10ms and each successive attempt
    69  // doubles the range of the possible delay.
    70  //
    71  // Exponential strategies are not thread safe.
    72  // The Backoff() method returns a referentially independent backoff generator
    73  // and random number generator.
    74  var DefaultExponential = &ExponentialStrategy{
    75  	opts: defaultExponentialOpts,
    76  }
    77  
    78  // FirstBackoff sets the initial range of durations that the first backoff
    79  // duration will provide.
    80  // The range of durations will double for each successive attempt.
    81  func FirstBackoff(t time.Duration) ExponentialOption {
    82  	return func(options *exponentialOptions) {
    83  		options.first = t
    84  	}
    85  }
    86  
    87  // MaxBackoff sets absolute max time that will ever be returned for a backoff.
    88  func MaxBackoff(t time.Duration) ExponentialOption {
    89  	return func(options *exponentialOptions) {
    90  		options.max = t
    91  	}
    92  }
    93  
    94  // randGenerator is an internal option for overriding the random number
    95  // generator.
    96  func randGenerator(newRand func() *rand.Rand) ExponentialOption {
    97  	return func(options *exponentialOptions) {
    98  		options.newRand = newRand
    99  	}
   100  }
   101  
   102  // ExponentialStrategy can create instances of the exponential backoff strategy
   103  // with full jitter.
   104  // Each instance has referentially independent random number generators.
   105  type ExponentialStrategy struct {
   106  	opts exponentialOptions
   107  }
   108  
   109  var _ backoff.Strategy = (*ExponentialStrategy)(nil)
   110  
   111  // NewExponential returns a new exponential backoff strategy, which in turn
   112  // returns backoff functions.
   113  //
   114  // Exponential is an exponential backoff strategy with jitter.  Under the
   115  // AWS backoff strategies this is a "Full Jitter" backoff implementation
   116  // https://www.awsarchitectureblog.com/2015/03/backoff.html with the addition
   117  // of a Min and Max Value.  The range of durations will be contained in
   118  // a closed [Min, Max] interval.
   119  //
   120  // Backoff functions are lockless and referentially independent, but not
   121  // thread-safe.
   122  func NewExponential(opts ...ExponentialOption) (*ExponentialStrategy, error) {
   123  	options := defaultExponentialOpts
   124  	for _, opt := range opts {
   125  		opt(&options)
   126  	}
   127  
   128  	if err := options.validate(); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	return &ExponentialStrategy{
   133  		opts: options,
   134  	}, nil
   135  }
   136  
   137  // Backoff returns an instance of the exponential backoff strategy with its own
   138  // random number generator.
   139  func (e *ExponentialStrategy) Backoff() backoff.Backoff {
   140  	return &exponentialBackoff{
   141  		first: e.opts.first,
   142  		max:   e.opts.max.Nanoseconds(),
   143  		rand:  e.opts.newRand(),
   144  	}
   145  }
   146  
   147  // IsEqual returns whether this strategy is equivalent to another strategy.
   148  func (e *ExponentialStrategy) IsEqual(o *ExponentialStrategy) bool {
   149  	if e.opts.first != o.opts.first {
   150  		return false
   151  	}
   152  	if e.opts.max != o.opts.max {
   153  		return false
   154  	}
   155  	return true
   156  }
   157  
   158  // ExponentialBackoff is an instance of the exponential backoff strategy with
   159  // full jitter.
   160  type exponentialBackoff struct {
   161  	first time.Duration
   162  	max   int64
   163  	rand  *rand.Rand
   164  }
   165  
   166  // Duration takes an attempt number and returns the duration the caller should
   167  // wait.
   168  func (e *exponentialBackoff) Duration(attempts uint) time.Duration {
   169  	spread := (1 << attempts) * e.first.Nanoseconds()
   170  	if spread <= 0 || spread > e.max {
   171  		spread = e.max
   172  	}
   173  	// Adding 1 to the spread ensures that the upper bound of the range of
   174  	// possible durations includes the maximum.
   175  	return time.Duration(e.rand.Int63n(spread + 1))
   176  }