1
0
Fork 0
mirror of https://github.com/Luzifer/go_helpers.git synced 2024-10-18 06:14:21 +00:00

Add special error to terminate retries immediately

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-11-27 23:11:07 +01:00
parent 9dec41b9ea
commit 8991a9232b
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
3 changed files with 65 additions and 14 deletions

View file

@ -1,20 +1,23 @@
// Package backoff contains a configurable retry-functionality using
// either exponential or constant backoff
package backoff package backoff
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
) )
const ( 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 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 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 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 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 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 // 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 // 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. // 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 { func (b Backoff) Retry(f Retryable) error {
var ( var (
iterations uint64 iterations uint64
@ -55,13 +64,18 @@ func (b Backoff) Retry(f Retryable) error {
return nil return nil
} }
var ecr ErrCannotRetry
if errors.As(err, &ecr) {
return ecr.Unwrap()
}
iterations++ iterations++
if b.MaxIterations > 0 && iterations == b.MaxIterations { 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 { 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) time.Sleep(sleepTime)

View file

@ -4,9 +4,22 @@ import (
"errors" "errors"
"testing" "testing"
"time" "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) { func TestMaxExecutionTime(t *testing.T) {
b := NewBackoff() b := NewBackoff()
@ -17,9 +30,9 @@ func TestMaxExecutionTime(t *testing.T) {
b.MinIterationTime = 100 * time.Millisecond b.MinIterationTime = 100 * time.Millisecond
b.Multiplier = 1.5 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 // After 6 iterations the time of 2078.125ms and after 7 iterations
// the time of 3217.1875ms should be reached and therefore no further // 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 { err := b.Retry(func() error {
counter++ counter++
return testError return errTestError
}) })
if counter != 5 { if counter != 5 {
@ -58,7 +71,6 @@ func TestSuccessfulExecution(t *testing.T) {
b.MaxIterations = 5 b.MaxIterations = 5
err := b.Retry(func() error { return nil }) err := b.Retry(func() error { return nil })
if err != nil { if err != nil {
t.Errorf("An error was thrown: %s", err) t.Errorf("An error was thrown: %s", err)
} }
@ -68,9 +80,9 @@ func TestWrappedError(t *testing.T) {
b := NewBackoff() b := NewBackoff()
b.MaxIterations = 5 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) t.Errorf("Error unwrapping did not yield test error: %v", err)
} }
} }

25
backoff/error.go Normal file
View file

@ -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
}