Skip to content

Commit 818ea5c

Browse files
committed
Fix
1 parent eaf6986 commit 818ea5c

3 files changed

Lines changed: 16 additions & 269 deletions

File tree

internal/core/client.go

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package core
22

33
import (
4-
"context"
54
"fmt"
65
"net/http"
76
"strings"
8-
"time"
97

10-
"github.com/hashicorp/go-retryablehttp"
118
"github.com/scaleway/scaleway-sdk-go/logger"
129
"github.com/scaleway/scaleway-sdk-go/scw"
1310
"github.com/scaleway/scaleway-sdk-go/validation"
@@ -65,6 +62,9 @@ func createClient(buildInfo *BuildInfo, profileName string) (*scw.Client, error)
6562
scw.WithDefaultZone(scw.ZoneFrPar1),
6663
scw.WithUserAgent(buildInfo.GetUserAgent()),
6764
scw.WithProfile(profile),
65+
scw.WithHTTPClient(&http.Client{
66+
Transport: &retryableHTTPTransport{transport: http.DefaultTransport},
67+
}),
6868
}
6969

7070
client, err := scw.NewClient(opts...)
@@ -80,6 +80,9 @@ func createAnonymousClient(buildInfo *BuildInfo) (*scw.Client, error) {
8080
scw.WithDefaultRegion(scw.RegionFrPar),
8181
scw.WithDefaultZone(scw.ZoneFrPar1),
8282
scw.WithUserAgent(buildInfo.GetUserAgent()),
83+
scw.WithHTTPClient(&http.Client{
84+
Transport: &retryableHTTPTransport{transport: http.DefaultTransport},
85+
}),
8386
}
8487

8588
client, err := scw.NewClient(opts...)
@@ -205,21 +208,3 @@ func validateClient(client *scw.Client) error {
205208

206209
return nil
207210
}
208-
209-
// createRetryableHTTPClient creates a retryablehttp.Client.
210-
func createRetryableHTTPClient(shouldLog bool) *client {
211-
c := retryablehttp.NewClient()
212-
213-
c.RetryMax = 3
214-
c.RetryWaitMax = 2 * time.Minute
215-
c.Logger = l
216-
c.RetryWaitMin = time.Second * 2
217-
c.CheckRetry = func(_ context.Context, resp *http.Response, err error) (bool, error) {
218-
if resp == nil || resp.StatusCode == http.StatusTooManyRequests {
219-
return true, err
220-
}
221-
return retryablehttp.DefaultRetryPolicy(context.TODO(), resp, err)
222-
}
223-
224-
return &client{c}
225-
}

internal/core/http_retry.go

Lines changed: 9 additions & 247 deletions
Original file line numberDiff line numberDiff line change
@@ -1,259 +1,21 @@
11
package core
22

33
import (
4-
"fmt"
5-
"io"
6-
"io/ioutil"
7-
"math"
84
"net/http"
95
"time"
10-
11-
"github.com/hashicorp/go-cleanhttp"
12-
"github.com/hashicorp/go-hclog"
13-
)
14-
15-
var (
16-
// Default retry configuration
17-
defaultRetryWaitMin = 1 * time.Second
18-
defaultRetryWaitMax = 30 * time.Second
19-
defaultRetryMax = 4
206
)
217

