@@ -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 , 32 )
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
97119func (h HTTPFetcher ) ReadCheckpoint (ctx context.Context ) ([]byte , error ) {
0 commit comments