github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/retryutils/retryv2.go (about)

     1  /*
     2  Copyright 2019-2022 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package retryutils defines common retry and jitter logic.
    18  package retryutils
    19  
    20  import (
    21  	"time"
    22  
    23  	"github.com/gravitational/trace"
    24  	"github.com/jonboulle/clockwork"
    25  )
    26  
    27  // maxBackoff is an absolute maximum amount of backoff that our backoff helpers will
    28  // apply. Used as a safety precaution to limit the impact of misconfigured backoffs.
    29  const maxBackoff = 16 * time.Minute
    30  
    31  // maxAttempts is the peak attempt number we will scale to (used to prevent overflows).
    32  const maxAttempts = 16
    33  
    34  // statically assert that we don't overflow.
    35  const _ = maxBackoff << (maxAttempts - 1)
    36  
    37  // statically assert that RetryV2 implements the Retry interface.
    38  var _ Retry = (*RetryV2)(nil)
    39  
    40  // driver is the underlying retry driver. determines the difference in behavior between
    41  // linear/exponential retries.
    42  //
    43  // NOTE: drivers must be stateless. If a stateful driver needs to be implemented in the
    44  // future, this interface will need to be extended to support safe use of Retry.Clone.
    45  type Driver interface {
    46  	// Duration calculates the step-specific delay for a given attempt. Excludes
    47  	// base duration and jitter, which are applied by the outer retry instance.
    48  	Duration(attempt int64) time.Duration
    49  
    50  	// Check verifies the correctness of any driver-internal parameters.
    51  	Check() error
    52  }
    53  
    54  // NewLinearDriver creates a linear retry driver with the supplied step value. Resulting retries
    55  // have increase their backoff by a fixed step amount on each increment, with the first retry
    56  // having a base step amount of zero.
    57  func NewLinearDriver(step time.Duration) Driver {
    58  	return linearDriver{step}
    59  }
    60  
    61  type linearDriver struct {
    62  	step time.Duration
    63  }
    64  
    65  func (d linearDriver) Duration(attempt int64) time.Duration {
    66  	dur := d.step * time.Duration(attempt)
    67  	if dur > maxBackoff {
    68  		return maxBackoff
    69  	}
    70  	return dur
    71  }
    72  
    73  func (d linearDriver) Check() error {
    74  	if d.step <= 0 {
    75  		return trace.BadParameter("linear driver requires positive step value")
    76  	}
    77  
    78  	if d.step > maxBackoff {
    79  		return trace.BadParameter("linear backoff step value too large: %v (max=%v)", d.step, maxBackoff)
    80  	}
    81  	return nil
    82  }
    83  
    84  // NewExponentialDriver creates a new exponential retry driver with the supplied base
    85  // step value. Resulting retries double their base backoff on each increment.
    86  func NewExponentialDriver(base time.Duration) Driver {
    87  	return exponentialDriver{base}
    88  }
    89  
    90  type exponentialDriver struct {
    91  	base time.Duration
    92  }
    93  
    94  func (d exponentialDriver) Duration(attempt int64) time.Duration {
    95  	if attempt > maxAttempts {
    96  		// 16 will exceed any reasonable Max value already, and we don't
    97  		// want to accidentally wrap and end up w/ negative durations.
    98  		attempt = 16
    99  	}
   100  
   101  	// in order to maintain consistency with existing linear behavior, the first attempt
   102  	// results in a base duration of 0.
   103  	if attempt <= 0 {
   104  		return 0
   105  	}
   106  
   107  	// duration calculated as step * the square of the attempt number
   108  	dur := d.base << (attempt - 1)
   109  
   110  	if dur > maxBackoff {
   111  		return maxBackoff
   112  	}
   113  
   114  	return dur
   115  }
   116  
   117  func (d exponentialDriver) Check() error {
   118  	if d.base <= 0 {
   119  		return trace.BadParameter("exponential driver requires positive base")
   120  	}
   121  
   122  	if d.base > maxBackoff {
   123  		return trace.BadParameter("exponential backoff base too large: %v (max=%v)", d.base, maxBackoff)
   124  	}
   125  	return nil
   126  }
   127  
   128  // RetryV2Config sets up retry configuration
   129  // using arithmetic progression
   130  type RetryV2Config struct {
   131  	// First is a first element of the progression,
   132  	// could be 0
   133  	First time.Duration
   134  	// Driver generates the underlying progression of delays. Cannot be nil.
   135  	Driver Driver
   136  	// Max is a maximum value of the progression,
   137  	// can't be 0
   138  	Max time.Duration
   139  	// Jitter is an optional jitter function to be applied
   140  	// to the delay.  Note that supplying a jitter means that
   141  	// successive calls to Duration may return different results.
   142  	Jitter Jitter `json:"-"`
   143  	// AutoReset, if greater than zero, causes the linear retry to automatically
   144  	// reset after Max * AutoReset has elapsed since the last call to Incr.
   145  	AutoReset int64
   146  	// Clock to override clock in tests
   147  	Clock clockwork.Clock
   148  }
   149  
   150  // CheckAndSetDefaults checks and sets defaults
   151  func (c *RetryV2Config) CheckAndSetDefaults() error {
   152  	if c.Driver == nil {
   153  		return trace.BadParameter("missing parameter Driver")
   154  	}
   155  	if err := c.Driver.Check(); err != nil {
   156  		return trace.Wrap(err)
   157  	}
   158  	if c.Max == 0 {
   159  		return trace.BadParameter("missing parameter Max")
   160  	}
   161  	if c.Clock == nil {
   162  		c.Clock = clockwork.NewRealClock()
   163  	}
   164  	return nil
   165  }
   166  
   167  // NewRetryV2 returns a new retry instance.
   168  func NewRetryV2(cfg RetryV2Config) (*RetryV2, error) {
   169  	if err := cfg.CheckAndSetDefaults(); err != nil {
   170  		return nil, trace.Wrap(err)
   171  	}
   172  	return newRetryV2(cfg), nil
   173  }
   174  
   175  // newRetryV2 creates an instance of RetryV2 from a
   176  // previously verified configuration.
   177  func newRetryV2(cfg RetryV2Config) *RetryV2 {
   178  	return &RetryV2{RetryV2Config: cfg}
   179  }
   180  
   181  // RetryV2 is used to moderate the rate of retries by applying successively increasing
   182  // delays. The nature of the progression is determined by the 'Driver', which generates
   183  // the portion of the delay corresponding to the attempt number (e.g. Exponential(1s) might
   184  // generate the sequence 0s, 1s, 2s, 4s, 8s, etc). This progression is can be modified through
   185  // the use of a custom base/start value, jitters, etc.
   186  type RetryV2 struct {
   187  	// RetryV2Config is a linear retry config
   188  	RetryV2Config
   189  	lastUse time.Time
   190  	attempt int64
   191  }
   192  
   193  // Reset resets retry period to initial state
   194  func (r *RetryV2) Reset() {
   195  	r.attempt = 0
   196  }
   197  
   198  // Clone creates an identical copy of RetryV2 with fresh state.
   199  func (r *RetryV2) Clone() Retry {
   200  	return newRetryV2(r.RetryV2Config)
   201  }
   202  
   203  // Inc increments attempt counter
   204  func (r *RetryV2) Inc() {
   205  	r.attempt++
   206  }
   207  
   208  // Duration returns retry duration based on state
   209  func (r *RetryV2) Duration() time.Duration {
   210  	if r.AutoReset > 0 {
   211  		now := r.Clock.Now()
   212  		if now.After(r.lastUse.Add(r.Max * time.Duration(r.AutoReset))) {
   213  			r.Reset()
   214  		}
   215  		r.lastUse = now
   216  	}
   217  
   218  	a := r.First + r.Driver.Duration(r.attempt)
   219  	if a < 1 {
   220  		return 0
   221  	}
   222  
   223  	if a > r.Max {
   224  		a = r.Max
   225  	}
   226  
   227  	if r.Jitter != nil {
   228  		a = r.Jitter(a)
   229  	}
   230  
   231  	return a
   232  }
   233  
   234  // After returns channel that fires with timeout
   235  // defined in Duration method.
   236  func (r *RetryV2) After() <-chan time.Time {
   237  	return r.Clock.After(r.Duration())
   238  }