From b05e677998a8255683595b330fb255500a54c8b8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 9 Jan 2020 13:58:57 -0700 Subject: [PATCH] Support random jitter combined with other delay options. Adds `RandomDelay` and `CombineDelay` as `DelayTypeFunc`s Adds `CombineDelay` which allows multiple `DelayTypeFunc`s to b chained together. Updates the default `DelayTypeFunc` to exponential backoff with random jitter. --- README.md | 26 ++++++++++++++++++++++++-- options.go | 25 +++++++++++++++++++++++++ retry.go | 3 ++- retry_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b282110..1b6b103 100644 --- a/README.md +++ b/README.md @@ -109,10 +109,17 @@ func IsRecoverable(err error) bool ``` IsRecoverable checks if error is an instance of `unrecoverableError` +#### func RandomDelay + +```go +func RandomDelay(_ uint, config *Config) time.Duration +``` +RandomDelay is a DelayType which picks a random delay up to config.maxJitter + #### func Unrecoverable ```go -func Unrecoverable(err error) unrecoverableError +func Unrecoverable(err error) error ``` Unrecoverable wraps an error in `unrecoverableError` struct @@ -131,6 +138,14 @@ type DelayTypeFunc func(n uint, config *Config) time.Duration ``` +#### func CombineDelay + +```go +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc +``` +CombineDelay is a DelayType the combines all of the specified delays into a new +DelayTypeFunc + #### type Error ```go @@ -202,6 +217,13 @@ func LastErrorOnly(lastErrorOnly bool) Option return the direct last error that came from the retried function default is false (return wrapped errors with everything) +#### func MaxJitter + +```go +func MaxJitter(maxJitter time.Duration) Option +``` +MaxJitter sets the maximum random Jitter between retries for RandomDelay + #### func OnRetry ```go @@ -242,7 +264,7 @@ skip retry if special error example: }) ) -The default RetryIf stops execution if the error is wrapped using +By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, so above example may also be shortened to: retry.Do( diff --git a/options.go b/options.go index db20f5c..fdcd0b7 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,7 @@ package retry import ( + "math/rand" "time" ) @@ -16,6 +17,7 @@ type DelayTypeFunc func(n uint, config *Config) time.Duration type Config struct { attempts uint delay time.Duration + maxJitter time.Duration onRetry OnRetryFunc retryIf RetryIfFunc delayType DelayTypeFunc @@ -49,6 +51,13 @@ func Delay(delay time.Duration) Option { } } +// MaxJitter sets the maximum random Jitter between retries for RandomDelay +func MaxJitter(maxJitter time.Duration) Option { + return func(c *Config) { + c.maxJitter = maxJitter + } +} + // DelayType set type of the delay between retries // default is BackOff func DelayType(delayType DelayTypeFunc) Option { @@ -67,6 +76,22 @@ func FixedDelay(_ uint, config *Config) time.Duration { return config.delay } +// RandomDelay is a DelayType which picks a random delay up to config.maxJitter +func RandomDelay(_ uint, config *Config) time.Duration { + return time.Duration(rand.Int63n(int64(config.maxJitter))) +} + +// CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc +func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { + return func(n uint, config *Config) time.Duration { + var total time.Duration + for _, delay := range delays { + total += delay(n, config) + } + return total + } +} + // OnRetry function callback are called each retry // // log each retry example: diff --git a/retry.go b/retry.go index ea7f367..4e8e35e 100644 --- a/retry.go +++ b/retry.go @@ -80,9 +80,10 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error { config := &Config{ attempts: 10, delay: 100 * time.Millisecond, + maxJitter: 100 * time.Millisecond, onRetry: func(n uint, err error) {}, retryIf: IsRecoverable, - delayType: BackOffDelay, + delayType: CombineDelay(BackOffDelay, RandomDelay), lastErrorOnly: false, } diff --git a/retry_test.go b/retry_test.go index 8a33d74..1f04361 100644 --- a/retry_test.go +++ b/retry_test.go @@ -121,3 +121,30 @@ func TestUnrecoverableError(t *testing.T) { assert.Equal(t, expectedErr, err) assert.Equal(t, 1, attempts, "unrecoverable error broke the loop") } + +func TestCombineFixedDelays(t *testing.T) { + start := time.Now() + err := Do( + func() error { return errors.New("test") }, + Attempts(3), + DelayType(CombineDelay(FixedDelay, FixedDelay)), + ) + dur := time.Since(start) + assert.Error(t, err) + assert.True(t, dur > 400*time.Millisecond, "3 times combined, fixed retry is longer then 400ms") + assert.True(t, dur < 500*time.Millisecond, "3 times combined, fixed retry is shorter then 500ms") +} + +func TestRandomDelay(t *testing.T) { + start := time.Now() + err := Do( + func() error { return errors.New("test") }, + Attempts(3), + DelayType(RandomDelay), + MaxJitter(50 * time.Millisecond), + ) + dur := time.Since(start) + assert.Error(t, err) + assert.True(t, dur > 2*time.Millisecond, "3 times random retry is longer then 2ms") + assert.True(t, dur < 100*time.Millisecond, "3 times random retry is shorter then 100ms") +}