Skip to content

Commit

Permalink
Merge pull request #35 from avast/delay_function_based_on_error
Browse files Browse the repository at this point in the history
delay based on error
  • Loading branch information
JaSei authored Oct 13, 2020
2 parents bf0c2cc + b009386 commit abbd4ed
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 23 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: go

go:
- 1.7
- 1.8
- 1.9
- "1.10"
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ 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
BREAKING CHANGES 3.0.0 * `DelayTypeFunc` accepts a new parameter `err` - this
breaking change affects only your custom Delay Functions. This change allow
[make delay functions based on error](examples/delay_based_on_error_test.go).

1.0.2 -> 2.0.0

Expand Down Expand Up @@ -98,7 +99,7 @@ var (
#### 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

Expand All @@ -111,7 +112,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

Expand All @@ -125,7 +126,7 @@ IsRecoverable checks if error is an instance of `unrecoverableError`
#### func RandomDelay

```go
func RandomDelay(_ uint, config *Config) time.Duration
func RandomDelay(_ uint, _ error, config *Config) time.Duration
```
RandomDelay is a DelayType which picks a random delay up to config.maxJitter

Expand All @@ -147,9 +148,11 @@ 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

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.0
3.0.0
12 changes: 9 additions & 3 deletions examples/custom_retry_function_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package retry_test

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"

Expand All @@ -11,12 +13,16 @@ import (
)

func TestCustomRetryFunction(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() {
Expand All @@ -29,7 +35,7 @@ func TestCustomRetryFunction(t *testing.T) {

return err
},
retry.DelayType(func(n uint, config *retry.Config) time.Duration {
retry.DelayType(func(n uint, _ error, config *retry.Config) time.Duration {
return 0
}),
)
Expand Down
84 changes: 84 additions & 0 deletions examples/delay_based_on_error_test.go
Original file line number Diff line number Diff line change
@@ -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"
"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
}
10 changes: 8 additions & 2 deletions examples/http_get_test.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package retry_test

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/avast/retry-go"
"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() {
Expand Down
16 changes: 10 additions & 6 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ 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

type Config struct {
attempts uint
Expand Down Expand Up @@ -81,45 +82,48 @@ func DelayType(delayType DelayTypeFunc) Option {
}

// BackOffDelay is a DelayType which increases delay between consecutive retries
func BackOffDelay(n uint, config *Config) time.Duration {
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, config *Config) time.Duration {
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, config *Config) time.Duration {
return func(n uint, err error, config *Config) time.Duration {
var total uint64
for _, delay := range delays {
total += uint64(delay(n, config))
total += uint64(delay(n, err, config))
if total > maxInt64 {
total = maxInt64
}
}

return time.Duration(total)
}
}
Expand Down
5 changes: 4 additions & 1 deletion retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ SEE ALSO
* [matryer/try](https://github.com/matryer/try) - very popular package, nonintuitive interface (for me)
BREAKING CHANGES
3.0.0
* `DelayTypeFunc` accepts a new parameter `err` - this breaking change affects only your custom Delay Functions. This change allow [make delay functions based on error](examples/delay_based_on_error_test.go).
1.0.2 -> 2.0.0
Expand Down Expand Up @@ -134,7 +137,7 @@ func Do(retryableFunc RetryableFunc, opts ...Option) error {
break
}

delayTime := config.delayType(n, config)
delayTime := config.delayType(n, err, config)
if config.maxDelay > 0 && delayTime > config.maxDelay {
delayTime = config.maxDelay
}
Expand Down
6 changes: 3 additions & 3 deletions retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func TestBackOffDelay(t *testing.T) {
config := Config{
delay: c.delay,
}
delay := BackOffDelay(c.n, &config)
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")
},
Expand All @@ -209,7 +209,7 @@ func TestBackOffDelay(t *testing.T) {

func TestCombineDelay(t *testing.T) {
f := func(d time.Duration) DelayTypeFunc {
return func(_ uint, _ *Config) time.Duration {
return func(_ uint, _ error, _ *Config) time.Duration {
return d
}
}
Expand Down Expand Up @@ -254,7 +254,7 @@ func TestCombineDelay(t *testing.T) {
for i, d := range c.delays {
funcs[i] = f(d)
}
actual := CombineDelay(funcs...)(0, nil)
actual := CombineDelay(funcs...)(0, nil, nil)
assert.Equal(t, c.expected, actual, "delay duration mismatch")
},
)
Expand Down

0 comments on commit abbd4ed

Please sign in to comment.