update github.com/hashicorp/go-retryablehttp

This commit is contained in:
James Bardin 2016-08-10 13:33:42 -04:00
parent f3a06c0cb2
commit ee6159cd9d
4 changed files with 84 additions and 28 deletions

View File

@ -1,3 +0,0 @@
.idea/
*.iml
*.test

View File

@ -1,12 +0,0 @@
sudo: false
language: go
go:
- 1.5.1
branches:
only:
- master
script: make updatedeps test

View File

@ -38,6 +38,10 @@ var (
// defaultClient is used for performing requests without explicitly making // defaultClient is used for performing requests without explicitly making
// a new client. It is purposely private to avoid modifications. // a new client. It is purposely private to avoid modifications.
defaultClient = NewClient() defaultClient = NewClient()
// We need to consume response bodies to maintain http connections, but
// limit the size we consume to respReadLimit.
respReadLimit = int64(4096)
) )
// LenReader is an interface implemented by many in-memory io.Reader's. Used // LenReader is an interface implemented by many in-memory io.Reader's. Used
@ -86,6 +90,23 @@ func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
// consumers. // consumers.
type RequestLogHook func(*log.Logger, *http.Request, int) type RequestLogHook func(*log.Logger, *http.Request, int)
// ResponseLogHook is like RequestLogHook, but allows running a function
// on each HTTP response. This function will be invoked at the end of
// every HTTP request executed, regardless of whether a subsequent retry
// needs to be performed or not. If the response body is read or closed
// from this method, this will affect the response returned from Do().
type ResponseLogHook func(*log.Logger, *http.Response)
// CheckRetry specifies a policy for handling retries. It is called
// following each request with the response and error values returned by
// the http.Client. If CheckRetry returns false, the Client stops retrying
// and returns the response to the caller. If CheckRetry returns an error,
// that error value is returned in lieu of the error from the request. The
// Client will close any response body when retrying, but if the retry is
// aborted it is up to the CheckResponse callback to properly close any
// response body before returning.
type CheckRetry func(resp *http.Response, err error) (bool, error)
// Client is used to make HTTP requests. It adds additional functionality // Client is used to make HTTP requests. It adds additional functionality
// like automatic retries to tolerate minor outages. // like automatic retries to tolerate minor outages.
type Client struct { type Client struct {
@ -99,6 +120,14 @@ type Client struct {
// RequestLogHook allows a user-supplied function to be called // RequestLogHook allows a user-supplied function to be called
// before each retry. // before each retry.
RequestLogHook RequestLogHook RequestLogHook RequestLogHook
// ResponseLogHook allows a user-supplied function to be called
// with the response from each HTTP request executed.
ResponseLogHook ResponseLogHook
// CheckRetry specifies the policy for handling retries, and is called
// after each request. The default policy is DefaultRetryPolicy.
CheckRetry CheckRetry
} }
// NewClient creates a new Client with default settings. // NewClient creates a new Client with default settings.
@ -109,9 +138,27 @@ func NewClient() *Client {
RetryWaitMin: defaultRetryWaitMin, RetryWaitMin: defaultRetryWaitMin,
RetryWaitMax: defaultRetryWaitMax, RetryWaitMax: defaultRetryWaitMax,
RetryMax: defaultRetryMax, RetryMax: defaultRetryMax,
CheckRetry: DefaultRetryPolicy,
} }
} }
// DefaultRetryPolicy provides a default callback for Client.CheckRetry, which
// will retry on connection errors and server errors.
func DefaultRetryPolicy(resp *http.Response, err error) (bool, error) {
if err != nil {
return true, err
}
// Check the response code. We retry on 500-range responses to allow
// the server time to recover, as 500's are typically not permanent
// errors and may relate to outages on the server side. This will catch
// invalid response codes as well, like 0 and 999.
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
return true, nil
}
return false, nil
}
// Do wraps calling an HTTP method with retries. // Do wraps calling an HTTP method with retries.
func (c *Client) Do(req *Request) (*http.Response, error) { func (c *Client) Do(req *Request) (*http.Response, error) {
c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL) c.Logger.Printf("[DEBUG] %s %s", req.Method, req.URL)
@ -132,23 +179,36 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
// Attempt the request // Attempt the request
resp, err := c.HTTPClient.Do(req.Request) resp, err := c.HTTPClient.Do(req.Request)
// Check if we should continue with retries.
checkOK, checkErr := c.CheckRetry(resp, err)
if err != nil { if err != nil {
c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err) c.Logger.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
goto RETRY } else {
// Call this here to maintain the behavior of logging all requests,
// even if CheckRetry signals to stop.
if c.ResponseLogHook != nil {
// Call the response logger function if provided.
c.ResponseLogHook(c.Logger, resp)
} }
code = resp.StatusCode
// Check the response code. We retry on 500-range responses to allow
// the server time to recover, as 500's are typically not permanent
// errors and may relate to outages on the server side.
if code%500 < 100 {
resp.Body.Close()
goto RETRY
} }
return resp, nil
RETRY: // Now decide if we should continue.
if i == c.RetryMax { if !checkOK {
if checkErr != nil {
err = checkErr
}
return resp, err
}
// We're going to retry, consume any response to reuse the connection.
if err == nil {
c.drainBody(resp.Body)
}
remain := c.RetryMax - i
if remain == 0 {
break break
} }
wait := backoff(c.RetryWaitMin, c.RetryWaitMax, i) wait := backoff(c.RetryWaitMin, c.RetryWaitMax, i)
@ -156,7 +216,7 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
if code > 0 { if code > 0 {
desc = fmt.Sprintf("%s (status: %d)", desc, code) desc = fmt.Sprintf("%s (status: %d)", desc, code)
} }
c.Logger.Printf("[DEBUG] %s: retrying in %s", desc, wait) c.Logger.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
time.Sleep(wait) time.Sleep(wait)
} }
@ -165,6 +225,15 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
req.Method, req.URL, c.RetryMax+1) req.Method, req.URL, c.RetryMax+1)
} }
// Try to read the response body so we can reuse this connection.
func (c *Client) drainBody(body io.ReadCloser) {
defer body.Close()
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
if err != nil {
c.Logger.Printf("[ERR] error reading response body: %v", err)
}
}
// Get is a shortcut for doing a GET request without making a new client. // Get is a shortcut for doing a GET request without making a new client.
func Get(url string) (*http.Response, error) { func Get(url string) (*http.Response, error) {
return defaultClient.Get(url) return defaultClient.Get(url)

4
vendor/vendor.json vendored
View File

@ -1108,8 +1108,10 @@
"revision": "cccb4a1328abbb89898f3ecf4311a05bddc4de6d" "revision": "cccb4a1328abbb89898f3ecf4311a05bddc4de6d"
}, },
{ {
"checksumSHA1": "GBDE1KDl/7c5hlRPYRZ7+C0WQ0g=",
"path": "github.com/hashicorp/go-retryablehttp", "path": "github.com/hashicorp/go-retryablehttp",
"revision": "5ec125ef739293cb4d57c3456dd92ba9af29ed6e" "revision": "f4ed9b0fa01a2ac614afe7c897ed2e3d8208f3e8",
"revisionTime": "2016-08-10T17:22:55Z"
}, },
{ {
"path": "github.com/hashicorp/go-rootcerts", "path": "github.com/hashicorp/go-rootcerts",