diff --git a/retry/retry.go b/retry/retry.go index 2588a31e..a9eb851b 100644 --- a/retry/retry.go +++ b/retry/retry.go @@ -68,28 +68,44 @@ func WithMaxAttempts(maxAttempts int) RetryerOptsFunc { } } +// WithMaxDuration sets the timeout which will cancel the retry loop if it has +// expired. It does not affect the timeout of the method given in the Do method. +// When MaxDuration is sets but not MaxAttempts, the number of attempts is +// math.MaxInt32. func WithMaxDuration(duration time.Duration) RetryerOptsFunc { return func(r *Retryer) { r.maxDuration = duration } } +// WithoutMaxAttempts sets the max attempts to math.MaxInt32. This is useful +// when you want to set a max duration but not a max attempts. In this case, the +// retry loop will continue until the max duration is reached. func WithoutMaxAttempts() RetryerOptsFunc { return func(r *Retryer) { r.maxAttempts = math.MaxInt32 } } +// WithErrorCallback adds a callback to be called after each failed attempt. func WithErrorCallback(c ErrorCallback) RetryerOptsFunc { return func(r *Retryer) { r.errorCallbacks = append(r.errorCallbacks, c) } } +// New creates a new Retryer with the given options. If no options are given, +// the default values are used: +// * waitDuration: 10 seconds +// * maxAttempts: math.MaxInt32 +// * maxDuration: 0 +// * errorCallbacks: empty +// If maxAttempts and maxDuration are both 0, the default value for maxAttempts +// is set to 5. func New(opts ...RetryerOptsFunc) Retryer { r := &Retryer{ waitDuration: 10 * time.Second, - maxAttempts: 5, + maxAttempts: math.MaxInt32, errorCallbacks: make([]ErrorCallback, 0), } @@ -97,6 +113,10 @@ func New(opts ...RetryerOptsFunc) Retryer { opt(r) } + if r.maxAttempts == math.MaxInt32 && r.maxDuration == 0 { + r.maxAttempts = 5 + } + return *r } diff --git a/retry/retry_test.go b/retry/retry_test.go index 6b696ed8..c2b3a7ea 100644 --- a/retry/retry_test.go +++ b/retry/retry_test.go @@ -53,7 +53,7 @@ func TestRetrier(t *testing.T) { assert.Error(t, err) }) - t.Run("It should cancel the retry if a RetryCancelError is retuned", func(t *testing.T) { + t.Run("It should cancel the retry if a RetryCancelError is returned", func(t *testing.T) { retrier := New(WithWaitDuration(1 * time.Millisecond)) count := 0 err := retrier.Do(context.Background(), func(ctx context.Context) error { @@ -181,4 +181,61 @@ func TestRetrier(t *testing.T) { assert.EqualValues(t, err.(RetryError).Scope, MaxDurationScope) assert.Equal(t, err.(RetryError).Err, context.DeadlineExceeded) }) + + t.Run("With MaxDuration there is no max attempts", func(t *testing.T) { + retrier := New( + WithWaitDuration(10*time.Millisecond), + WithMaxDuration(500*time.Millisecond), + ) + tries := 0 + + ctx := context.Background() + before := time.Now() + err := retrier.Do(ctx, func(ctx context.Context) error { + tries++ + if tries == 10 { + return nil + } + return fmt.Errorf("Error attempt %v", tries) + }) + assert.NoError(t, err) + assert.WithinDuration(t, time.Now(), before, 100*time.Millisecond) + }) + + t.Run("With MaxDuration and MaxAttempts", func(t *testing.T) { + retrier := New( + WithWaitDuration(10*time.Millisecond), + WithMaxAttempts(5), + WithMaxDuration(100*time.Millisecond), + ) + ctx := context.Background() + before := time.Now() + + t.Run("MaxAttempts expires first", func(t *testing.T) { + tries := 0 + + err := retrier.Do(ctx, func(ctx context.Context) error { + tries++ + return fmt.Errorf("Error attempt %v", tries) + }) + assert.Error(t, err) + assert.WithinDuration(t, time.Now(), before, 100*time.Millisecond) + assert.Equal(t, tries, 5) + }) + t.Run("MaxDuration expires first", func(t *testing.T) { + tries := 0 + + err := retrier.Do(ctx, func(ctx context.Context) error { + tries++ + time.Sleep(20 * time.Millisecond) + if tries == 10 { + return nil + } + return fmt.Errorf("Error attempt %v", tries) + }) + assert.Error(t, err) + assert.WithinDuration(t, time.Now(), before, 200*time.Millisecond) + assert.Less(t, tries, 5) + }) + }) }