Skip to content

Commit 08cdd4a

Browse files
committed
Allow the HTTP fetcher to retry failed requests
1 parent c1e016f commit 08cdd4a

4 files changed

Lines changed: 58 additions & 32 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ require (
1313
github.com/aws/aws-sdk-go-v2/service/s3 v1.81.0
1414
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.7
1515
github.com/aws/smithy-go v1.22.4
16+
github.com/cenkalti/backoff/v5 v5.0.2
17+
github.com/dustin/go-humanize v1.0.1
1618
github.com/gdamore/tcell/v2 v2.8.1
1719
github.com/go-sql-driver/mysql v1.9.3
1820
github.com/google/go-cmp v0.7.0
@@ -68,7 +70,6 @@ require (
6870
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
6971
github.com/dgraph-io/badger/v4 v4.7.0 // indirect
7072
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
71-
github.com/dustin/go-humanize v1.0.1 // indirect
7273
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
7374
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
7475
github.com/felixge/httpsnoop v1.0.4 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,8 @@ github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
692692
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
693693
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
694694
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
695+
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
696+
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
695697
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
696698
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
697699
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=

internal/client/fetcher.go

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"net/url"
2323
"os"
2424
"path"
25+
"strconv"
2526
"strings"
2627

28+
"github.com/cenkalti/backoff/v5"
2729
"github.com/transparency-dev/tessera/api/layout"
2830
"k8s.io/klog/v2"
2931
)
@@ -43,6 +45,7 @@ func NewHTTPFetcher(rootURL *url.URL, c *http.Client) (*HTTPFetcher, error) {
4345
return &HTTPFetcher{
4446
c: c,
4547
rootURL: rootURL,
48+
backOff: []backoff.RetryOption{backoff.WithMaxTries(1)},
4649
}, nil
4750
}
4851

@@ -51,6 +54,7 @@ type HTTPFetcher struct {
5154
c *http.Client
5255
rootURL *url.URL
5356
authHeader string
57+
backOff []backoff.RetryOption
5458
}
5559

5660
// SetAuthorizationHeader sets the value to be used with an Authorization: header
@@ -59,39 +63,57 @@ func (h *HTTPFetcher) SetAuthorizationHeader(v string) {
5963
h.authHeader = v
6064
}
6165

62-
func (h HTTPFetcher) fetch(ctx context.Context, p string) ([]byte, error) {
63-
u, err := h.rootURL.Parse(p)
64-
if err != nil {
65-
return nil, fmt.Errorf("invalid URL: %v", err)
66-
}
67-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
68-
if err != nil {
69-
return nil, fmt.Errorf("NewRequestWithContext(%q): %v", u.String(), err)
70-
}
71-
if h.authHeader != "" {
72-
req.Header.Add("Authorization", h.authHeader)
73-
}
74-
r, err := h.c.Do(req)
75-
if err != nil {
76-
return nil, fmt.Errorf("get(%q): %v", u.String(), err)
77-
}
78-
switch r.StatusCode {
79-
case http.StatusOK:
80-
// All good, continue below
81-
break
82-
case http.StatusNotFound:
83-
// Need to return ErrNotExist here, by contract.
84-
return nil, fmt.Errorf("get(%q): %w", u.String(), os.ErrNotExist)
85-
default:
86-
return nil, fmt.Errorf("get(%q): %v", u.String(), r.StatusCode)
87-
}
66+
// EnableRetries causes requests which result in a non-permanent error to be retried with up to maxRetries attempts.
67+
func (h *HTTPFetcher) EnableRetries(maxRetries uint) {
68+
h.backOff = []backoff.RetryOption{backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(10)}
69+
}
8870

89-
defer func() {
90-
if err := r.Body.Close(); err != nil {
91-
klog.Errorf("resp.Body.Close(): %v", err)
71+
func (h HTTPFetcher) fetch(ctx context.Context, p string) ([]byte, error) {
72+
return backoff.Retry(ctx, func() ([]byte, error) {
73+
u, err := h.rootURL.Parse(p)
74+
if err != nil {
75+
return nil, fmt.Errorf("invalid URL: %v", err)
76+
}
77+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
78+
if err != nil {
79+
return nil, fmt.Errorf("NewRequestWithContext(%q): %v", u.String(), err)
80+
}
81+
if h.authHeader != "" {
82+
req.Header.Add("Authorization", h.authHeader)
9283
}
93-
}()
94-
return io.ReadAll(r.Body)
84+
r, err := h.c.Do(req)
85+
if err != nil {
86+
return nil, fmt.Errorf("get(%q): %v", u.String(), err)
87+
}
88+
switch r.StatusCode {
89+
case http.StatusOK:
90+
// All good, continue below
91+
break
92+
case http.StatusTooManyRequests:
93+
seconds, err := strconv.ParseInt(r.Header.Get("Retry-After"), 10, 64)
94+
if err != nil {
95+
// The server didn't say how long to wait, so we'll wait an arbitrary amount of time.
96+
seconds = 10
97+
}
98+
return nil, backoff.RetryAfter(int(seconds))
99+
case http.StatusNotFound:
100+
// Need to return ErrNotExist here, by contract, and also let the backoff know not to retry).
101+
return nil, backoff.Permanent(fmt.Errorf("get(%q): %w", u.String(), os.ErrNotExist))
102+
case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusMethodNotAllowed, http.StatusConflict, http.StatusUnprocessableEntity:
103+
// Should not retry for any of these status codes.
104+
return nil, backoff.Permanent(fmt.Errorf("get(%q): %v", u.String(), r.StatusCode))
105+
default:
106+
// Everything else will be retried
107+
return nil, fmt.Errorf("get(%q): %v", u.String(), r.StatusCode)
108+
}
109+
110+
defer func() {
111+
if err := r.Body.Close(); err != nil {
112+
klog.Errorf("resp.Body.Close(): %v", err)
113+
}
114+
}()
115+
return io.ReadAll(r.Body)
116+
}, h.backOff...)
95117
}
96118

97119
func (h HTTPFetcher) ReadCheckpoint(ctx context.Context) ([]byte, error) {

internal/hammer/hammer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ func mustCreateReaders(ctx context.Context, us []string) loadtest.LogReader {
426426
if err != nil {
427427
klog.Exitf("Failed to create HTTP fetcher for %q: %v", u, err)
428428
}
429+
c.EnableRetries(5)
429430
if *bearerToken != "" {
430431
c.SetAuthorizationHeader(fmt.Sprintf("Bearer %s", *bearerToken))
431432
}

0 commit comments

Comments
 (0)