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

Conditional retry #37

Closed
wants to merge 5 commits into from
Closed
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
38 changes: 27 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,19 @@ 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
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 []func(*Response) (bool, error)

httpClient *http.Client
transport *http.Transport
Expand Down Expand Up @@ -339,6 +341,20 @@ func (c *Client) SetDebug(d bool) *Client {
return c
}

// SetRetryCount method enables retry on `go-resty` client. Uses a Backoff mechanism.
func (c *Client) SetRetryCount(count int) *Client {
c.RetryCount = count
return c
}

// AddRetryCondition adds a function to the array of functions that are checked
// to determine if the request is retried. The request will retry if any of the
// functions return true.
func (c *Client) AddRetryCondition(m func(*Response) (bool, error)) *Client {
c.RetryConditions = append(c.RetryConditions, m)
return c
}

// SetDisableWarn method disables the warning message on `go-resty` client.
// For example: go-resty warns the user when BasicAuth used on HTTP mode.
// resty.SetDisableWarn(true)
Expand Down
11 changes: 11 additions & 0 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func New() *Client {
httpClient: &http.Client{Jar: cookieJar},
transport: &http.Transport{},
mutex: &sync.Mutex{},
RetryCount: 0,
}

// Default redirect policy
Expand Down Expand Up @@ -133,6 +134,16 @@ func SetDebug(d bool) *Client {
return DefaultClient.SetDebug(d)
}

// SetRetryCount method set the retry count. See `Client.SetRetryCount` for more information.
func SetRetryCount(count int) *Client {
return DefaultClient.SetRetryCount(count)
}

// AddRetryCondition method appends check function for retry. See `Client.AddRetryCondition` for more information.
func AddRetryCondition(m func(*Response) (bool, error)) *Client {
return DefaultClient.AddRetryCondition(m)
}

// SetDisableWarn method disables warning comes from `go-resty` client. See `Client.SetDisableWarn` for more information.
func SetDisableWarn(d bool) *Client {
return DefaultClient.SetDisableWarn(d)
Expand Down
19 changes: 18 additions & 1 deletion request.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,24 @@ func (r *Request) Execute(method, url string) (*Response, error) {
r.Method = method
r.URL = url

return r.client.execute(r)
if r.client.RetryCount == 0 {
return r.client.execute(r)
}

var resp *Response
var err error
attempt := 0
_ = Backoff(func() (*Response, error) {
attempt++
resp, err = r.client.execute(r)
if err != nil {
r.client.Log.Printf("ERROR [%v] Attempt [%v]", err, attempt)
}

return resp, err
}, Retries(r.client.RetryCount), RetryConditions(r.client.RetryConditions))

return resp, err
}

func (r *Request) fmtBodyString() (body string) {
Expand Down
28 changes: 28 additions & 0 deletions resty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,20 @@ func TestClientTimeout(t *testing.T) {
assertEqual(t, true, strings.Contains(err.Error(), "i/o timeout"))
}

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

c := dc()
c.SetHTTPMode().
SetTimeout(time.Duration(time.Second * 3)).
SetRetryCount(3)

_, err := c.R().Get(ts.URL + "/set-retrycount-test")

assertError(t, err)
}

func TestClientTimeoutInternalError(t *testing.T) {
c := dc()
c.SetHTTPMode()
Expand Down Expand Up @@ -1320,6 +1334,9 @@ func TestClientOptions(t *testing.T) {
SetDisableWarn(true)
assertEqual(t, DefaultClient.DisableWarn, true)

SetRetryCount(3)
assertEqual(t, 3, DefaultClient.RetryCount)

err := &AuthError{}
SetError(err)
if reflect.TypeOf(err) == DefaultClient.Error {
Expand Down Expand Up @@ -1361,20 +1378,31 @@ func getTestDataPath() string {
return pwd + "/test-data"
}

// Used for retry testing...
var attempt int

func createGetServer(t *testing.T) *httptest.Server {
ts := createTestServer(func(w http.ResponseWriter, r *http.Request) {
t.Logf("Method: %v", r.Method)
t.Logf("Path: %v", r.URL.Path)

if r.Method == GET {
if r.URL.Path == "/" {
w.Write([]byte("TestGet: text response"))
} else if r.URL.Path == "/mypage" {
w.WriteHeader(http.StatusBadRequest)
} else if r.URL.Path == "/mypage2" {
w.Write([]byte("TestGet: text response from mypage2"))
} else if r.URL.Path == "/set-retrycount-test" {
attempt++
if attempt != 3 {
time.Sleep(time.Second * 6)
}
w.Write([]byte("TestClientRetry page"))
} else if r.URL.Path == "/set-timeout-test" {
time.Sleep(time.Second * 6)
w.Write([]byte("TestClientTimeout page"))

} else if r.URL.Path == "/my-image.png" {
fileBytes, _ := ioutil.ReadFile(getTestDataPath() + "/test-img.png")
w.Header().Set("Content-Type", "image/png")
Expand Down
90 changes: 90 additions & 0 deletions retry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package resty

import (
"math"
"math/rand"
"time"
)

type function func() error

// Option ...
type Option func(*Options)

// Options ...
type Options struct {
maxRetries int
waitTime int
maxWaitTime int
retryConditions []func(*Response) (bool, error)
}

// Retries sets the max number of retries
func Retries(value int) Option {
return func(o *Options) {
o.maxRetries = value
}
}

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

// MaxWaitTime sets the max wait time to sleep between requests
func MaxWaitTime(value int) Option {
return func(o *Options) {
o.maxWaitTime = value
}
}

// RetryConditions sets the conditions that will be checked for retry.
func RetryConditions(conditions []func(*Response) (bool, error)) Option {
return func(o *Options) {
o.retryConditions = conditions
}
}

//Backoff retries with increasing timeout duration up until X amount of retries (Default is 3 attempts, Override with option Retries(n))
func Backoff(operation func() (*Response, error), options ...Option) error {
// Defaults
opts := Options{maxRetries: 3, waitTime: 100, maxWaitTime: 2000, retryConditions: []func(*Response) (bool, error){}}
for _, o := range options {
o(&opts)
}

var resp *Response
var err error
base := float64(opts.waitTime) // Time to wait between each attempt
capLevel := float64(opts.maxWaitTime) // Maximum amount of wait time for the retry
for attempt := 0; attempt < opts.maxRetries; attempt++ {
resp, err = operation()

var needsRetry bool
var conditionErr error
for _, condition := range opts.retryConditions {
needsRetry, conditionErr = condition(resp)
if needsRetry || conditionErr != nil {
break
}
}

// If the operation returned no error, there was no condition satisfied and
// there was no error caused by the conditional functions.
if err == nil && needsRetry == false && conditionErr == nil {
return nil
}
// Adding capped exponential backup with jitter
// 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(sleepTime) * time.Millisecond
time.Sleep(sleepDuration)
}

return err
}
Loading