348 lines
7.9 KiB
Go
348 lines
7.9 KiB
Go
|
/*
|
||
|
Simple library for retry mechanism
|
||
|
|
||
|
slightly inspired by [Try::Tiny::Retry](https://metacpan.org/pod/Try::Tiny::Retry)
|
||
|
|
||
|
# SYNOPSIS
|
||
|
|
||
|
http get with retry:
|
||
|
|
||
|
url := "http://example.com"
|
||
|
var body []byte
|
||
|
|
||
|
err := retry.Do(
|
||
|
func() error {
|
||
|
resp, err := http.Get(url)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
body, err = ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
},
|
||
|
)
|
||
|
if err != nil {
|
||
|
// handle error
|
||
|
}
|
||
|
|
||
|
fmt.Println(string(body))
|
||
|
|
||
|
http get with retry with data:
|
||
|
|
||
|
url := "http://example.com"
|
||
|
|
||
|
body, err := retry.DoWithData(
|
||
|
func() ([]byte, error) {
|
||
|
resp, err := http.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
body, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return body, nil
|
||
|
},
|
||
|
)
|
||
|
if err != nil {
|
||
|
// handle error
|
||
|
}
|
||
|
|
||
|
fmt.Println(string(body))
|
||
|
|
||
|
[next examples](https://github.com/avast/retry-go/tree/master/examples)
|
||
|
|
||
|
# SEE ALSO
|
||
|
|
||
|
* [giantswarm/retry-go](https://github.com/giantswarm/retry-go) - slightly complicated interface.
|
||
|
|
||
|
* [sethgrid/pester](https://github.com/sethgrid/pester) - only http retry for http calls with retries and backoff
|
||
|
|
||
|
* [cenkalti/backoff](https://github.com/cenkalti/backoff) - Go port of the exponential backoff algorithm from Google's HTTP Client Library for Java. Really complicated interface.
|
||
|
|
||
|
* [rafaeljesus/retry-go](https://github.com/rafaeljesus/retry-go) - looks good, slightly similar as this package, don't have 'simple' `Retry` method
|
||
|
|
||
|
* [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me)
|
||
|
|
||
|
# BREAKING CHANGES
|
||
|
|
||
|
* 4.0.0
|
||
|
- infinity retry is possible by set `Attempts(0)` by PR [#49](https://github.com/avast/retry-go/pull/49)
|
||
|
|
||
|
* 3.0.0
|
||
|
- `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go).
|
||
|
|
||
|
* 1.0.2 -> 2.0.0
|
||
|
- argument of `retry.Delay` is final delay (no multiplication by `retry.Units` anymore)
|
||
|
- function `retry.Units` are removed
|
||
|
- [more about this breaking change](https://github.com/avast/retry-go/issues/7)
|
||
|
|
||
|
* 0.3.0 -> 1.0.0
|
||
|
- `retry.Retry` function are changed to `retry.Do` function
|
||
|
- `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are now implement via functions produces Options (aka `retry.OnRetry`)
|
||
|
*/
|
||
|
package retry
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Function signature of retryable function
|
||
|
type RetryableFunc func() error
|
||
|
|
||
|
// Function signature of retryable function with data
|
||
|
type RetryableFuncWithData[T any] func() (T, error)
|
||
|
|
||
|
// Default timer is a wrapper around time.After
|
||
|
type timerImpl struct{}
|
||
|
|
||
|
func (t *timerImpl) After(d time.Duration) <-chan time.Time {
|
||
|
return time.After(d)
|
||
|
}
|
||
|
|
||
|
func Do(retryableFunc RetryableFunc, opts ...Option) error {
|
||
|
retryableFuncWithData := func() (any, error) {
|
||
|
return nil, retryableFunc()
|
||
|
}
|
||
|
|
||
|
_, err := DoWithData(retryableFuncWithData, opts...)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) {
|
||
|
var n uint
|
||
|
var emptyT T
|
||
|
|
||
|
// default
|
||
|
config := newDefaultRetryConfig()
|
||
|
|
||
|
// apply opts
|
||
|
for _, opt := range opts {
|
||
|
opt(config)
|
||
|
}
|
||
|
|
||
|
if err := config.context.Err(); err != nil {
|
||
|
return emptyT, err
|
||
|
}
|
||
|
|
||
|
// Setting attempts to 0 means we'll retry until we succeed
|
||
|
var lastErr error
|
||
|
if config.attempts == 0 {
|
||
|
for {
|
||
|
t, err := retryableFunc()
|
||
|
if err == nil {
|
||
|
return t, nil
|
||
|
}
|
||
|
|
||
|
if !IsRecoverable(err) {
|
||
|
return emptyT, err
|
||
|
}
|
||
|
|
||
|
if !config.retryIf(err) {
|
||
|
return emptyT, err
|
||
|
}
|
||
|
|
||
|
lastErr = err
|
||
|
|
||
|
n++
|
||
|
config.onRetry(n, err)
|
||
|
select {
|
||
|
case <-config.timer.After(delay(config, n, err)):
|
||
|
case <-config.context.Done():
|
||
|
if config.wrapContextErrorWithLastError {
|
||
|
return emptyT, Error{config.context.Err(), lastErr}
|
||
|
}
|
||
|
return emptyT, config.context.Err()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
errorLog := Error{}
|
||
|
|
||
|
attemptsForError := make(map[error]uint, len(config.attemptsForError))
|
||
|
for err, attempts := range config.attemptsForError {
|
||
|
attemptsForError[err] = attempts
|
||
|
}
|
||
|
|
||
|
shouldRetry := true
|
||
|
for shouldRetry {
|
||
|
t, err := retryableFunc()
|
||
|
if err == nil {
|
||
|
return t, nil
|
||
|
}
|
||
|
|
||
|
errorLog = append(errorLog, unpackUnrecoverable(err))
|
||
|
|
||
|
if !config.retryIf(err) {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
config.onRetry(n, err)
|
||
|
|
||
|
for errToCheck, attempts := range attemptsForError {
|
||
|
if errors.Is(err, errToCheck) {
|
||
|
attempts--
|
||
|
attemptsForError[errToCheck] = attempts
|
||
|
shouldRetry = shouldRetry && attempts > 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if this is last attempt - don't wait
|
||
|
if n == config.attempts-1 {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
select {
|
||
|
case <-config.timer.After(delay(config, n, err)):
|
||
|
case <-config.context.Done():
|
||
|
if config.lastErrorOnly {
|
||
|
return emptyT, config.context.Err()
|
||
|
}
|
||
|
|
||
|
return emptyT, append(errorLog, config.context.Err())
|
||
|
}
|
||
|
|
||
|
n++
|
||
|
shouldRetry = shouldRetry && n < config.attempts
|
||
|
}
|
||
|
|
||
|
if config.lastErrorOnly {
|
||
|
return emptyT, errorLog.Unwrap()
|
||
|
}
|
||
|
return emptyT, errorLog
|
||
|
}
|
||
|
|
||
|
func newDefaultRetryConfig() *Config {
|
||
|
return &Config{
|
||
|
attempts: uint(10),
|
||
|
attemptsForError: make(map[error]uint),
|
||
|
delay: 100 * time.Millisecond,
|
||
|
maxJitter: 100 * time.Millisecond,
|
||
|
onRetry: func(n uint, err error) {},
|
||
|
retryIf: IsRecoverable,
|
||
|
delayType: CombineDelay(BackOffDelay, RandomDelay),
|
||
|
lastErrorOnly: false,
|
||
|
context: context.Background(),
|
||
|
timer: &timerImpl{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Error type represents list of errors in retry
|
||
|
type Error []error
|
||
|
|
||
|
// Error method return string representation of Error
|
||
|
// It is an implementation of error interface
|
||
|
func (e Error) Error() string {
|
||
|
logWithNumber := make([]string, len(e))
|
||
|
for i, l := range e {
|
||
|
if l != nil {
|
||
|
logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n"))
|
||
|
}
|
||
|
|
||
|
func (e Error) Is(target error) bool {
|
||
|
for _, v := range e {
|
||
|
if errors.Is(v, target) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (e Error) As(target interface{}) bool {
|
||
|
for _, v := range e {
|
||
|
if errors.As(v, target) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Unwrap the last error for compatibility with `errors.Unwrap()`.
|
||
|
When you need to unwrap all errors, you should use `WrappedErrors()` instead.
|
||
|
|
||
|
err := Do(
|
||
|
func() error {
|
||
|
return errors.New("original error")
|
||
|
},
|
||
|
Attempts(1),
|
||
|
)
|
||
|
|
||
|
fmt.Println(errors.Unwrap(err)) # "original error" is printed
|
||
|
|
||
|
Added in version 4.2.0.
|
||
|
*/
|
||
|
func (e Error) Unwrap() error {
|
||
|
return e[len(e)-1]
|
||
|
}
|
||
|
|
||
|
// WrappedErrors returns the list of errors that this Error is wrapping.
|
||
|
// It is an implementation of the `errwrap.Wrapper` interface
|
||
|
// in package [errwrap](https://github.com/hashicorp/errwrap) so that
|
||
|
// `retry.Error` can be used with that library.
|
||
|
func (e Error) WrappedErrors() []error {
|
||
|
return e
|
||
|
}
|
||
|
|
||
|
type unrecoverableError struct {
|
||
|
error
|
||
|
}
|
||
|
|
||
|
func (e unrecoverableError) Error() string {
|
||
|
if e.error == nil {
|
||
|
return "unrecoverable error"
|
||
|
}
|
||
|
return e.error.Error()
|
||
|
}
|
||
|
|
||
|
func (e unrecoverableError) Unwrap() error {
|
||
|
return e.error
|
||
|
}
|
||
|
|
||
|
// Unrecoverable wraps an error in `unrecoverableError` struct
|
||
|
func Unrecoverable(err error) error {
|
||
|
return unrecoverableError{err}
|
||
|
}
|
||
|
|
||
|
// IsRecoverable checks if error is an instance of `unrecoverableError`
|
||
|
func IsRecoverable(err error) bool {
|
||
|
return !errors.Is(err, unrecoverableError{})
|
||
|
}
|
||
|
|
||
|
// Adds support for errors.Is usage on unrecoverableError
|
||
|
func (unrecoverableError) Is(err error) bool {
|
||
|
_, isUnrecoverable := err.(unrecoverableError)
|
||
|
return isUnrecoverable
|
||
|
}
|
||
|
|
||
|
func unpackUnrecoverable(err error) error {
|
||
|
if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable {
|
||
|
return unrecoverable.error
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func delay(config *Config, n uint, err error) time.Duration {
|
||
|
delayTime := config.delayType(n, err, config)
|
||
|
if config.maxDelay > 0 && delayTime > config.maxDelay {
|
||
|
delayTime = config.maxDelay
|
||
|
}
|
||
|
|
||
|
return delayTime
|
||
|
}
|