22-
// CheckForRetry specifies a policy for handling retries. It is called
23-
// following each request with the response and error values returned by
24-
// the http.Client. If it returns false, the Client stops retrying
25-
// and returns the response to the caller. If it returns an error ,
26-
// that error value is returned in lieu of the error from the request .
27-
type CheckForRetry func(resp *http.Response, err error) (bool, error)
28-
29-
// DefaultRetryPolicy provides a default callback for Client.CheckForRetry,
30-
// will retry on connection errors and server errors .
31-
func DefaultRetryPolicy(resp *http.Response, err error) (bool, error) {
32-
if err != nil {
33-
return true, err
34-
}
35-
// Check the response code. Here, we retry on 500—range responses to allow
36-
//the server time to recover
37-
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
38-
return true, nil
39-
}
40-
return false, nil
41-
}
42-
43-
// Backoff specifies a policy for how long to wait between retries.
44-
// It is called after a failing request to determine the amount of time
45-
// that should pass before trying again.
46-
type Backoff func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration
47-
48-
// DefaultBackoff provides a default callback for Client.Backoff which
49-
// will perform exponential backoff based on the attempt number and limited
50-
// by the provided minimum and maximum durations.
51-
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
52-
mult := math.Pow(2, float64(attemptNum)) * float64(min)
53-
sleep := time.Duration(mult)
54-
if float64(sleep) != mult || sleep > max {
55-
sleep = max
56-
}
57-
return sleep
58-
}
59-
60-
// Client is used to make HTTP requests. It adds additional functionality
61-
// like automatic retries to tolerate minor outages.
62-
type Client struct {
63-
HTTPClient *http.Client // Internal HTTP client.
64-
RetryWaitMin time.Duration // Minimum time to wait
65-
RetryWaitMax time.Duration // Maximum time to wait
66-
RetryMax int // Maximum number of retries
67-
68-
// CheckRetry specifies the policy for handling retries, and is called
69-
// after each request. The default policy is DefaultRetryPolicy.
70-
CheckForRetry CheckForRetry
71-
72-
// Backoff specifies the policy for how long to wait between retries
73-
Backoff Backoff
74-
}
75-
76-
func NewClient() *Client {
77-
return &Client{
78-
HTTPClient: cleanhttp.DefaultClient(),
79-
RetryWaitMin: defaultRetryWaitMin,
80-
RetryWaitMax: defaultRetryWaitMax,
81-
RetryMax: defaultRetryMax,
82-
CheckForRetry: DefaultRetryPolicy,
83-
Backoff: DefaultBackoff,
84-
}
85-
}
86-
87-
// Request wraps the metadata needed to create HTTP requests.
88-
type Request struct {
89-
// body is a seekable reader over the request body payload. This is
90-
// used to rewind the request data in between retries.
91-
body io.ReadSeeker
92-
93-
// Embed an HTTP request directly. This makes a *Request act exactly
94-
// like an *http.Request so that all meta methods are supported.
95-
*http.Request
96-
}
97-
98-
// Try to read the response body so we can reuse this connection.
99-
func (c *Client) drainBody(body io.ReadCloser) {
100-
defer body.Close()
101-
_, err := io.Copy(ioutil.Discard, io.LimitReader(body, respReadLimit))
102-
if err != nil {
103-
fmt.Printf("[ERR] error reading response body: %v", err)
104-
}
105-
}
106-
107-
// Get is a convenience helper for doing simple GET requests.
108-
func (c *Client) Get(url string) (*http.Response, error) {
109-
req, err := NewRequest("GET", url, nil)
110-
if err != nil {
111-
return nil, err
112-
}
113-
return c.Do(req)
114-
}
8+
const defaultRetryInterval = 1 * time.Second
1159

