/* 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 }, ) fmt.Println(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 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" "fmt" "strings" "time" ) // Function signature of retryable function type RetryableFunc func() error var ( DefaultAttempts = uint(10) DefaultDelay = 100 * time.Millisecond DefaultMaxJitter = 100 * time.Millisecond DefaultOnRetry = func(n uint, err error) {} DefaultRetryIf = IsRecoverable DefaultDelayType = CombineDelay(BackOffDelay, RandomDelay) DefaultLastErrorOnly = false DefaultContext = context.Background() ) func Do(retryableFunc RetryableFunc, opts ...Option) error { var n uint //default config := &Config{ attempts: DefaultAttempts, delay: DefaultDelay, maxJitter: DefaultMaxJitter, onRetry: DefaultOnRetry, retryIf: DefaultRetryIf, delayType: DefaultDelayType, lastErrorOnly: DefaultLastErrorOnly, context: DefaultContext, } //apply opts for _, opt := range opts { opt(config) } if err := config.context.Err(); err != nil { return err } var errorLog Error if !config.lastErrorOnly { errorLog = make(Error, config.attempts) } else { errorLog = make(Error, 1) } lastErrIndex := n for n < config.attempts { err := retryableFunc() if err != nil { errorLog[lastErrIndex] = unpackUnrecoverable(err) if !config.retryIf(err) { break } config.onRetry(n, err) // if this is last attempt - don't wait if n == config.attempts-1 { break } delayTime := config.delayType(n, err, config) if config.maxDelay > 0 && delayTime > config.maxDelay { delayTime = config.maxDelay } select { case <-time.After(delayTime): case <-config.context.Done(): return config.context.Err() } } else { return nil } n++ if !config.lastErrorOnly { lastErrIndex = n } } if config.lastErrorOnly { return errorLog[lastErrIndex] } return errorLog } // 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, lenWithoutNil(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 lenWithoutNil(e Error) (count int) { for _, v := range e { if v != nil { count++ } } return } // 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 } // 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 { _, isUnrecoverable := err.(unrecoverableError) return !isUnrecoverable } func unpackUnrecoverable(err error) error { if unrecoverable, isUnrecoverable := err.(unrecoverableError); isUnrecoverable { return unrecoverable.error } return err }