Repository: go-resty/resty
Pull request #384 (2020-10-30 16:39:52)

Added AddRetryAfterErrorCondition() option
A really simple option so folks can set the retry options with easy and less verbosity in their code.

codecov[bot]: # [Codecov](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=h1) Report
> Merging [#384](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=desc) into [master](https://codecov.io/gh/go-resty/resty/commit/9e4ce7a0b8e317309b9a8a3b4c7d4f058ac26a84?el=desc) will **decrease** coverage by `0.28%`.
> The diff coverage is `25.00%`.

[![Impacted file tree graph](https://codecov.io/gh/go-resty/resty/pull/384/graphs/tree.svg?width=650&height=150&src=pr&token=9t4RCu3fVO)](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #384      +/-   ##
==========================================
- Coverage   95.28%   95.00%   -0.29%     
==========================================
  Files          10       10              
  Lines         997     1001       +4     
==========================================
+ Hits          950      951       +1     
- Misses         26       29       +3     
  Partials       21       21              
```


| [Impacted Files](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [client.go](https://codecov.io/gh/go-resty/resty/pull/384/diff?src=pr&el=tree#diff-Y2xpZW50Lmdv) | `94.60% <25.00%> (-1.18%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=footer). Last update [9e4ce7a...6a5f23e](https://codecov.io/gh/go-resty/resty/pull/384?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

gtpaulose: Is there anymore changes required?

Patch:
diff --git a/client.go b/client.go
index 26f1385..4e54f69 100644
--- a/client.go
+++ b/client.go
@@ -578,6 +578,19 @@ func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client {
 	return c
 }
 
+// AddRetryAfterErrorCondition adds the basic condition of retrying after encountering
+// an error from the http response
+func (c *Client) AddRetryAfterErrorCondition() *Client {
+	c.AddRetryCondition(func(response *Response, err error) bool {
+		if response.IsError() {
+			return true
+		}
+
+		return false
+	})
+	return c
+}
+
 // SetTLSClientConfig method sets TLSClientConfig for underling client Transport.
 //
 // For Example:
diff --git a/retry.go b/retry.go
index 7500b70..a56f3b4 100644
--- a/retry.go
+++ b/retry.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"math"
 	"math/rand"
+	"sync"
 	"time"
 )
 
@@ -133,46 +134,67 @@ func Backoff(operation func() (*Response, error), options ...Option) error {
 
 func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) {
 	const maxInt = 1<<31 - 1 // max int for arch 386
-
 	if max < 0 {
 		max = maxInt
 	}
-
 	if resp == nil {
-		goto defaultCase
+		return jitterBackoff(min, max, attempt), nil
 	}
 
-	// 1. Check for custom callback
-	if retryAfterFunc := resp.Request.client.RetryAfter; retryAfterFunc != nil {
-		result, err := retryAfterFunc(resp.Request.client, resp)
-		if err != nil {
-			return 0, err // i.e. 'API quota exceeded'
-		}
-		if result == 0 {
-			goto defaultCase
-		}
-		if result < 0 || max < result {
-			result = max
-		}
-		if result < min {
-			result = min
-		}
-		return result, nil
+	retryAfterFunc := resp.Request.client.RetryAfter
+
+	// Check for custom callback
+	if retryAfterFunc == nil {
+		return jitterBackoff(min, max, attempt), nil
 	}
 
-	// 2. Return capped exponential backoff with jitter
-	// http://www.awsarchitectureblog.com/2015/03/backoff.html
-defaultCase:
+	result, err := retryAfterFunc(resp.Request.client, resp)
+	if err != nil {
+		return 0, err // i.e. 'API quota exceeded'
+	}
+	if result == 0 {
+		return jitterBackoff(min, max, attempt), nil
+	}
+	if result < 0 || max < result {
+		result = max
+	}
+	if result < min {
+		result = min
+	}
+	return result, nil
+}
+
+// Return capped exponential backoff with jitter
+// http://www.awsarchitectureblog.com/2015/03/backoff.html
+func jitterBackoff(min, max time.Duration, attempt int) time.Duration {
 	base := float64(min)
 	capLevel := float64(max)
 
 	temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
-	ri := int64(temp / 2)
-	result := time.Duration(math.Abs(float64(ri + rand.Int63n(ri))))
+	ri := time.Duration(temp / 2)
+	result := randDuration(ri)
 
 	if result < min {
 		result = min
 	}
 
-	return result, nil
+	return result
+}
+
+var rnd = newRnd()
+var rndMu sync.Mutex
+
+func randDuration(center time.Duration) time.Duration {
+	rndMu.Lock()
+	defer rndMu.Unlock()
+
+	var ri = int64(center)
+	var jitter = rnd.Int63n(ri)
+	return time.Duration(math.Abs(float64(ri + jitter)))
+}
+
+func newRnd() *rand.Rand {
+	var seed = time.Now().UnixNano()
+	var src = rand.NewSource(seed)
+	return rand.New(src)
 }


Test patch:
diff --git a/client_test.go b/client_test.go
index 3c9ab76..46fded1 100644
--- a/client_test.go
+++ b/client_test.go
@@ -352,6 +352,15 @@ func TestClientOptions(t *testing.T) {
 	client.SetRetryMaxWaitTime(mrwt)
 	assertEqual(t, mrwt, client.RetryMaxWaitTime)
 
+	client.AddRetryAfterErrorCondition()
+	equal(client.RetryConditions[0], func(response *Response, err error) bool {
+		if response.IsError() {
+			return true
+		}
+
+		return false
+	})
+
 	err := &AuthError{}
 	client.SetError(err)
 	if reflect.TypeOf(err) == client.Error {
diff --git a/retry_test.go b/retry_test.go
index cfd7b7a..19d274d 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -626,6 +626,31 @@ func TestClientRetryCount(t *testing.T) {
 		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
 }
 
+func TestClientErrorRetry(t *testing.T) {
+	ts := createGetServer(t)
+	defer ts.Close()
+
+	c := dc().
+		SetTimeout(time.Second * 3).
+		SetRetryCount(1).
+		AddRetryAfterErrorCondition()
+
+	resp, err := c.R().
+		SetHeader(hdrContentTypeKey, "application/json; charset=utf-8").
+		SetJSONEscapeHTML(false).
+		SetResult(AuthSuccess{}).
+		Get(ts.URL + "/set-retry-error-recover")
+
+	assertError(t, err)
+
+	authSuccess := resp.Result().(*AuthSuccess)
+
+	assertEqual(t, http.StatusOK, resp.StatusCode())
+	assertEqual(t, "hello", authSuccess.Message)
+
+	assertNil(t, resp.Error())
+}
+
 func filler(*Response, error) bool {
 	return false
 }