github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/backoff/backoff.go (about) 1 // Package backoff implements backoff algorithms for retrying operations. 2 // 3 // Use Retry function for retrying operations that may fail. 4 // If Retry does not meet your needs, 5 // copy/paste the function into your project and modify as you wish. 6 // 7 // There is also Ticker type similar to time.Ticker. 8 // You can use it if you need to work with channels. 9 // 10 // See Examples section below for usage examples. 11 package backoff 12 13 import ( 14 "context" 15 "errors" 16 "math/rand" 17 "sync" 18 "time" 19 ) 20 21 // BackOff is a backoff policy for retrying an operation. 22 type BackOff interface { 23 // NextBackOff returns the duration to wait before retrying the operation, 24 // or backoff. Stop to indicate that no more retries should be made. 25 // 26 // Example usage: 27 // 28 // duration := backoff.NextBackOff(); 29 // if (duration == backoff.Stop) { 30 // // Do not retry operation. 31 // } else { 32 // // Sleep for duration and retry operation. 33 // } 34 // 35 NextBackOff() time.Duration 36 37 // Reset to initial state. 38 Reset() 39 } 40 41 // Stop indicates that no more retries should be made for use in NextBackOff(). 42 const Stop time.Duration = -1 43 44 // ZeroBackOff is a fixed backoff policy whose backoff time is always zero, 45 // meaning that the operation is retried immediately without waiting, indefinitely. 46 type ZeroBackOff struct{} 47 48 func (b *ZeroBackOff) Reset() {} 49 50 func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } 51 52 // StopBackOff is a fixed backoff policy that always returns backoff.Stop for 53 // NextBackOff(), meaning that the operation should never be retried. 54 type StopBackOff struct{} 55 56 func (b *StopBackOff) Reset() {} 57 58 func (b *StopBackOff) NextBackOff() time.Duration { return Stop } 59 60 // ConstantBackOff is a backoff policy that always returns the same backoff delay. 61 // This is in contrast to an exponential backoff policy, 62 // which returns a delay that grows longer as you call NextBackOff() over and over again. 63 type ConstantBackOff struct { 64 Interval time.Duration 65 } 66 67 func (b *ConstantBackOff) Reset() {} 68 func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } 69 70 func NewConstantBackOff(d time.Duration) *ConstantBackOff { 71 return &ConstantBackOff{Interval: d} 72 } 73 74 // Context is a backoff policy that stops retrying after the context 75 // is canceled. 76 type Context interface { // nolint: golint 77 BackOff 78 Context() context.Context 79 } 80 81 type backOffContext struct { 82 BackOff 83 ctx context.Context 84 } 85 86 // WithContext returns a Context with context ctx 87 // 88 // ctx must not be nil 89 func WithContext(b BackOff, ctx context.Context) Context { // nolint: golint 90 if ctx == nil { 91 panic("nil context") 92 } 93 94 if b, ok := b.(*backOffContext); ok { 95 return &backOffContext{BackOff: b.BackOff, ctx: ctx} 96 } 97 98 return &backOffContext{BackOff: b, ctx: ctx} 99 } 100 101 func getContext(b BackOff) context.Context { 102 if cb, ok := b.(Context); ok { 103 return cb.Context() 104 } 105 if tb, ok := b.(*backOffTries); ok { 106 return getContext(tb.delegate) 107 } 108 return context.Background() 109 } 110 111 func (b *backOffContext) Context() context.Context { return b.ctx } 112 113 func (b *backOffContext) NextBackOff() time.Duration { 114 select { 115 case <-b.ctx.Done(): 116 return Stop 117 default: 118 return b.BackOff.NextBackOff() 119 } 120 } 121 122 /* 123 ExponentialBackOff is a backoff implementation that increases the backoff 124 period for each retry attempt using a randomization function that grows exponentially. 125 126 NextBackOff() is calculated using the following formula: 127 128 randomized interval = 129 RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) 130 131 In other words NextBackOff() will range between the randomization factor 132 percentage below and above the retry interval. 133 134 For example, given the following parameters: 135 136 RetryInterval = 2 137 RandomizationFactor = 0.5 138 Multiplier = 2 139 140 the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, 141 multiplied by the exponential, that is, between 2 and 6 seconds. 142 143 Note: MaxInterval caps the RetryInterval and not the randomized interval. 144 145 If the time elapsed since an ExponentialBackOff instance is created goes past the 146 MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. 147 148 The elapsed time can be reset by calling Reset(). 149 150 Example: Given the following default arguments, for 10 tries the sequence will be, 151 and assuming we go over the MaxElapsedTime on the 10th try: 152 153 Request # RetryInterval (seconds) Randomized Interval (seconds) 154 155 1 0.5 [0.25, 0.75] 156 2 0.75 [0.375, 1.125] 157 3 1.125 [0.562, 1.687] 158 4 1.687 [0.8435, 2.53] 159 5 2.53 [1.265, 3.795] 160 6 3.795 [1.897, 5.692] 161 7 5.692 [2.846, 8.538] 162 8 8.538 [4.269, 12.807] 163 9 12.807 [6.403, 19.210] 164 10 19.210 backoff.Stop 165 166 Note: Implementation is not thread-safe. 167 */ 168 type ExponentialBackOff struct { 169 InitialInterval time.Duration 170 RandomizationFactor float64 171 Multiplier float64 172 MaxInterval time.Duration 173 // After MaxElapsedTime the ExponentialBackOff returns Stop. 174 // It never stops if MaxElapsedTime == 0. 175 MaxElapsedTime time.Duration 176 Stop time.Duration 177 Clock Clock 178 179 currentInterval time.Duration 180 startTime time.Time 181 } 182 183 // Clock is an interface that returns current time for BackOff. 184 type Clock interface { 185 Now() time.Time 186 } 187 188 // Default values for ExponentialBackOff. 189 const ( 190 DefaultInitialInterval = 500 * time.Millisecond 191 DefaultRandomizationFactor = 0.5 192 DefaultMultiplier = 1.5 193 DefaultMaxInterval = 60 * time.Second 194 DefaultMaxElapsedTime = 15 * time.Minute 195 ) 196 197 // NewExponentialBackOff creates an instance of ExponentialBackOff using default values. 198 func NewExponentialBackOff() *ExponentialBackOff { 199 b := &ExponentialBackOff{ 200 InitialInterval: DefaultInitialInterval, 201 RandomizationFactor: DefaultRandomizationFactor, 202 Multiplier: DefaultMultiplier, 203 MaxInterval: DefaultMaxInterval, 204 MaxElapsedTime: DefaultMaxElapsedTime, 205 Stop: Stop, 206 Clock: SystemClock, 207 } 208 b.Reset() 209 return b 210 } 211 212 type systemClock struct{} 213 214 func (t systemClock) Now() time.Time { return time.Now() } 215 216 // SystemClock implements Clock interface that uses time.Now(). 217 var SystemClock = systemClock{} 218 219 // Reset the interval back to the initial retry interval and restarts the timer. 220 // Reset must be called before using b. 221 func (b *ExponentialBackOff) Reset() { 222 b.currentInterval = b.InitialInterval 223 b.startTime = b.Clock.Now() 224 } 225 226 // NextBackOff calculates the next backoff interval using the formula: 227 // 228 // Randomized interval = RetryInterval * (1 ± RandomizationFactor) 229 func (b *ExponentialBackOff) NextBackOff() time.Duration { 230 // Make sure we have not gone over the maximum elapsed time. 231 elapsed := b.GetElapsedTime() 232 next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) 233 b.incrementCurrentInterval() 234 if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime { 235 return b.Stop 236 } 237 return next 238 } 239 240 // GetElapsedTime returns the elapsed time since an ExponentialBackOff instance 241 // is created and is reset when Reset() is called. 242 // 243 // The elapsed time is computed using time.Now().UnixNano(). It is 244 // safe to call even while the backoff policy is used by a running 245 // ticker. 246 func (b *ExponentialBackOff) GetElapsedTime() time.Duration { 247 return b.Clock.Now().Sub(b.startTime) 248 } 249 250 // Increments the current interval by multiplying it with the multiplier. 251 func (b *ExponentialBackOff) incrementCurrentInterval() { 252 // Check for overflow, if overflow is detected set the current interval to the max interval. 253 if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { 254 b.currentInterval = b.MaxInterval 255 } else { 256 b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) 257 } 258 } 259 260 // Returns a random value from the following interval: 261 // 262 // [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval]. 263 func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { 264 if randomizationFactor == 0 { 265 return currentInterval // make sure no randomness is used when randomizationFactor is 0. 266 } 267 delta := randomizationFactor * float64(currentInterval) 268 minInterval := float64(currentInterval) - delta 269 maxInterval := float64(currentInterval) + delta 270 271 // Get a random value from the range [minInterval, maxInterval]. 272 // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then 273 // we want a 33% chance for selecting either 1, 2 or 3. 274 return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) 275 } 276 277 // An Operation is executing by Retry() or RetryNotify(). 278 // The operation will be retried using a backoff policy if it returns an error. 279 type Operation func(retryTimes int) error 280 281 // Notify is a notify-on-error function. It receives an operation error and 282 // backoff delay if the operation failed (with an error) or recovered (err = nil). 283 // 284 // NOTE that if the backoff policy stated to stop retrying, 285 // the notify function isn't called. 286 type Notify func(retryTimes int, err error, nextBackOff time.Duration) 287 288 // Retry the operation o until it does not return error or BackOff stops. 289 // o is guaranteed to be run at least once. 290 // 291 // If o returns a *PermanentError, the operation is not retried, and the 292 // wrapped error is returned. 293 // 294 // Retry sleeps the goroutine for the duration returned by BackOff after a 295 // failed operation returns. 296 func Retry(o Operation, b BackOff) error { 297 return RetryNotify(o, b, nil) 298 } 299 300 // RetryNotify calls notify function with the error and wait duration 301 // for each failed attempt before sleep. 302 func RetryNotify(operation Operation, b BackOff, notify Notify) error { 303 return RetryNotifyWithTimer(operation, b, notify, nil) 304 } 305 306 // RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer 307 // for each failed attempt before sleep. 308 // A default timer that uses system timer is used when nil is passed. 309 func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error { 310 var err error 311 var next time.Duration 312 if t == nil { 313 t = &defaultTimer{} 314 } 315 316 defer func() { 317 t.Stop() 318 }() 319 320 ctx := getContext(b) 321 322 b.Reset() 323 324 for retryTimes := 0; ; retryTimes++ { 325 if err = operation(retryTimes); err == nil { 326 if retryTimes > 0 && notify != nil { 327 notify(retryTimes, err, 0) 328 } 329 return nil 330 } 331 332 var permanent *PermanentError 333 if errors.As(err, &permanent) { 334 return permanent.Err 335 } 336 337 if next = b.NextBackOff(); next == Stop { 338 if cerr := ctx.Err(); cerr != nil { 339 return cerr 340 } 341 342 return err 343 } 344 345 if notify != nil { 346 notify(retryTimes, err, next) 347 } 348 349 t.Start(next) 350 351 select { 352 case <-ctx.Done(): 353 return ctx.Err() 354 case <-t.C(): 355 } 356 } 357 } 358 359 // PermanentError signals that the operation should not be retried. 360 type PermanentError struct { 361 Err error 362 } 363 364 func (e *PermanentError) Error() string { return e.Err.Error() } 365 func (e *PermanentError) Unwrap() error { return e.Err } 366 func (e *PermanentError) Is(target error) bool { _, ok := target.(*PermanentError); return ok } 367 368 // Permanent wraps the given err in a *PermanentError. 369 func Permanent(err error) error { 370 if err == nil { 371 return nil 372 } 373 return &PermanentError{Err: err} 374 } 375 376 // Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff. 377 // 378 // Ticks will continue to arrive when the previous operation is still running, 379 // so operations that take a while to fail could run in quick succession. 380 type Ticker struct { 381 C <-chan time.Time 382 c chan time.Time 383 b BackOff 384 ctx context.Context 385 timer Timer 386 stop chan struct{} 387 stopOnce sync.Once 388 } 389 390 // NewTicker returns a new Ticker containing a channel that will send 391 // the time at times specified by the BackOff argument. Ticker is 392 // guaranteed to tick at least once. The channel is closed when Stop 393 // method is called or BackOff stops. It is not safe to manipulate the 394 // provided backoff policy (notably calling NextBackOff or Reset) 395 // while the ticker is running. 396 func NewTicker(b BackOff) *Ticker { 397 return NewTickerWithTimer(b, &defaultTimer{}) 398 } 399 400 // NewTickerWithTimer returns a new Ticker with a custom timer. 401 // A default timer that uses system timer is used when nil is passed. 402 func NewTickerWithTimer(b BackOff, timer Timer) *Ticker { 403 if timer == nil { 404 timer = &defaultTimer{} 405 } 406 c := make(chan time.Time) 407 t := &Ticker{ 408 C: c, 409 c: c, 410 b: b, 411 ctx: getContext(b), 412 timer: timer, 413 stop: make(chan struct{}), 414 } 415 t.b.Reset() 416 go t.run() 417 return t 418 } 419 420 // Stop turns off a ticker. After Stop, no more ticks will be sent. 421 func (t *Ticker) Stop() { 422 t.stopOnce.Do(func() { close(t.stop) }) 423 } 424 425 func (t *Ticker) run() { 426 c := t.c 427 defer close(c) 428 429 // Ticker is guaranteed to tick at least once. 430 afterC := t.send(time.Now()) 431 432 for { 433 if afterC == nil { 434 return 435 } 436 437 select { 438 case tick := <-afterC: 439 afterC = t.send(tick) 440 case <-t.stop: 441 t.c = nil // Prevent future ticks from being sent to the channel. 442 return 443 case <-t.ctx.Done(): 444 return 445 } 446 } 447 } 448 449 func (t *Ticker) send(tick time.Time) <-chan time.Time { 450 select { 451 case t.c <- tick: 452 case <-t.stop: 453 return nil 454 } 455 456 next := t.b.NextBackOff() 457 if next == Stop { 458 t.Stop() 459 return nil 460 } 461 462 t.timer.Start(next) 463 return t.timer.C() 464 } 465 466 type Timer interface { 467 Start(duration time.Duration) 468 Stop() 469 C() <-chan time.Time 470 } 471 472 // defaultTimer implements Timer interface using time.Timer 473 type defaultTimer struct { 474 timer *time.Timer 475 } 476 477 // C returns the timers channel which receives the current time when the timer fires. 478 func (t *defaultTimer) C() <-chan time.Time { 479 return t.timer.C 480 } 481 482 // Start starts the timer to fire after the given duration 483 func (t *defaultTimer) Start(duration time.Duration) { 484 if t.timer == nil { 485 t.timer = time.NewTimer(duration) 486 } else { 487 t.timer.Reset(duration) 488 } 489 } 490 491 // Stop is called when the timer is not used anymore and resources may be freed. 492 func (t *defaultTimer) Stop() { 493 if t.timer != nil { 494 t.timer.Stop() 495 } 496 } 497 498 /* 499 WithMaxRetries creates a wrapper around another BackOff, which will 500 return Stop if NextBackOff() has been called too many times since 501 the last time Reset() was called 502 503 Note: Implementation is not thread-safe. 504 */ 505 func WithMaxRetries(b BackOff, max uint64) BackOff { 506 return &backOffTries{delegate: b, maxTries: max} 507 } 508 509 type backOffTries struct { 510 delegate BackOff 511 maxTries uint64 512 numTries uint64 513 } 514 515 func (b *backOffTries) NextBackOff() time.Duration { 516 if b.maxTries == 0 { 517 return Stop 518 } 519 if b.maxTries > 0 { 520 if b.maxTries <= b.numTries { 521 return Stop 522 } 523 b.numTries++ 524 } 525 return b.delegate.NextBackOff() 526 } 527 528 func (b *backOffTries) Reset() { 529 b.numTries = 0 530 b.delegate.Reset() 531 }