Skip to content

Commit

Permalink
Allow to override default retry wait times for Client (#66)
Browse files Browse the repository at this point in the history
* Added Client's retry wait times config methods
* Make sure Backoff always sleeps at least WaitTime
* Document retries usage
  • Loading branch information
bak1an authored and jeevatkm committed Apr 13, 2017
1 parent c45c7bc commit 97effac
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 52 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ resty tested with Go `v1.2` and above.
* FlexibleRedirectPolicy
* DomainCheckRedirectPolicy
* etc. [more info](redirect.go)
* Retry Mechanism [how to use](retry_test.go)
* Retry Mechanism [how to use](#retries)
* Backoff Retry
* Conditional Retry
* SRV Record based request instead of Host URL [how to use](resty_test.go#L1412)
Expand Down Expand Up @@ -448,6 +448,50 @@ resp, err := c.R().
Get("http://httpbin.org/get")
```

#### Retries

Resty uses [backoff](http://www.awsarchitectureblog.com/2015/03/backoff.html)
to increase retry intervals after each attempt.

Usage example:
```go
// Retries are configured per client
resty.DefaultClient.
// Set retry count to non zero to enable retries
SetRetryCount(3).
// You can override initial retry wait time.
// Default is 100 milliseconds.
SetRetryWaitTime(5 * time.Second).
// MaxWaitTime can be overridden as well.
// Default is 2 seconds.
SetRetryMaxWaitTime(20 * time.Second)
```

Above setup will result in resty retrying requests returned non nil error up to
3 times with delay increased after each attempt.

You can optionally provide client with custom retry conditions:

```go
resty.DefaultClient.
AddRetryCondition(
// Condition function will be provided with *resty.Response as a
// parameter. It is expected to return (bool, error) pair. Resty will retry
// in case condition returns true or non nil error.
func(r *resty.Response) (bool, error) {
return r.StatusCode() == http.StatusTooManyRequests, nil
}
)
```

Above example will make resty retry requests ended with `429 Too Many Requests`
status code.

Multiple retry conditions can be added.

It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios
implemented. [Reference](retry_test.go).

#### Choose REST or HTTP mode
```go
// REST mode. This is Default.
Expand Down
44 changes: 31 additions & 13 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,21 @@ var (
// Client type is used for HTTP/RESTful global values
// for all request raised from the client
type Client struct {
HostURL string
QueryParam url.Values
FormData url.Values
Header http.Header
UserInfo *User
Token string
Cookies []*http.Cookie
Error reflect.Type
Debug bool
DisableWarn bool
Log *log.Logger
RetryCount int
RetryConditions []RetryConditionFunc
HostURL string
QueryParam url.Values
FormData url.Values
Header http.Header
UserInfo *User
Token string
Cookies []*http.Cookie
Error reflect.Type
Debug bool
DisableWarn bool
Log *log.Logger
RetryCount int
RetryWaitTime time.Duration
RetryMaxWaitTime time.Duration
RetryConditions []RetryConditionFunc

httpClient *http.Client
transport *http.Transport
Expand Down Expand Up @@ -414,6 +416,22 @@ func (c *Client) SetRetryCount(count int) *Client {
return c
}

// SetRetryWaitTime method sets default wait time to sleep before retrying
// request.
// Default is 100 milliseconds.
func (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client {
c.RetryWaitTime = waitTime
return c
}

// SetRetryMaxWaitTime method sets max wait time to sleep before retrying
// request.
// Default is 2 seconds.
func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {
c.RetryMaxWaitTime = maxWaitTime
return c
}

// AddRetryCondition method adds a retry condition function to array of functions
// that are checked to determine if the request is retried. The request will
// retry if any of the functions return true and error is nil.
Expand Down
26 changes: 14 additions & 12 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ func New() *Client {
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})

c := &Client{
HostURL: "",
QueryParam: url.Values{},
FormData: url.Values{},
Header: http.Header{},
UserInfo: nil,
Token: "",
Cookies: make([]*http.Cookie, 0),
Debug: false,
Log: getLogger(os.Stderr),
RetryCount: 0,
httpClient: &http.Client{Jar: cookieJar},
transport: &http.Transport{},
HostURL: "",
QueryParam: url.Values{},
FormData: url.Values{},
Header: http.Header{},
UserInfo: nil,
Token: "",
Cookies: make([]*http.Cookie, 0),
Debug: false,
Log: getLogger(os.Stderr),
RetryCount: 0,
RetryWaitTime: defaultWaitTime,
RetryMaxWaitTime: defaultMaxWaitTime,
httpClient: &http.Client{Jar: cookieJar},
transport: &http.Transport{},
}

c.httpClient.Transport = c.transport
Expand Down
36 changes: 21 additions & 15 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,23 +411,29 @@ func (r *Request) Execute(method, url string) (*Response, error) {

var resp *Response
attempt := 0
_ = Backoff(func() (*Response, error) {
attempt++

r.URL = r.selectAddr(addrs, url, attempt)

resp, err = r.client.execute(r)
if err != nil {
r.client.Log.Printf("ERROR [%v] Attempt [%v]", err, attempt)
if r.isContextCancelledIfAvailable() {
// stop Backoff from retrying request if request has been
// canceled by context
return resp, nil
_ = Backoff(
func() (*Response, error) {
attempt++

r.URL = r.selectAddr(addrs, url, attempt)

resp, err = r.client.execute(r)
if err != nil {
r.client.Log.Printf("ERROR [%v] Attempt [%v]", err, attempt)
if r.isContextCancelledIfAvailable() {
// stop Backoff from retrying request if request has been
// canceled by context
return resp, nil
}
}
}

return resp, err
}, Retries(r.client.RetryCount), RetryConditions(r.client.RetryConditions))
return resp, err
},
Retries(r.client.RetryCount),
WaitTime(r.client.RetryWaitTime),
MaxWaitTime(r.client.RetryMaxWaitTime),
RetryConditions(r.client.RetryConditions),
)

return resp, err
}
Expand Down
19 changes: 16 additions & 3 deletions resty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1163,10 +1163,10 @@ func getTestDataPath() string {
return pwd + "/test-data"
}

var attempt int32
var sequence int32

func createGetServer(t *testing.T) *httptest.Server {
var attempt int32
var sequence int32
var lastRequest time.Time
ts := createTestServer(func(w http.ResponseWriter, r *http.Request) {
t.Logf("Method: %v", r.Method)
t.Logf("Path: %v", r.URL.Path)
Expand All @@ -1184,6 +1184,19 @@ func createGetServer(t *testing.T) *httptest.Server {
time.Sleep(time.Second * 6)
}
_, _ = w.Write([]byte("TestClientRetry page"))
} else if r.URL.Path == "/set-retrywaittime-test" {
// Returns time.Duration since last request here
// or 0 for the very first request
if atomic.LoadInt32(&attempt) == 0 {
lastRequest = time.Now()
_, _ = fmt.Fprint(w, "0")
} else {
now := time.Now()
sinceLastRequest := now.Sub(lastRequest)
lastRequest = now
_, _ = fmt.Fprintf(w, "%d", uint64(sinceLastRequest))
}
atomic.AddInt32(&attempt, 1)
} else if r.URL.Path == "/set-timeout-test-with-sequence" {
seq := atomic.AddInt32(&sequence, 1)
time.Sleep(time.Second * 2)
Expand Down
18 changes: 10 additions & 8 deletions retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (

const (
defaultMaxRetries = 3
defaultWaitTime = 100 // base Milliseconds
defaultMaxWaitTime = 2000 // cap level Milliseconds
defaultWaitTime = time.Duration(100) * time.Millisecond
defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
)

// Option is to create convenient retry options like wait time, max retries, etc.
Expand All @@ -25,8 +25,8 @@ type RetryConditionFunc func(*Response) (bool, error)
// Options to hold go-resty retry values
type Options struct {
maxRetries int
waitTime int
maxWaitTime int
waitTime time.Duration
maxWaitTime time.Duration
retryConditions []RetryConditionFunc
}

Expand All @@ -38,14 +38,14 @@ func Retries(value int) Option {
}

// WaitTime sets the default wait time to sleep between requests
func WaitTime(value int) Option {
func WaitTime(value time.Duration) Option {
return func(o *Options) {
o.waitTime = value
}
}

// MaxWaitTime sets the max wait time to sleep between requests
func MaxWaitTime(value int) Option {
func MaxWaitTime(value time.Duration) Option {
return func(o *Options) {
o.maxWaitTime = value
}
Expand Down Expand Up @@ -100,9 +100,11 @@ func Backoff(operation func() (*Response, error), options ...Option) error {
// See the following article...
// http://www.awsarchitectureblog.com/2015/03/backoff.html
temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
sleepTime := int(temp/2) + rand.Intn(int(temp/2))
sleepDuration := time.Duration(int(temp/2) + rand.Intn(int(temp/2)))

sleepDuration := time.Duration(sleepTime) * time.Millisecond
if sleepDuration < opts.waitTime {
sleepDuration = opts.waitTime
}
time.Sleep(sleepDuration)
}

Expand Down
44 changes: 44 additions & 0 deletions retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,50 @@ func TestClientRetryGet(t *testing.T) {
assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test"))
}

func TestClientRetryWait(t *testing.T) {
ts := createGetServer(t)
defer ts.Close()

attempt := 0

retryCount := 5
retryIntervals := make([]uint64, retryCount)

// Set retry wait times that do not intersect with default ones
retryWaitTime := time.Duration(3) * time.Second
retryMaxWaitTime := time.Duration(9) * time.Second

c := dc()
c.SetHTTPMode().
SetRetryCount(retryCount).
SetRetryWaitTime(retryWaitTime).
SetRetryMaxWaitTime(retryMaxWaitTime).
AddRetryCondition(
func(r *Response) (bool, error) {
timeSlept, _ := strconv.ParseUint(string(r.Body()), 10, 64)
retryIntervals[attempt] = timeSlept
attempt++
return true, nil
},
)
c.R().Get(ts.URL + "/set-retrywaittime-test")

// 5 attempts were made
assertEqual(t, attempt, 5)

// Initial attempt has 0 time slept since last request
assertEqual(t, retryIntervals[0], uint64(0))

for i := 1; i < len(retryIntervals); i++ {
slept := time.Duration(retryIntervals[i])
// Ensure that client has slept some duration between
// waitTime and maxWaitTime for consequent requests
if slept < retryWaitTime || slept > retryMaxWaitTime {
t.Errorf("Client has slept %f seconds before retry %d", slept.Seconds(), i)
}
}
}

func TestClientRetryPost(t *testing.T) {
ts := createPostServer(t)
defer ts.Close()
Expand Down

0 comments on commit 97effac

Please sign in to comment.