New Upstream Snapshot - golang-github-avast-retry-go
Ready changes
Summary
Merged new upstream version: 4.0.4+git20220610.1.2616d60+ds (was: 2.4.3).
Resulting package
Built on 2022-09-29T06:51 (took 31m26s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots golang-github-avast-retry-go-dev
Lintian Result
- golang-github-avast-retry-go-dev_4.0.4+git20220610.1.2616d60+ds-1~jan+nus2_all.deb
- golang-github-avast-retry-go_4.0.4+git20220610.1.2616d60+ds-1~jan+nus2.dsc
- golang-github-avast-retry-go_4.0.4+git20220610.1.2616d60+ds-1~jan+nus2_amd64.buildinfo
- golang-github-avast-retry-go_4.0.4+git20220610.1.2616d60+ds-1~jan+nus2_amd64.changes
Diff
diff --git a/.travis.yml b/.travis.yml
index a0c14a0..2b8366a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,9 @@
language: go
go:
- - 1.6
- - 1.7
- - 1.8
- - 1.9
- - "1.10"
- - 1.11
- - 1.12
+ - 1.13
+ - 1.14
+ - 1.15
install:
- make setup
diff --git a/Gopkg.toml b/Gopkg.toml
deleted file mode 100644
index cf8c9eb..0000000
--- a/Gopkg.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-[[constraint]]
- name = "github.com/stretchr/testify"
- version = "1.1.4"
diff --git a/Makefile b/Makefile
index 769816d..5b8b27e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,20 +1,17 @@
SOURCE_FILES?=$$(go list ./... | grep -v /vendor/)
TEST_PATTERN?=.
TEST_OPTIONS?=
-DEP?=$$(which dep)
VERSION?=$$(cat VERSION)
LINTER?=$$(which golangci-lint)
LINTER_VERSION=1.15.0
ifeq ($(OS),Windows_NT)
- DEP_VERS=dep-windows-amd64
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
- DEP_VERS=dep-linux-amd64
LINTER_FILE=golangci-lint-$(LINTER_VERSION)-linux-amd64.tar.gz
LINTER_UNPACK= | tar xzf - -C $$GOPATH/bin --wildcards --strip 1 "**/golangci-lint"
endif
@@ -27,11 +24,7 @@ setup:
curl -L https://github.com/golangci/golangci-lint/releases/download/v$(LINTER_VERSION)/$(LINTER_FILE) $(LINTER_UNPACK) ;\
chmod +x $$GOPATH/bin/golangci-lint;\
fi
- @if [ "$(DEP)" = "" ]; then\
- curl -L https://github.com/golang/dep/releases/download/v0.3.1/$(DEP_VERS) >| $$GOPATH/bin/dep;\
- chmod +x $$GOPATH/bin/dep;\
- fi
- dep ensure
+ go mod download
generate: ## Generate README.md
godocdown >| README.md
diff --git a/README.md b/README.md
index b282110..dd0aeae 100644
--- a/README.md
+++ b/README.md
@@ -64,28 +64,31 @@ nonintuitive interface (for me)
### BREAKING CHANGES
-1.0.2 -> 2.0.0
+* 4.0.0
-* argument of `retry.Delay` is final delay (no multiplication by `retry.Units`
-anymore)
+ * infinity retry is possible by set `Attempts(0)` by PR [#49](https://github.com/avast/retry-go/pull/49)
-* function `retry.Units` are removed
+* 3.0.0
-* [more about this breaking change](https://github.com/avast/retry-go/issues/7)
+ * `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).
-0.3.0 -> 1.0.0
+* 1.0.2 -> 2.0.0
-* `retry.Retry` function are changed to `retry.Do` function
+ * 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)
-* `retry.RetryCustom` (OnRetry) and `retry.RetryCustomWithOpts` functions are
-now implement via functions produces Options (aka `retry.OnRetry`)
+* 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, config *Config) time.Duration
+func BackOffDelay(n uint, _ error, config *Config) time.Duration
```
BackOffDelay is a DelayType which increases delay between consecutive retries
@@ -98,7 +101,7 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error
#### func FixedDelay
```go
-func FixedDelay(_ uint, config *Config) time.Duration
+func FixedDelay(_ uint, _ error, config *Config) time.Duration
```
FixedDelay is a DelayType which keeps delay the same through all iterations
@@ -109,10 +112,17 @@ 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) unrecoverableError
+func Unrecoverable(err error) error
```
Unrecoverable wraps an error in `unrecoverableError` struct
@@ -127,9 +137,19 @@ type Config struct {
#### type DelayTypeFunc
```go
-type DelayTypeFunc func(n uint, config *Config) time.Duration
+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
@@ -178,7 +198,28 @@ Option represents an option for retry.
```go
func Attempts(attempts uint) Option
```
-Attempts set count of retry default is 10
+Attempts set count of retry. Setting to 0 will retry until the retried function
+succeeds. default is 10
+
+#### 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
@@ -202,6 +243,20 @@ 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
@@ -242,7 +297,7 @@ skip retry if special error example:
})
)
-The default RetryIf stops execution if the error is wrapped using
+By default RetryIf stops execution if the error is wrapped using
`retry.Unrecoverable`, so above example may also be shortened to:
retry.Do(
diff --git a/VERSION b/VERSION
index 35cee72..c4e41f9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.4.3
+4.0.3
diff --git a/debian/changelog b/debian/changelog
index fdec8ea..8ef4f36 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,11 +1,12 @@
-golang-github-avast-retry-go (2.4.3-2) UNRELEASED; urgency=medium
+golang-github-avast-retry-go (4.0.4+git20220610.1.2616d60+ds-1) UNRELEASED; urgency=medium
* Bump debhelper from old 12 to 13.
* Set upstream metadata fields: Bug-Database, Bug-Submit, Repository,
Repository-Browse.
* Update standards version to 4.6.0, no changes needed.
+ * New upstream snapshot.
- -- Debian Janitor <janitor@jelmer.uk> Fri, 02 Sep 2022 02:58:03 -0000
+ -- Debian Janitor <janitor@jelmer.uk> Thu, 29 Sep 2022 06:27:54 -0000
golang-github-avast-retry-go (2.4.3-1) unstable; urgency=medium
diff --git a/examples/custom_retry_function_test.go b/examples/custom_retry_function_test.go
index bf6f7b3..2a48229 100644
--- a/examples/custom_retry_function_test.go
+++ b/examples/custom_retry_function_test.go
@@ -1,22 +1,54 @@
package retry_test
import (
+ "fmt"
"io/ioutil"
"net/http"
+ "net/http/httptest"
+ "strconv"
"testing"
"time"
- "github.com/avast/retry-go"
+ "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) {
- url := "http://example.com"
+ 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(url)
+ resp, err := http.Get(ts.URL)
if err == nil {
defer func() {
@@ -25,15 +57,41 @@ func TestCustomRetryFunction(t *testing.T) {
}
}()
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, config *retry.Config) time.Duration {
- return 0
+ 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.NotEmpty(t, body)
+ assert.Equal(t, "hello", string(body))
}
diff --git a/examples/delay_based_on_error_test.go b/examples/delay_based_on_error_test.go
new file mode 100644
index 0000000..55e5dda
--- /dev/null
+++ b/examples/delay_based_on_error_test.go
@@ -0,0 +1,84 @@
+// 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
+}
diff --git a/examples/http_get_test.go b/examples/http_get_test.go
index 7ccdd56..f1a064a 100644
--- a/examples/http_get_test.go
+++ b/examples/http_get_test.go
@@ -1,21 +1,27 @@
package retry_test
import (
+ "fmt"
"io/ioutil"
"net/http"
+ "net/http/httptest"
"testing"
- "github.com/avast/retry-go"
+ "github.com/avast/retry-go/v4"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
- url := "http://example.com"
+ 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(url)
+ resp, err := http.Get(ts.URL)
if err == nil {
defer func() {
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..bcaaa23
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,11 @@
+module github.com/avast/retry-go/v4
+
+go 1.13
+
+require (
+ github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0 // indirect
+ github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481 // indirect
+ github.com/stretchr/testify v1.7.0
+ golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42 // indirect
+ golang.org/x/tools v0.1.7 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..1ca3979
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,38 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0 h1:i5VIxp6QB8oWZ8IkK8zrDgeT6ORGIUeiN+61iETwJbI=
+github.com/pierrre/gotestcover v0.0.0-20160517101806-924dca7d15f0/go.mod h1:4xpMLz7RBWyB+ElzHu8Llua96TRCB3YwX+l5EP1wmHk=
+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/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/options.go b/options.go
index db20f5c..b18cf65 100644
--- a/options.go
+++ b/options.go
@@ -1,6 +1,9 @@
package retry
import (
+ "context"
+ "math"
+ "math/rand"
"time"
)
@@ -11,20 +14,34 @@ type RetryIfFunc func(error) bool
// n = count of attempts
type OnRetryFunc func(n uint, err error)
-type DelayTypeFunc func(n uint, config *Config) time.Duration
+// 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
delay time.Duration
+ maxDelay time.Duration
+ maxJitter time.Duration
onRetry OnRetryFunc
retryIf RetryIfFunc
delayType DelayTypeFunc
lastErrorOnly bool
+ context context.Context
+ timer Timer
+
+ 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 {
@@ -33,7 +50,7 @@ func LastErrorOnly(lastErrorOnly bool) Option {
}
}
-// Attempts set count of retry
+// 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) {
@@ -49,24 +66,79 @@ func Delay(delay time.Duration) Option {
}
}
+// 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, config *Config) time.Duration {
- return config.delay * (1 << n)
+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, config *Config) time.Duration {
+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:
@@ -80,6 +152,9 @@ func FixedDelay(_ uint, config *Config) time.Duration {
// }),
// )
func OnRetry(onRetry OnRetryFunc) Option {
+ if onRetry == nil {
+ return emptyOption
+ }
return func(c *Config) {
c.onRetry = onRetry
}
@@ -111,7 +186,54 @@ func OnRetry(onRetry OnRetryFunc) Option {
// }
// )
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
+ }
+}
diff --git a/retry.go b/retry.go
index ea7f367..87b60f6 100644
--- a/retry.go
+++ b/retry.go
@@ -43,28 +43,27 @@ SEE ALSO
* [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me)
-BREAKING CHANGES
-
-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)
+BREAKING CHANGES
-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`)
+* 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"
"fmt"
"strings"
"time"
@@ -73,24 +72,44 @@ import (
// Function signature of retryable function
type RetryableFunc func() 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 {
var n uint
- //default
- config := &Config{
- attempts: 10,
- delay: 100 * time.Millisecond,
- onRetry: func(n uint, err error) {},
- retryIf: IsRecoverable,
- delayType: BackOffDelay,
- lastErrorOnly: false,
- }
+ // default
+ config := newDefaultRetryConfig()
- //apply opts
+ // apply opts
for _, opt := range opts {
opt(config)
}
+ if err := config.context.Err(); err != nil {
+ return err
+ }
+
+ // Setting attempts to 0 means we'll retry until we succeed
+ if config.attempts == 0 {
+ for err := retryableFunc(); err != nil; err = retryableFunc() {
+ n++
+
+ config.onRetry(n, err)
+ select {
+ case <-time.After(delay(config, n, err)):
+ case <-config.context.Done():
+ return nil
+ }
+ }
+
+ return nil
+ }
+
var errorLog Error
if !config.lastErrorOnly {
errorLog = make(Error, config.attempts)
@@ -116,8 +135,16 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
break
}
- delayTime := config.delayType(n, config)
- time.Sleep(delayTime)
+ select {
+ case <-config.timer.After(delay(config, n, err)):
+ case <-config.context.Done():
+ if config.lastErrorOnly {
+ return config.context.Err()
+ }
+ errorLog[n] = config.context.Err()
+ return errorLog
+ }
+
} else {
return nil
}
@@ -134,6 +161,20 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
return errorLog
}
+func newDefaultRetryConfig() *Config {
+ return &Config{
+ attempts: uint(10),
+ 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
@@ -172,6 +213,10 @@ type unrecoverableError struct {
error
}
+func (e unrecoverableError) Unwrap() error {
+ return e.error
+}
+
// Unrecoverable wraps an error in `unrecoverableError` struct
func Unrecoverable(err error) error {
return unrecoverableError{err}
@@ -190,3 +235,12 @@ func unpackUnrecoverable(err error) 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
+}
diff --git a/retry_test.go b/retry_test.go
index 8a33d74..6b96756 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -1,12 +1,12 @@
package retry
import (
+ "context"
"errors"
+ "fmt"
"testing"
"time"
- "fmt"
-
"github.com/stretchr/testify/assert"
)
@@ -72,6 +72,43 @@ func TestRetryIf(t *testing.T) {
}
+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 TestDefaultSleep(t *testing.T) {
start := time.Now()
err := Do(
@@ -121,3 +158,267 @@ func TestUnrecoverableError(t *testing.T) {
assert.Equal(t, expectedErr, err)
assert.Equal(t, 1, attempts, "unrecoverable error broke the loop")
}
+
+func TestCombineFixedDelays(t *testing.T) {
+ 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.True(t, dur > 400*time.Millisecond, "3 times combined, fixed retry is longer then 400ms")
+ assert.True(t, dur < 500*time.Millisecond, "3 times combined, fixed retry is shorter then 500ms")
+}
+
+func TestRandomDelay(t *testing.T) {
+ 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.True(t, dur > 2*time.Millisecond, "3 times random retry is longer then 2ms")
+ assert.True(t, dur < 100*time.Millisecond, "3 times random retry is shorter then 100ms")
+}
+
+func TestMaxDelay(t *testing.T) {
+ 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.True(t, dur > 120*time.Millisecond, "5 times with maximum delay retry is longer than 120ms")
+ assert.True(t, dur < 205*time.Millisecond, "5 times with maximum delay retry is shorter than 205ms")
+}
+
+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: context canceled`
+ 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) {
+ testFailedInRetry := make(chan bool)
+ testEnded := make(chan bool)
+
+ 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()
+ }
+
+ if retrySum > 2 {
+ testFailedInRetry <- true
+ }
+
+ }),
+ Context(ctx),
+ Attempts(0),
+ )
+
+ assert.NoError(t, err, "infinite attempts should not report error")
+ assert.Error(t, ctx.Err(), "immediately canceled after context cancel called")
+ testEnded <- true
+ }()
+
+ select {
+ case <-testFailedInRetry:
+ t.Error("Test ran longer than expected, cancel did not work")
+ case <-testEnded:
+ }
+
+ })
+}
+
+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)
+
+}
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/doc/golang-github-avast-retry-go-dev/examples/delay_based_on_error_test.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/avast/retry-go/go.mod -rw-r--r-- root/root /usr/share/gocode/src/github.com/avast/retry-go/go.sum
No differences were encountered in the control files