Skip to content

Commit 4ab111a

Browse files
authored
client -> retries (#4)
1 parent 51ae474 commit 4ab111a

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

jira.go

+86-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"encoding/json"
99
"fmt"
1010
"io"
11+
"log"
1112
"net/http"
1213
"net/url"
1314
"reflect"
1415
"sort"
16+
"strconv"
1517
"strings"
1618
"time"
1719

@@ -20,6 +22,27 @@ import (
2022
"github.com/pkg/errors"
2123
)
2224

25+
const (
26+
retryIntervalDefault = 30 * time.Second
27+
retryStepSeconds = 1
28+
)
29+
30+
var retryStatusCodes = map[int]struct{}{
31+
http.StatusTooManyRequests: {},
32+
}
33+
34+
type failFastKey struct{}
35+
36+
// WithFailFast disables retries on rate limit.
37+
func WithFailFast(ctx context.Context) context.Context {
38+
return context.WithValue(ctx, failFastKey{}, true)
39+
}
40+
41+
func failFast(ctx context.Context) bool {
42+
ok, _ := ctx.Value(failFastKey{}).(bool)
43+
return ok
44+
}
45+
2346
// httpClient defines an interface for an http.Client implementation so that alternative
2447
// http Clients can be passed in for making requests
2548
type httpClient interface {
@@ -267,9 +290,71 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
267290
return c.NewMultiPartRequestWithContext(context.Background(), method, urlStr, buf)
268291
}
269292

293+
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
294+
return c.doWithDeadline(req, v)
295+
}
296+
297+
func (c *Client) doWithDeadline(req *http.Request, v interface{}) (*Response, error) {
298+
ctx := req.Context()
299+
if failFast(ctx) {
300+
return c.do(req, v)
301+
}
302+
303+
deadline, deadlineIsSet := ctx.Deadline()
304+
if !deadlineIsSet {
305+
deadline = time.Now().Add(retryIntervalDefault)
306+
dctx, cancel := context.WithDeadline(ctx, deadline)
307+
defer cancel()
308+
ctx = dctx
309+
req = req.WithContext(ctx)
310+
}
311+
errDeadlineWrapped := func(resp *Response, err error) error {
312+
return errors.Wrapf(context.DeadlineExceeded, "path: %s, err: %+v, code: %d", req.URL.String(), err, resp.StatusCode)
313+
}
314+
315+
for {
316+
select {
317+
case <-ctx.Done():
318+
return nil, ctx.Err()
319+
default:
320+
}
321+
322+
resp, err := c.do(req, v)
323+
if resp != nil {
324+
if _, ok := retryStatusCodes[resp.StatusCode]; !ok {
325+
return resp, err
326+
}
327+
retrySeconds := retryStepSeconds
328+
retryAfter := resp.Header.Get("retry-after")
329+
if retryAfter != "" {
330+
retryAfterSeconds, errConv := strconv.Atoi(retryAfter)
331+
if errConv != nil {
332+
log.Println("failed to parse retry-after:", err)
333+
}
334+
if retryAfterSeconds != 0 {
335+
retrySeconds = retryAfterSeconds
336+
}
337+
}
338+
retryAfterStep := time.Duration(retrySeconds) * time.Second
339+
t := time.NewTimer(retryAfterStep)
340+
select {
341+
case <-ctx.Done():
342+
t.Stop()
343+
return nil, errDeadlineWrapped(resp, err)
344+
case <-t.C:
345+
}
346+
continue
347+
} else if err != nil {
348+
return nil, err
349+
} else {
350+
return resp, err
351+
}
352+
}
353+
}
354+
270355
// Do sends an API request and returns the API response.
271356
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
272-
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
357+
func (c *Client) do(req *http.Request, v interface{}) (*Response, error) {
273358
httpResp, err := c.client.Do(req)
274359
if err != nil {
275360
return nil, err

0 commit comments

Comments
 (0)