diff --git a/backoff/backoff.go b/backoff/backoff.go index 95bdeb6..b33c237 100644 --- a/backoff/backoff.go +++ b/backoff/backoff.go @@ -1,20 +1,23 @@ +// Package backoff contains a configurable retry-functionality using +// either exponential or constant backoff package backoff import ( + "errors" "fmt" "time" ) const ( - // Default value to use for number of iterations: infinite + // DefaultMaxIterations contains the default value to use for number of iterations: infinite DefaultMaxIterations uint64 = 0 - // Default value to use for maximum iteration time + // DefaultMaxIterationTime contains the default value to use for maximum iteration time DefaultMaxIterationTime = 60 * time.Second - // Default value to use for maximum execution time: infinite + // DefaultMaxTotalTime contains the default value to use for maximum execution time: infinite DefaultMaxTotalTime time.Duration = 0 - // Default value to use for initial iteration time + // DefaultMinIterationTime contains the default value to use for initial iteration time DefaultMinIterationTime = 100 * time.Millisecond - // Default multiplier to apply to iteration time after each iteration + // DefaultMultipler contains the default multiplier to apply to iteration time after each iteration DefaultMultipler float64 = 1.5 ) @@ -41,6 +44,12 @@ func NewBackoff() *Backoff { // Retry executes the function and waits for it to end successul or for the // given limites to be reached. The returned error uses Go1.13 wrapping of // errors and can be unwrapped into the error of the function itself. +// +// To break free from the Retry function ignoring the remaining retries +// return an ErrCannotRetry containing the original error. At this +// point the ErrCannotRetry will NOT be returned but unwrapped. So +// returning NewErrCannotRetry(errors.New("foo")) will give you the +// errors.New("foo") as a return value from Retry. func (b Backoff) Retry(f Retryable) error { var ( iterations uint64 @@ -55,13 +64,18 @@ func (b Backoff) Retry(f Retryable) error { return nil } + var ecr ErrCannotRetry + if errors.As(err, &ecr) { + return ecr.Unwrap() + } + iterations++ if b.MaxIterations > 0 && iterations == b.MaxIterations { - return fmt.Errorf("Maximum iterations reached: %w", err) + return fmt.Errorf("maximum iterations reached: %w", err) } if b.MaxTotalTime > 0 && time.Since(start) >= b.MaxTotalTime { - return fmt.Errorf("Maximum execution time reached: %w", err) + return fmt.Errorf("maximum execution time reached: %w", err) } time.Sleep(sleepTime) diff --git a/backoff/backoff_test.go b/backoff/backoff_test.go index 27f1c2a..16d7975 100644 --- a/backoff/backoff_test.go +++ b/backoff/backoff_test.go @@ -4,9 +4,22 @@ import ( "errors" "testing" "time" + + "github.com/stretchr/testify/assert" ) -var testError = errors.New("Test-Error") +var errTestError = errors.New("Test-Error") + +func TestBreakFree(t *testing.T) { + var seen int + err := NewBackoff().WithMaxIterations(5).Retry(func() error { + seen++ + return NewErrCannotRetry(errTestError) + }) + assert.Error(t, err) + assert.Equal(t, 1, seen) + assert.Equal(t, errTestError, err) +} func TestMaxExecutionTime(t *testing.T) { b := NewBackoff() @@ -17,9 +30,9 @@ func TestMaxExecutionTime(t *testing.T) { b.MinIterationTime = 100 * time.Millisecond b.Multiplier = 1.5 - var start = time.Now() + start := time.Now() - err := b.Retry(func() error { return testError }) + err := b.Retry(func() error { return errTestError }) // After 6 iterations the time of 2078.125ms and after 7 iterations // the time of 3217.1875ms should be reached and therefore no further @@ -41,7 +54,7 @@ func TestMaxIterations(t *testing.T) { err := b.Retry(func() error { counter++ - return testError + return errTestError }) if counter != 5 { @@ -58,7 +71,6 @@ func TestSuccessfulExecution(t *testing.T) { b.MaxIterations = 5 err := b.Retry(func() error { return nil }) - if err != nil { t.Errorf("An error was thrown: %s", err) } @@ -68,9 +80,9 @@ func TestWrappedError(t *testing.T) { b := NewBackoff() b.MaxIterations = 5 - err := b.Retry(func() error { return testError }) + err := b.Retry(func() error { return errTestError }) - if errors.Unwrap(err) != testError { + if errors.Unwrap(err) != errTestError { t.Errorf("Error unwrapping did not yield test error: %v", err) } } diff --git a/backoff/error.go b/backoff/error.go new file mode 100644 index 0000000..47e398f --- /dev/null +++ b/backoff/error.go @@ -0,0 +1,25 @@ +package backoff + +import "fmt" + +type ( + // ErrCannotRetry wraps the original error and signals the backoff + // should be stopped now as a retry i.e. would be harmful or would + // make no sense + ErrCannotRetry struct{ inner error } +) + +// NewErrCannotRetry wraps the given error into an ErrCannotRetry and +// should be used to break from a Retry() function when the retry +// should stop immediately +func NewErrCannotRetry(err error) error { + return ErrCannotRetry{err} +} + +func (e ErrCannotRetry) Error() string { + return fmt.Sprintf("retry cancelled by error: %s", e.inner.Error()) +} + +func (e ErrCannotRetry) Unwrap() error { + return e.inner +}