116-
// Post is a convenience method for doing simple POST requests.
117-
func (c *Client) Post(url, bodyType string, body interface{}) (*http.Response, error) {
118-
req, err := NewRequest("POST", url, body)
119-
if err != nil {
120-
return nil, err
121-
}
122-
req.Header.Set("Content-Type", bodyType)
123-
return c.Do(req)
10+
type retryableHTTPTransport struct {
11+
transport http.RoundTripper
12412
}
12513

126-
// NewRequest creates a new wrapped request.
127-
func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
128-
// Wrap the body in a noop ReadCloser if non-nil. This prevents the
129-
// reader from being closed by the HTTP client.
130-
var rcBody io.ReadCloser
131-
if body != nil {
132-
rcBody = ioutil.NopCloser(body)
14+
func (r *retryableHTTPTransport) RoundTrip(request *http.Request) (*http.Response, error) {
15+
res, err := r.transport.RoundTrip(request)
16+
if err == nil && res.StatusCode == http.StatusTooManyRequests {
17+
time.Sleep(defaultRetryInterval)
18+
return r.RoundTrip(request)
13319
}
134-
135-
// Make the request with the noop-closer for the body.
136-
httpReq, err := http.NewRequest(method, url, rcBody)
137-
if err != nil {
138-
return nil, err
139-
}
140-
141-
return &Request{body, httpReq}, nil
142-
}
143-
144-
// Do wraps calling an HTTP method with retries.
145-
func (c *Client) Do(req *Request) (*http.Response, error) {
146-
if c.HTTPClient == nil {
147-
c.HTTPClient = cleanhttp.DefaultPooledClient()
148-
}
149-
150-
logger := c.logger()
151-
152-
if logger != nil {
153-
switch v := logger.(type) {
154-
case Logger:
155-
v.Printf("[DEBUG] %s %s", req.Method, req.URL)
156-
case hclog.Logger:
157-
v.Debug("performing request", "method", req.Method, "url", req.URL)
158-
}
159-
}
160-
161-
var resp *http.Response
162-
var err error
163-
164-
for i := 0; ; i++ {
165-
var code int // HTTP response code
166-
167-
// Always rewind the request body when non-nil.
168-
if req.body != nil {
169-
body, err := req.body()
170-
if err != nil {
171-
c.HTTPClient.CloseIdleConnections()
172-
return resp, err
173-
}
174-
if c, ok := body.(io.ReadCloser); ok {
175-
req.Body = c
176-
} else {
177-
req.Body = ioutil.NopCloser(body)
178-
}
179-
}
180-
181-
if c.RequestLogHook != nil && logger != nil {
182-
switch v := logger.(type) {
183-
case Logger:
184-
c.RequestLogHook(v, req.Request, i)
185-
case hclog.Logger:
186-
c.RequestLogHook(hookLogger{v}, req.Request, i)
187-
default:
188-
c.RequestLogHook(nil, req.Request, i)
189-
}
190-
}
191-
192-
// Attempt the request
193-
resp, err = c.HTTPClient.Do(req.Request)
194-
if resp != nil {
195-
code = resp.StatusCode
196-
}
197-
198-
// Check if we should continue with retries.
199-
checkOK, checkErr := c.CheckRetry(req.Context(), resp, err)
200-
201-
if logger != nil {
202-
if err != nil {
203-
switch v := logger.(type) {
204-
case Logger:
205-
v.Printf("[ERR] %s %s request failed: %v", req.Method, req.URL, err)
206-
case hclog.Logger:
207-
v.Error("request failed", "error", err, "method", req.Method, "url", req.URL)
208-
}
209-
} else {
210-
// Call this here to maintain the behavior of logging all requests,
211-
// even if CheckRetry signals to stop.
212-
if c.ResponseLogHook != nil {
213-
// Call the response logger function if provided.
214-
switch v := logger.(type) {
215-
case Logger:
216-
c.ResponseLogHook(v, resp)
217-
case hclog.Logger:
218-
c.ResponseLogHook(hookLogger{v}, resp)
219-
default:
220-
c.ResponseLogHook(nil, resp)
221-
}
222-
}
223-
}
224-
}
225-
226-
// Now decide if we should continue.
227-
if !checkOK {
228-
if checkErr != nil {
229-
err = checkErr
230-
}
231-
c.HTTPClient.CloseIdleConnections()
232-
return resp, err
233-
}
234-
235-
// We do this before drainBody beause there's no need for the I/O if
236-
// we're breaking out
237-
remain := c.RetryMax - i
238-
if remain <= 0 {
239-
break
240-
}
241-
242-
// We're going to retry, consume any response to reuse the connection.
243-
if err == nil && resp != nil {
244-
c.drainBody(resp.Body)
245-
}
246-
247-
wait := c.Backoff(c.RetryWaitMin, c.RetryWaitMax, i, resp)
248-
desc := fmt.Sprintf("%s %s", req.Method, req.URL)
249-
if code > 0 {
250-
desc = fmt.Sprintf("%s (status: %d)", desc, code)
251-
}
252-
fmt.Printf("[DEBUG] %s: retrying in %s (%d left)", desc, wait, remain)
253-
time.Sleep(wait)
254-
}
255-
256-
// Return an error if we fail out of the retry loop
257-
return nil, fmt.Errorf("%s %s giving up after %d attempts",
258-
req.Method, req.URL, c.RetryMax+1)
20+
return res, err
25921
}

internal/core/testing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ func getHTTPRecoder(t *testing.T, update bool) (client *http.Client, cleanup fun
615615
return nil
616616
})
617617

618-
return &http.Client{Transport: r}, func() {
618+
return &http.Client{Transport: &retryableHTTPTransport{transport: r}}, func() {
619619
assert.NoError(t, r.Stop()) // Make sure recorder is stopped once done with it
620620
}, nil
621621
}

0 commit comments

Comments
 (0)