Skip to content

Commit bb1217f

Browse files
authored
feat: Use retry http and add backoff (#151)
1 parent 3066337 commit bb1217f

4 files changed

Lines changed: 129 additions & 21 deletions

File tree

client/backoffs.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package client
2+
3+
import (
4+
"net/http"
5+
"regexp"
6+
"strconv"
7+
"time"
8+
9+
"github.com/hashicorp/go-retryablehttp"
10+
)
11+
12+
var (
13+
rateLimitResetHeaders = []string{
14+
"x-organization-rate-limit-reset",
15+
"x-instant-test-rate-limit-reset",
16+
}
17+
resetHeaderPattern = regexp.MustCompile(`^\s*[0-9]+\s*$`)
18+
timeNow = time.Now
19+
)
20+
21+
// Custom backoff function
22+
func thousandEyesBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
23+
if resp == nil {
24+
return 0
25+
}
26+
27+
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable {
28+
if sleep, ok := parseRetryAfterHeader(resp.Header["Retry-After"]); ok {
29+
return sleep
30+
}
31+
}
32+
33+
for _, header := range rateLimitResetHeaders {
34+
if resetTimeStr := resp.Header.Get(header); resetTimeStr != "" {
35+
if sleep, ok := parseResetHeader(&resetTimeStr); ok {
36+
return sleep
37+
}
38+
39+
// Default backoff if no valid reset time is found
40+
return retryablehttp.DefaultBackoff(min, max, attemptNum, resp)
41+
}
42+
}
43+
44+
// Default case for no Retry-After and rateLimitResetHeaders
45+
return retryablehttp.DefaultBackoff(min, max, attemptNum, resp)
46+
}
47+
48+
func parseRetryAfterHeader(headers []string) (time.Duration, bool) {
49+
if len(headers) == 0 || headers[0] == "" {
50+
return 0, false
51+
}
52+
header := headers[0]
53+
// Retry-After: 120
54+
if sleep, err := strconv.ParseInt(header, 10, 64); err == nil {
55+
if sleep < 0 { // a negative sleep doesn't make sense
56+
return 0, false
57+
}
58+
return time.Second * time.Duration(sleep), true
59+
}
60+
61+
// Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
62+
retryTime, err := time.Parse(time.RFC1123, header)
63+
if err != nil {
64+
return 0, false
65+
}
66+
if until := retryTime.Sub(timeNow()); until > 0 {
67+
return until, true
68+
}
69+
// date is in the past
70+
return 0, true
71+
}
72+
73+
// Function to parse the reset header
74+
func parseResetHeader(value *string) (time.Duration, bool) {
75+
if value == nil || !resetHeaderPattern.MatchString(*value) {
76+
return 0, false
77+
}
78+
79+
// Parse the header value to a Unix timestamp
80+
resetTimeUnix, err := strconv.ParseInt(*value, 10, 64)
81+
if err != nil {
82+
return 0, false
83+
}
84+
85+
// Calculate the duration until the reset time
86+
resetTime := time.Unix(resetTimeUnix, 0)
87+
waitDuration := time.Until(resetTime)
88+
89+
// Return 0 if the reset time has already passed
90+
if waitDuration < 0 {
91+
return 0, true
92+
}
93+
return waitDuration, true
94+
}

client/client.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"reflect"
1515
"regexp"
1616
"strings"
17+
18+
"github.com/hashicorp/go-retryablehttp"
1719
)
1820

1921
var (
@@ -32,12 +34,23 @@ type Service struct {
3234
Client *APIClient
3335
}
3436

37+
func NewRetryAPIClient(httpClient *http.Client) *http.Client {
38+
retryClient := retryablehttp.NewClient()
39+
retryClient.CheckRetry = retryablehttp.DefaultRetryPolicy
40+
retryClient.Backoff = thousandEyesBackoff
41+
42+
if httpClient != nil {
43+
retryClient.HTTPClient = httpClient
44+
}
45+
46+
// Return the retry-enabled client
47+
return retryClient.StandardClient()
48+
}
49+
3550
// NewAPIClient creates a new API client.
3651
// optionally a custom http.Client to allow for advanced features such as caching.
3752
func NewAPIClient(cfg *Configuration) *APIClient {
38-
if cfg.HTTPClient == nil {
39-
cfg.HTTPClient = http.DefaultClient
40-
}
53+
cfg.HTTPClient = NewRetryAPIClient(cfg.HTTPClient)
4154

4255
c := new(APIClient)
4356
c.cfg = cfg

go.mod

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
module github.com/thousandeyes/thousandeyes-sdk-go/v3
22

3-
go 1.18
3+
go 1.23
44

5-
require github.com/stretchr/testify v1.7.2
5+
toolchain go1.24.8
66

7-
require (
8-
github.com/davecgh/go-spew v1.1.0 // indirect
9-
github.com/pmezard/go-difflib v1.0.0 // indirect
10-
gopkg.in/yaml.v3 v3.0.1 // indirect
11-
)
7+
require github.com/hashicorp/go-retryablehttp v0.7.8
8+
9+
require github.com/hashicorp/go-cleanhttp v0.5.2 // indirect

go.sum

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6-
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
7-
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
8-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
9-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
11-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
2+
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
3+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
4+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
5+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
6+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
7+
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
8+
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
9+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
10+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
11+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
12+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
13+
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
14+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

0 commit comments

Comments
 (0)