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

Add backoff retry-helper

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2019-11-15 16:35:59 +01:00
parent dabc93c52b
commit 4db41332c1
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
2 changed files with 151 additions and 0 deletions

86
backoff/backoff.go Normal file
View file

@ -0,0 +1,86 @@
package backoff
import (
"fmt"
"time"
)
const (
// Default value to use for number of iterations: infinite
DefaultMaxIterations uint64 = 0
// Default value to use for maximum iteration time
DefaultMaxIterationTime = 60 * time.Second
// Default value to use for maximum execution time: infinite
DefaultMaxTotalTime time.Duration = 0
// Default value to use for initial iteration time
DefaultMinIterationTime = 100 * time.Millisecond
// Default multiplier to apply to iteration time after each iteration
DefaultMultipler float64 = 1.5
)
// Backoff holds the configuration for backoff function retries
type Backoff struct {
MaxIterations uint64
MaxIterationTime time.Duration
MaxTotalTime time.Duration
MinIterationTime time.Duration
Multiplier float64
}
// NewBackoff creates a new Backoff configuration with default values (see constants)
func NewBackoff() *Backoff {
return &Backoff{
MaxIterations: DefaultMaxIterations,
MaxIterationTime: DefaultMaxIterationTime,
MaxTotalTime: DefaultMaxTotalTime,
MinIterationTime: DefaultMinIterationTime,
Multiplier: DefaultMultipler,
}
}
// 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.
func (b Backoff) Retry(f Retryable) error {
var (
iterations uint64
sleepTime = b.MinIterationTime
start = time.Now()
)
for {
err := f()
if err == nil {
return nil
}
iterations++
if b.MaxIterations > 0 && iterations == b.MaxIterations {
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)
}
time.Sleep(sleepTime)
sleepTime = b.nextIterationSleep(sleepTime)
}
}
func (b Backoff) nextIterationSleep(currentSleep time.Duration) time.Duration {
next := time.Duration(float64(currentSleep) * b.Multiplier)
if next > b.MaxIterationTime {
next = b.MaxIterationTime
}
return next
}
// Retryable is a function which takes no parameters and yields an error
// when it should be retried and nil when it was successful
type Retryable func() error
// Retry is a convenience wrapper to execute the retry with default values
// (see exported constants)
func Retry(f Retryable) error { return NewBackoff().Retry(f) }

65
backoff/backoff_test.go Normal file
View file

@ -0,0 +1,65 @@
package backoff
import (
"errors"
"testing"
"time"
)
var testError = errors.New("Test-Error")
func TestMaxExecutionTime(t *testing.T) {
b := NewBackoff()
// Define these values even if they match the defaults as
// the defaults might change and should not break this test
b.MaxIterationTime = 60 * time.Second
b.MaxTotalTime = 2500 * time.Millisecond
b.MinIterationTime = 100 * time.Millisecond
b.Multiplier = 1.5
var start = time.Now()
err := b.Retry(func() error { return testError })
// After 6 iterations the time of 2078.125ms and after 7 iterations
// the time of 3217.1875ms should be reached and therefore no further
// iteration should be done.
if d := time.Since(start); d < 3000*time.Millisecond || d > 3400*time.Millisecond {
t.Errorf("Function did not end within expected time: duration=%s", d)
}
if err == nil {
t.Error("Retry function had successful exit")
}
}
func TestMaxIterations(t *testing.T) {
b := NewBackoff()
b.MaxIterations = 5
var counter int
err := b.Retry(func() error {
counter++
return testError
})
if counter != 5 {
t.Errorf("Function was not executed 5 times: counter=%d", counter)
}
if err == nil {
t.Error("Retry function had successful exit")
}
}
func TestWrappedError(t *testing.T) {
b := NewBackoff()
b.MaxIterations = 5
err := b.Retry(func() error { return testError })
if errors.Unwrap(err) != testError {
t.Errorf("Error unwrapping did not yield test error: %v", err)
}
}