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

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

More details

Full run details