Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to override default retry wait times for Client #66

Merged
merged 3 commits into from
Apr 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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