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 }