pax_global_header00006660000000000000000000000064146101510410014503gustar00rootroot0000000000000052 comment=839c8ceb66c3308e79d46f2552fec3980aaec6f2 retry-go-4.6.0/000077500000000000000000000000001461015104100132625ustar00rootroot00000000000000retry-go-4.6.0/.github/000077500000000000000000000000001461015104100146225ustar00rootroot00000000000000retry-go-4.6.0/.github/dependabot.yml000066400000000000000000000002211461015104100174450ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily time: "04:00" open-pull-requests-limit: 10 retry-go-4.6.0/.github/workflows/000077500000000000000000000000001461015104100166575ustar00rootroot00000000000000retry-go-4.6.0/.github/workflows/workflow.yaml000066400000000000000000000031311461015104100214130ustar00rootroot00000000000000name: Go on: push: pull_request: branches: - master jobs: golangci-lint: runs-on: ubuntu-latest steps: - uses: actions/setup-go@v4 with: go-version: 1.18 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: latest tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: go-version: ['1.18', '1.19', '1.20'] os: [ubuntu-latest, macos-latest, windows-latest] env: OS: ${{ matrix.os }} GOVERSION: ${{ matrix.go-version }} steps: - uses: actions/checkout@v3 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} check-latest: true cache: true - name: Display Go version run: go version - name: Install dependencies run: make setup - name: Test run: make ci - name: Archive code coverage results uses: actions/upload-artifact@v3 with: name: code-coverage-report-${{ matrix.os }} path: coverage.txt coverage: needs: tests runs-on: ubuntu-latest steps: - name: Download a linux coverage report uses: actions/download-artifact@v3 with: name: code-coverage-report-ubuntu-latest - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true flags: unittest retry-go-4.6.0/.gitignore000066400000000000000000000005031461015104100152500ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ # dep vendor/ Gopkg.lock # cover coverage.txt retry-go-4.6.0/.godocdown.tmpl000066400000000000000000000031751461015104100162270ustar00rootroot00000000000000# {{ .Name }} [![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) ![GitHub Actions](https://github.com/avast/retry-go/actions/workflows/workflow.yaml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) [![GoDoc](https://godoc.org/github.com/avast/retry-go?status.svg&style=flat-square)](http://godoc.org/github.com/avast/retry-go) [![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=master)](https://codecov.io/github/avast/retry-go?branch=master) [![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) {{ .EmitSynopsis }} {{ .EmitUsage }} ## Contributing Contributions are very much welcome. ### Makefile Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... Try `make help` for more information. ### Before pull request > maybe you need `make setup` in order to setup environment please try: * run tests (`make test`) * run linter (`make lint`) * if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) ### README README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). Never edit README.md direct, because your change will be lost. retry-go-4.6.0/LICENSE000066400000000000000000000020461461015104100142710ustar00rootroot00000000000000MIT License Copyright (c) 2017 Avast Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. retry-go-4.6.0/Makefile000066400000000000000000000042401461015104100147220ustar00rootroot00000000000000SOURCE_FILES?=$$(go list ./... | grep -v /vendor/) TEST_PATTERN?=. TEST_OPTIONS?= VERSION?=$$(cat VERSION) LINTER?=$$(which golangci-lint) LINTER_VERSION=1.50.0 ifeq ($(OS),Windows_NT) LINTER_FILE=golangci-lint-$(LINTER_VERSION)-windows-amd64.zip LINTER_UNPACK= >| app.zip; unzip -j app.zip -d $$GOPATH/bin; rm app.zip else ifeq ($(OS), Darwin) LINTER_FILE=golangci-lint-$(LINTER_VERSION)-darwin-amd64.tar.gz LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" else LINTER_FILE=golangci-lint-$(LINTER_VERSION)-linux-amd64.tar.gz LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint" endif setup: go install github.com/pierrre/gotestcover@latest go install golang.org/x/tools/cmd/cover@latest go install github.com/robertkrimen/godocdown/godocdown@latest go mod download generate: ## Generate README.md godocdown >| README.md test: generate test_and_cover_report lint test_and_cover_report: gotestcover $(TEST_OPTIONS) -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m cover: test ## Run all the tests and opens the coverage report go tool cover -html=coverage.txt fmt: ## gofmt and goimports all go files find . -name '*.go' -not -wholename './vendor/*' | while read -r file; do gofmt -w -s "$$file"; goimports -w "$$file"; done lint: ## Run all the linters @if [ "$(LINTER)" = "" ]; then\ curl -L https://github.com/golangci/golangci-lint/releases/download/v$(LINTER_VERSION)/$(LINTER_FILE) $(LINTER_UNPACK) ;\ chmod +x $$GOPATH/bin/golangci-lint;\ fi golangci-lint run ci: test_and_cover_report ## Run all the tests but no linters - use https://golangci.com integration instead build: go build release: ## Release new version git tag | grep -q $(VERSION) && echo This version was released! Increase VERSION! || git tag $(VERSION) && git push origin $(VERSION) && git tag v$(VERSION) && git push origin v$(VERSION) # Absolutely awesome: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' .DEFAULT_GOAL := build retry-go-4.6.0/README.md000066400000000000000000000265321461015104100145510ustar00rootroot00000000000000# retry [![Release](https://img.shields.io/github/release/avast/retry-go.svg?style=flat-square)](https://github.com/avast/retry-go/releases/latest) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) ![GitHub Actions](https://github.com/avast/retry-go/actions/workflows/workflow.yaml/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/avast/retry-go?style=flat-square)](https://goreportcard.com/report/github.com/avast/retry-go) [![GoDoc](https://godoc.org/github.com/avast/retry-go?status.svg&style=flat-square)](http://godoc.org/github.com/avast/retry-go) [![codecov.io](https://codecov.io/github/avast/retry-go/coverage.svg?branch=master)](https://codecov.io/github/avast/retry-go?branch=master) [![Sourcegraph](https://sourcegraph.com/github.com/avast/retry-go/-/badge.svg)](https://sourcegraph.com/github.com/avast/retry-go?badge) 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)) [More 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`) ## Usage #### func BackOffDelay ```go func BackOffDelay(n uint, _ error, config *Config) time.Duration ``` BackOffDelay is a DelayType which increases delay between consecutive retries #### func Do ```go func Do(retryableFunc RetryableFunc, opts ...Option) error ``` #### func DoWithData ```go func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) ``` #### func FixedDelay ```go func FixedDelay(_ uint, _ error, config *Config) time.Duration ``` FixedDelay is a DelayType which keeps delay the same through all iterations #### func IsRecoverable ```go func IsRecoverable(err error) bool ``` IsRecoverable checks if error is an instance of `unrecoverableError` #### func RandomDelay ```go func RandomDelay(_ uint, _ error, config *Config) time.Duration ``` RandomDelay is a DelayType which picks a random delay up to config.maxJitter #### func Unrecoverable ```go func Unrecoverable(err error) error ``` Unrecoverable wraps an error in `unrecoverableError` struct #### type Config ```go type Config struct { } ``` #### type DelayTypeFunc ```go type DelayTypeFunc func(n uint, err error, config *Config) time.Duration ``` DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts. #### func CombineDelay ```go func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc ``` CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc #### type Error ```go type Error []error ``` Error type represents list of errors in retry #### func (Error) As ```go func (e Error) As(target interface{}) bool ``` #### func (Error) Error ```go func (e Error) Error() string ``` Error method return string representation of Error It is an implementation of error interface #### func (Error) Is ```go func (e Error) Is(target error) bool ``` #### func (Error) Unwrap ```go func (e Error) Unwrap() error ``` 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 (Error) WrappedErrors ```go func (e Error) WrappedErrors() []error ``` 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. #### type OnRetryFunc ```go type OnRetryFunc func(attempt uint, err error) ``` Function signature of OnRetry function #### type Option ```go type Option func(*Config) ``` Option represents an option for retry. #### func Attempts ```go func Attempts(attempts uint) Option ``` Attempts set count of retry. Setting to 0 will retry until the retried function succeeds. default is 10 #### func AttemptsForError ```go func AttemptsForError(attempts uint, err error) Option ``` AttemptsForError sets count of retry in case execution results in given `err` Retries for the given `err` are also counted against total retries. The retry will stop if any of given retries is exhausted. added in 4.3.0 #### func Context ```go func Context(ctx context.Context) Option ``` Context allow to set context of retry default are Background context example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope) ctx, cancel := context.WithCancel(context.Background()) cancel() retry.Do( func() error { ... }, retry.Context(ctx), ) #### func Delay ```go func Delay(delay time.Duration) Option ``` Delay set delay between retry default is 100ms #### func DelayType ```go func DelayType(delayType DelayTypeFunc) Option ``` DelayType set type of the delay between retries default is BackOff #### func LastErrorOnly ```go func LastErrorOnly(lastErrorOnly bool) Option ``` return the direct last error that came from the retried function default is false (return wrapped errors with everything) #### func MaxDelay ```go func MaxDelay(maxDelay time.Duration) Option ``` MaxDelay set maximum delay between retry does not apply by default #### func MaxJitter ```go func MaxJitter(maxJitter time.Duration) Option ``` MaxJitter sets the maximum random Jitter between retries for RandomDelay #### func OnRetry ```go func OnRetry(onRetry OnRetryFunc) Option ``` OnRetry function callback are called each retry log each retry example: retry.Do( func() error { return errors.New("some error") }, retry.OnRetry(func(n uint, err error) { log.Printf("#%d: %s\n", n, err) }), ) #### func RetryIf ```go func RetryIf(retryIf RetryIfFunc) Option ``` RetryIf controls whether a retry should be attempted after an error (assuming there are any retry attempts remaining) skip retry if special error example: retry.Do( func() error { return errors.New("special error") }, retry.RetryIf(func(err error) bool { if err.Error() == "special error" { return false } return true }) ) By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, so above example may also be shortened to: retry.Do( func() error { return retry.Unrecoverable(errors.New("special error")) } ) #### func UntilSucceeded ```go func UntilSucceeded() Option ``` UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0). #### func WithTimer ```go func WithTimer(t Timer) Option ``` WithTimer provides a way to swap out timer module implementations. This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration for retries. example of augmenting time.After with a print statement type struct MyTimer {} func (t *MyTimer) After(d time.Duration) <- chan time.Time { fmt.Print("Timer called!") return time.After(d) } retry.Do( func() error { ... }, retry.WithTimer(&MyTimer{}) ) #### func WrapContextErrorWithLastError ```go func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option ``` WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when using a context to cancel / timeout default is false ctx, cancel := context.WithCancel(context.Background()) defer cancel() retry.Do( func() error { ... }, retry.Context(ctx), retry.Attempts(0), retry.WrapContextErrorWithLastError(true), ) #### type RetryIfFunc ```go type RetryIfFunc func(error) bool ``` Function signature of retry if function #### type RetryableFunc ```go type RetryableFunc func() error ``` Function signature of retryable function #### type RetryableFuncWithData ```go type RetryableFuncWithData[T any] func() (T, error) ``` Function signature of retryable function with data #### type Timer ```go type Timer interface { After(time.Duration) <-chan time.Time } ``` Timer represents the timer used to track time for a retry. ## Contributing Contributions are very much welcome. ### Makefile Makefile provides several handy rules, like README.md `generator` , `setup` for prepare build/dev environment, `test`, `cover`, etc... Try `make help` for more information. ### Before pull request > maybe you need `make setup` in order to setup environment please try: * run tests (`make test`) * run linter (`make lint`) * if your IDE don't automaticaly do `go fmt`, run `go fmt` (`make fmt`) ### README README.md are generate from template [.godocdown.tmpl](.godocdown.tmpl) and code documentation via [godocdown](https://github.com/robertkrimen/godocdown). Never edit README.md direct, because your change will be lost. retry-go-4.6.0/VERSION000066400000000000000000000000061461015104100143260ustar00rootroot000000000000004.5.1 retry-go-4.6.0/examples/000077500000000000000000000000001461015104100151005ustar00rootroot00000000000000retry-go-4.6.0/examples/custom_retry_function_test.go000066400000000000000000000054301461015104100231340ustar00rootroot00000000000000package retry_test import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "strconv" "testing" "time" "github.com/avast/retry-go/v4" "github.com/stretchr/testify/assert" ) // RetriableError is a custom error that contains a positive duration for the next retry type RetriableError struct { Err error RetryAfter time.Duration } // Error returns error message and a Retry-After duration func (e *RetriableError) Error() string { return fmt.Sprintf("%s (retry after %v)", e.Err.Error(), e.RetryAfter) } var _ error = (*RetriableError)(nil) // TestCustomRetryFunction shows how to use a custom retry function func TestCustomRetryFunction(t *testing.T) { attempts := 5 // server succeeds after 5 attempts ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if attempts > 0 { // inform the client to retry after one second using standard // HTTP 429 status code with Retry-After header in seconds w.Header().Add("Retry-After", "1") w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte("Server limit reached")) attempts-- return } w.WriteHeader(http.StatusOK) w.Write([]byte("hello")) })) defer ts.Close() var body []byte err := retry.Do( func() error { resp, err := http.Get(ts.URL) if err == nil { defer func() { if err := resp.Body.Close(); err != nil { panic(err) } }() body, err = ioutil.ReadAll(resp.Body) if resp.StatusCode != 200 { err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) if resp.StatusCode == http.StatusTooManyRequests { // check Retry-After header if it contains seconds to wait for the next retry if retryAfter, e := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 32); e == nil { // the server returns 0 to inform that the operation cannot be retried if retryAfter <= 0 { return retry.Unrecoverable(err) } return &RetriableError{ Err: err, RetryAfter: time.Duration(retryAfter) * time.Second, } } // A real implementation should also try to http.Parse the retryAfter response header // to conform with HTTP specification. Herein we know here that we return only seconds. } } } return err }, retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration { fmt.Println("Server fails with: " + err.Error()) if retriable, ok := err.(*RetriableError); ok { fmt.Printf("Client follows server recommendation to retry after %v\n", retriable.RetryAfter) return retriable.RetryAfter } // apply a default exponential back off strategy return retry.BackOffDelay(n, err, config) }), ) fmt.Println("Server responds with: " + string(body)) assert.NoError(t, err) assert.Equal(t, "hello", string(body)) } retry-go-4.6.0/examples/delay_based_on_error_test.go000066400000000000000000000033711461015104100226330ustar00rootroot00000000000000// This test delay is based on kind of error // e.g. HTTP response [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) package retry_test import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "time" "github.com/avast/retry-go/v4" "github.com/stretchr/testify/assert" ) type RetryAfterError struct { response http.Response } func (err RetryAfterError) Error() string { return fmt.Sprintf( "Request to %s fail %s (%d)", err.response.Request.RequestURI, err.response.Status, err.response.StatusCode, ) } type SomeOtherError struct { err string retryAfter time.Duration } func (err SomeOtherError) Error() string { return err.err } func TestCustomRetryFunctionBasedOnKindOfError(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") })) defer ts.Close() var body []byte err := retry.Do( func() error { resp, err := http.Get(ts.URL) if err == nil { defer func() { if err := resp.Body.Close(); err != nil { panic(err) } }() body, err = ioutil.ReadAll(resp.Body) } return err }, retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration { switch e := err.(type) { case RetryAfterError: if t, err := parseRetryAfter(e.response.Header.Get("Retry-After")); err == nil { return time.Until(t) } case SomeOtherError: return e.retryAfter } //default is backoffdelay return retry.BackOffDelay(n, err, config) }), ) assert.NoError(t, err) assert.NotEmpty(t, body) } // use https://github.com/aereal/go-httpretryafter instead func parseRetryAfter(_ string) (time.Time, error) { return time.Now().Add(1 * time.Second), nil } retry-go-4.6.0/examples/errors_history_test.go000066400000000000000000000017461461015104100215730ustar00rootroot00000000000000package retry_test import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/avast/retry-go/v4" "github.com/stretchr/testify/assert" ) // TestErrorHistory shows an example of how to get all the previous errors when // retry.Do ends in success func TestErrorHistory(t *testing.T) { attempts := 3 // server succeeds after 3 attempts ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if attempts > 0 { attempts-- w.WriteHeader(http.StatusBadGateway) return } w.WriteHeader(http.StatusOK) })) defer ts.Close() var allErrors []error err := retry.Do( func() error { resp, err := http.Get(ts.URL) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("failed HTTP - %d", resp.StatusCode) } return nil }, retry.OnRetry(func(n uint, err error) { allErrors = append(allErrors, err) }), ) assert.NoError(t, err) assert.Len(t, allErrors, 3) } retry-go-4.6.0/examples/http_get_test.go000066400000000000000000000012301461015104100203000ustar00rootroot00000000000000package retry_test import ( "fmt" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/avast/retry-go/v4" "github.com/stretchr/testify/assert" ) func TestGet(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") })) defer ts.Close() var body []byte err := retry.Do( func() error { resp, err := http.Get(ts.URL) if err == nil { defer func() { if err := resp.Body.Close(); err != nil { panic(err) } }() body, err = ioutil.ReadAll(resp.Body) } return err }, ) assert.NoError(t, err) assert.NotEmpty(t, body) } retry-go-4.6.0/go.mod000066400000000000000000000003541461015104100143720ustar00rootroot00000000000000module github.com/avast/retry-go/v4 go 1.18 require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) retry-go-4.6.0/go.sum000066400000000000000000000015611461015104100144200ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= retry-go-4.6.0/options.go000066400000000000000000000157211461015104100153120ustar00rootroot00000000000000package retry import ( "context" "math" "math/rand" "time" ) // Function signature of retry if function type RetryIfFunc func(error) bool // Function signature of OnRetry function type OnRetryFunc func(attempt uint, err error) // DelayTypeFunc is called to return the next delay to wait after the retriable function fails on `err` after `n` attempts. type DelayTypeFunc func(n uint, err error, config *Config) time.Duration // Timer represents the timer used to track time for a retry. type Timer interface { After(time.Duration) <-chan time.Time } type Config struct { attempts uint attemptsForError map[error]uint delay time.Duration maxDelay time.Duration maxJitter time.Duration onRetry OnRetryFunc retryIf RetryIfFunc delayType DelayTypeFunc lastErrorOnly bool context context.Context timer Timer wrapContextErrorWithLastError bool maxBackOffN uint } // Option represents an option for retry. type Option func(*Config) func emptyOption(c *Config) {} // return the direct last error that came from the retried function // default is false (return wrapped errors with everything) func LastErrorOnly(lastErrorOnly bool) Option { return func(c *Config) { c.lastErrorOnly = lastErrorOnly } } // Attempts set count of retry. Setting to 0 will retry until the retried function succeeds. // default is 10 func Attempts(attempts uint) Option { return func(c *Config) { c.attempts = attempts } } // UntilSucceeded will retry until the retried function succeeds. Equivalent to setting Attempts(0). func UntilSucceeded() Option { return func(c *Config) { c.attempts = 0 } } // AttemptsForError sets count of retry in case execution results in given `err` // Retries for the given `err` are also counted against total retries. // The retry will stop if any of given retries is exhausted. // // added in 4.3.0 func AttemptsForError(attempts uint, err error) Option { return func(c *Config) { c.attemptsForError[err] = attempts } } // Delay set delay between retry // default is 100ms func Delay(delay time.Duration) Option { return func(c *Config) { c.delay = delay } } // MaxDelay set maximum delay between retry // does not apply by default func MaxDelay(maxDelay time.Duration) Option { return func(c *Config) { c.maxDelay = maxDelay } } // MaxJitter sets the maximum random Jitter between retries for RandomDelay func MaxJitter(maxJitter time.Duration) Option { return func(c *Config) { c.maxJitter = maxJitter } } // DelayType set type of the delay between retries // default is BackOff func DelayType(delayType DelayTypeFunc) Option { if delayType == nil { return emptyOption } return func(c *Config) { c.delayType = delayType } } // BackOffDelay is a DelayType which increases delay between consecutive retries func BackOffDelay(n uint, _ error, config *Config) time.Duration { // 1 << 63 would overflow signed int64 (time.Duration), thus 62. const max uint = 62 if config.maxBackOffN == 0 { if config.delay <= 0 { config.delay = 1 } config.maxBackOffN = max - uint(math.Floor(math.Log2(float64(config.delay)))) } if n > config.maxBackOffN { n = config.maxBackOffN } return config.delay << n } // FixedDelay is a DelayType which keeps delay the same through all iterations func FixedDelay(_ uint, _ error, config *Config) time.Duration { return config.delay } // RandomDelay is a DelayType which picks a random delay up to config.maxJitter func RandomDelay(_ uint, _ error, config *Config) time.Duration { return time.Duration(rand.Int63n(int64(config.maxJitter))) } // CombineDelay is a DelayType the combines all of the specified delays into a new DelayTypeFunc func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc { const maxInt64 = uint64(math.MaxInt64) return func(n uint, err error, config *Config) time.Duration { var total uint64 for _, delay := range delays { total += uint64(delay(n, err, config)) if total > maxInt64 { total = maxInt64 } } return time.Duration(total) } } // OnRetry function callback are called each retry // // log each retry example: // // retry.Do( // func() error { // return errors.New("some error") // }, // retry.OnRetry(func(n uint, err error) { // log.Printf("#%d: %s\n", n, err) // }), // ) func OnRetry(onRetry OnRetryFunc) Option { if onRetry == nil { return emptyOption } return func(c *Config) { c.onRetry = onRetry } } // RetryIf controls whether a retry should be attempted after an error // (assuming there are any retry attempts remaining) // // skip retry if special error example: // // retry.Do( // func() error { // return errors.New("special error") // }, // retry.RetryIf(func(err error) bool { // if err.Error() == "special error" { // return false // } // return true // }) // ) // // By default RetryIf stops execution if the error is wrapped using `retry.Unrecoverable`, // so above example may also be shortened to: // // retry.Do( // func() error { // return retry.Unrecoverable(errors.New("special error")) // } // ) func RetryIf(retryIf RetryIfFunc) Option { if retryIf == nil { return emptyOption } return func(c *Config) { c.retryIf = retryIf } } // Context allow to set context of retry // default are Background context // // example of immediately cancellation (maybe it isn't the best example, but it describes behavior enough; I hope) // // ctx, cancel := context.WithCancel(context.Background()) // cancel() // // retry.Do( // func() error { // ... // }, // retry.Context(ctx), // ) func Context(ctx context.Context) Option { return func(c *Config) { c.context = ctx } } // WithTimer provides a way to swap out timer module implementations. // This primarily is useful for mocking/testing, where you may not want to explicitly wait for a set duration // for retries. // // example of augmenting time.After with a print statement // // type struct MyTimer {} // // func (t *MyTimer) After(d time.Duration) <- chan time.Time { // fmt.Print("Timer called!") // return time.After(d) // } // // retry.Do( // func() error { ... }, // retry.WithTimer(&MyTimer{}) // ) func WithTimer(t Timer) Option { return func(c *Config) { c.timer = t } } // WrapContextErrorWithLastError allows the context error to be returned wrapped with the last error that the // retried function returned. This is only applicable when Attempts is set to 0 to retry indefinitly and when // using a context to cancel / timeout // // default is false // // ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // // retry.Do( // func() error { // ... // }, // retry.Context(ctx), // retry.Attempts(0), // retry.WrapContextErrorWithLastError(true), // ) func WrapContextErrorWithLastError(wrapContextErrorWithLastError bool) Option { return func(c *Config) { c.wrapContextErrorWithLastError = wrapContextErrorWithLastError } } retry-go-4.6.0/retry.go000066400000000000000000000175711461015104100147710ustar00rootroot00000000000000/* 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)) [More 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 config.onRetry(n, err) n++ 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 } retry-go-4.6.0/retry_test.go000066400000000000000000000331731461015104100160240ustar00rootroot00000000000000package retry import ( "context" "errors" "fmt" "os" "testing" "time" "github.com/stretchr/testify/assert" ) func TestDoWithDataAllFailed(t *testing.T) { var retrySum uint v, err := DoWithData( func() (int, error) { return 7, errors.New("test") }, OnRetry(func(n uint, err error) { retrySum += n }), Delay(time.Nanosecond), ) assert.Error(t, err) assert.Equal(t, 0, v) expectedErrorFormat := `All attempts fail: #1: test #2: test #3: test #4: test #5: test #6: test #7: test #8: test #9: test #10: test` assert.Len(t, err, 10) fmt.Println(err.Error()) assert.Equal(t, expectedErrorFormat, err.Error(), "retry error format") assert.Equal(t, uint(45), retrySum, "right count of retry") } func TestDoFirstOk(t *testing.T) { var retrySum uint err := Do( func() error { return nil }, OnRetry(func(n uint, err error) { retrySum += n }), ) assert.NoError(t, err) assert.Equal(t, uint(0), retrySum, "no retry") } func TestDoWithDataFirstOk(t *testing.T) { returnVal := 1 var retrySum uint val, err := DoWithData( func() (int, error) { return returnVal, nil }, OnRetry(func(n uint, err error) { retrySum += n }), ) assert.NoError(t, err) assert.Equal(t, returnVal, val) assert.Equal(t, uint(0), retrySum, "no retry") } func TestRetryIf(t *testing.T) { var retryCount uint err := Do( func() error { if retryCount >= 2 { return errors.New("special") } else { return errors.New("test") } }, OnRetry(func(n uint, err error) { retryCount++ }), RetryIf(func(err error) bool { return err.Error() != "special" }), Delay(time.Nanosecond), ) assert.Error(t, err) expectedErrorFormat := `All attempts fail: #1: test #2: test #3: special` assert.Len(t, err, 3) assert.Equal(t, expectedErrorFormat, err.Error(), "retry error format") assert.Equal(t, uint(2), retryCount, "right count of retry") } func TestRetryIf_ZeroAttempts(t *testing.T) { var retryCount, onRetryCount uint err := Do( func() error { if retryCount >= 2 { return errors.New("special") } else { retryCount++ return errors.New("test") } }, OnRetry(func(n uint, err error) { onRetryCount = n }), RetryIf(func(err error) bool { return err.Error() != "special" }), Delay(time.Nanosecond), Attempts(0), ) assert.Error(t, err) assert.Equal(t, "special", err.Error(), "retry error format") assert.Equal(t, retryCount, onRetryCount+1, "right count of retry") } func TestZeroAttemptsWithError(t *testing.T) { const maxErrors = 999 count := 0 err := Do( func() error { if count < maxErrors { count += 1 return errors.New("test") } return nil }, Attempts(0), MaxDelay(time.Nanosecond), ) assert.NoError(t, err) assert.Equal(t, count, maxErrors) } func TestZeroAttemptsWithoutError(t *testing.T) { count := 0 err := Do( func() error { count++ return nil }, Attempts(0), ) assert.NoError(t, err) assert.Equal(t, count, 1) } func TestZeroAttemptsWithUnrecoverableError(t *testing.T) { err := Do( func() error { return Unrecoverable(assert.AnError) }, Attempts(0), MaxDelay(time.Nanosecond), ) assert.Error(t, err) assert.Equal(t, Unrecoverable(assert.AnError), err) } func TestAttemptsForError(t *testing.T) { count := uint(0) testErr := os.ErrInvalid attemptsForTestError := uint(3) err := Do( func() error { count++ return testErr }, AttemptsForError(attemptsForTestError, testErr), Attempts(5), ) assert.Error(t, err) assert.Equal(t, attemptsForTestError, count) } func TestDefaultSleep(t *testing.T) { start := time.Now() err := Do( func() error { return errors.New("test") }, Attempts(3), ) dur := time.Since(start) assert.Error(t, err) assert.Greater(t, dur, 300*time.Millisecond, "3 times default retry is longer then 300ms") } func TestFixedSleep(t *testing.T) { start := time.Now() err := Do( func() error { return errors.New("test") }, Attempts(3), DelayType(FixedDelay), ) dur := time.Since(start) assert.Error(t, err) assert.Less(t, dur, 500*time.Millisecond, "3 times default retry is shorter then 500ms") } func TestLastErrorOnly(t *testing.T) { var retrySum uint err := Do( func() error { return fmt.Errorf("%d", retrySum) }, OnRetry(func(n uint, err error) { retrySum += 1 }), Delay(time.Nanosecond), LastErrorOnly(true), ) assert.Error(t, err) assert.Equal(t, "9", err.Error()) } func TestUnrecoverableError(t *testing.T) { attempts := 0 testErr := errors.New("error") expectedErr := Error{testErr} err := Do( func() error { attempts++ return Unrecoverable(testErr) }, Attempts(2), ) assert.Equal(t, expectedErr, err) assert.Equal(t, testErr, errors.Unwrap(err)) assert.Equal(t, 1, attempts, "unrecoverable error broke the loop") } func TestCombineFixedDelays(t *testing.T) { if os.Getenv("OS") == "macos-latest" { t.Skip("Skipping testing in MacOS GitHub actions - too slow, duration is wrong") } start := time.Now() err := Do( func() error { return errors.New("test") }, Attempts(3), DelayType(CombineDelay(FixedDelay, FixedDelay)), ) dur := time.Since(start) assert.Error(t, err) assert.Greater(t, dur, 400*time.Millisecond, "3 times combined, fixed retry is greater then 400ms") assert.Less(t, dur, 500*time.Millisecond, "3 times combined, fixed retry is less then 500ms") } func TestRandomDelay(t *testing.T) { if os.Getenv("OS") == "macos-latest" { t.Skip("Skipping testing in MacOS GitHub actions - too slow, duration is wrong") } start := time.Now() err := Do( func() error { return errors.New("test") }, Attempts(3), DelayType(RandomDelay), MaxJitter(50*time.Millisecond), ) dur := time.Since(start) assert.Error(t, err) assert.Greater(t, dur, 2*time.Millisecond, "3 times random retry is longer then 2ms") assert.Less(t, dur, 150*time.Millisecond, "3 times random retry is shorter then 150ms") } func TestMaxDelay(t *testing.T) { if os.Getenv("OS") == "macos-latest" { t.Skip("Skipping testing in MacOS GitHub actions - too slow, duration is wrong") } start := time.Now() err := Do( func() error { return errors.New("test") }, Attempts(5), Delay(10*time.Millisecond), MaxDelay(50*time.Millisecond), ) dur := time.Since(start) assert.Error(t, err) assert.Greater(t, dur, 120*time.Millisecond, "5 times with maximum delay retry is less than 120ms") assert.Less(t, dur, 250*time.Millisecond, "5 times with maximum delay retry is longer than 250ms") } func TestBackOffDelay(t *testing.T) { for _, c := range []struct { label string delay time.Duration expectedMaxN uint n uint expectedDelay time.Duration }{ { label: "negative-delay", delay: -1, expectedMaxN: 62, n: 2, expectedDelay: 4, }, { label: "zero-delay", delay: 0, expectedMaxN: 62, n: 65, expectedDelay: 1 << 62, }, { label: "one-second", delay: time.Second, expectedMaxN: 33, n: 62, expectedDelay: time.Second << 33, }, } { t.Run( c.label, func(t *testing.T) { config := Config{ delay: c.delay, } delay := BackOffDelay(c.n, nil, &config) assert.Equal(t, c.expectedMaxN, config.maxBackOffN, "max n mismatch") assert.Equal(t, c.expectedDelay, delay, "delay duration mismatch") }, ) } } func TestCombineDelay(t *testing.T) { f := func(d time.Duration) DelayTypeFunc { return func(_ uint, _ error, _ *Config) time.Duration { return d } } const max = time.Duration(1<<63 - 1) for _, c := range []struct { label string delays []time.Duration expected time.Duration }{ { label: "empty", }, { label: "single", delays: []time.Duration{ time.Second, }, expected: time.Second, }, { label: "negative", delays: []time.Duration{ time.Second, -time.Millisecond, }, expected: time.Second - time.Millisecond, }, { label: "overflow", delays: []time.Duration{ max, time.Second, time.Millisecond, }, expected: max, }, } { t.Run( c.label, func(t *testing.T) { funcs := make([]DelayTypeFunc, len(c.delays)) for i, d := range c.delays { funcs[i] = f(d) } actual := CombineDelay(funcs...)(0, nil, nil) assert.Equal(t, c.expected, actual, "delay duration mismatch") }, ) } } func TestContext(t *testing.T) { const defaultDelay = 100 * time.Millisecond t.Run("cancel before", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() retrySum := 0 start := time.Now() err := Do( func() error { return errors.New("test") }, OnRetry(func(n uint, err error) { retrySum += 1 }), Context(ctx), ) dur := time.Since(start) assert.Error(t, err) assert.True(t, dur < defaultDelay, "immediately cancellation") assert.Equal(t, 0, retrySum, "called at most once") }) t.Run("cancel in retry progress", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) retrySum := 0 err := Do( func() error { return errors.New("test") }, OnRetry(func(n uint, err error) { retrySum += 1 if retrySum > 1 { cancel() } }), Context(ctx), ) assert.Error(t, err) expectedErrorFormat := `All attempts fail: #1: test #2: test #3: context canceled` assert.Len(t, err, 3) assert.Equal(t, expectedErrorFormat, err.Error(), "retry error format") assert.Equal(t, 2, retrySum, "called at most once") }) t.Run("cancel in retry progress - last error only", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) retrySum := 0 err := Do( func() error { return errors.New("test") }, OnRetry(func(n uint, err error) { retrySum += 1 if retrySum > 1 { cancel() } }), Context(ctx), LastErrorOnly(true), ) assert.Equal(t, context.Canceled, err) assert.Equal(t, 2, retrySum, "called at most once") }) t.Run("cancel in retry progress - infinite attempts", func(t *testing.T) { go func() { ctx, cancel := context.WithCancel(context.Background()) retrySum := 0 err := Do( func() error { return errors.New("test") }, OnRetry(func(n uint, err error) { fmt.Println(n) retrySum += 1 if retrySum > 1 { cancel() } }), Context(ctx), Attempts(0), ) assert.Equal(t, context.Canceled, err) assert.Equal(t, 2, retrySum, "called at most once") }() }) t.Run("cancelled on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() retrySum := 0 err := Do( func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} }, OnRetry(func(n uint, err error) { retrySum += 1 if retrySum == 2 { cancel() } }), Context(ctx), Attempts(0), WrapContextErrorWithLastError(true), ) assert.ErrorIs(t, err, context.Canceled) assert.ErrorIs(t, err, fooErr{str: "error 2"}) }) t.Run("timed out on retry infinte attempts - wraps context error with last retried function error", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() retrySum := 0 err := Do( func() error { return fooErr{str: fmt.Sprintf("error %d", retrySum+1)} }, OnRetry(func(n uint, err error) { retrySum += 1 }), Context(ctx), Attempts(0), WrapContextErrorWithLastError(true), ) assert.ErrorIs(t, err, context.DeadlineExceeded) assert.ErrorIs(t, err, fooErr{str: "error 2"}) }) } type testTimer struct { called bool } func (t *testTimer) After(d time.Duration) <-chan time.Time { t.called = true return time.After(d) } func TestTimerInterface(t *testing.T) { var timer testTimer err := Do( func() error { return errors.New("test") }, Attempts(1), Delay(10*time.Millisecond), MaxDelay(50*time.Millisecond), WithTimer(&timer), ) assert.Error(t, err) } func TestErrorIs(t *testing.T) { var e Error expectErr := errors.New("error") closedErr := os.ErrClosed e = append(e, expectErr) e = append(e, closedErr) assert.True(t, errors.Is(e, expectErr)) assert.True(t, errors.Is(e, closedErr)) assert.False(t, errors.Is(e, errors.New("error"))) } type fooErr struct{ str string } func (e fooErr) Error() string { return e.str } type barErr struct{ str string } func (e barErr) Error() string { return e.str } func TestErrorAs(t *testing.T) { var e Error fe := fooErr{str: "foo"} e = append(e, fe) var tf fooErr var tb barErr assert.True(t, errors.As(e, &tf)) assert.False(t, errors.As(e, &tb)) assert.Equal(t, "foo", tf.str) } func TestUnwrap(t *testing.T) { testError := errors.New("test error") err := Do( func() error { return testError }, Attempts(1), ) assert.Error(t, err) assert.Equal(t, testError, errors.Unwrap(err)) } func BenchmarkDo(b *testing.B) { testError := errors.New("test error") for i := 0; i < b.N; i++ { _ = Do( func() error { return testError }, Attempts(10), Delay(0), ) } } func BenchmarkDoWithData(b *testing.B) { testError := errors.New("test error") for i := 0; i < b.N; i++ { _, _ = DoWithData( func() (int, error) { return 0, testError }, Attempts(10), Delay(0), ) } } func BenchmarkDoNoErrors(b *testing.B) { for i := 0; i < b.N; i++ { _ = Do( func() error { return nil }, Attempts(10), Delay(0), ) } } func BenchmarkDoWithDataNoErrors(b *testing.B) { for i := 0; i < b.N; i++ { _, _ = DoWithData( func() (int, error) { return 0, nil }, Attempts(10), Delay(0), ) } } func TestIsRecoverable(t *testing.T) { err := errors.New("err") assert.True(t, IsRecoverable(err)) err = Unrecoverable(err) assert.False(t, IsRecoverable(err)) err = fmt.Errorf("wrapping: %w", err) assert.False(t, IsRecoverable(err)) }