github.com/xmidt-org/webpa-common@v1.11.9/xhttp/retry.go (about) 1 package xhttp 2 3 import ( 4 "net/http" 5 "time" 6 7 "github.com/go-kit/kit/log" 8 "github.com/go-kit/kit/log/level" 9 "github.com/go-kit/kit/metrics" 10 "github.com/go-kit/kit/metrics/discard" 11 "github.com/xmidt-org/webpa-common/logging" 12 ) 13 14 const DefaultRetryInterval = time.Second 15 16 // temporaryError is the expected interface for a (possibly) temporary error. 17 // Several of the error types in the net package implicitely implement this interface, 18 // for example net.DNSError. 19 type temporaryError interface { 20 Temporary() bool 21 } 22 23 // ShouldRetryFunc is a predicate for determining if the error returned from an HTTP transaction 24 // should be retried. 25 type ShouldRetryFunc func(error) bool 26 27 // ShouldRetryStatusFunc is a predicate for determining if the status coded returned from an HTTP transaction 28 // should be retried. 29 type ShouldRetryStatusFunc func(int) bool 30 31 // DefaultShouldRetry is the default retry predicate. It returns true if and only if err exposes a Temporary() bool 32 // method and that method returns true. That means, for example, that for a net.DNSError with the temporary flag set to true 33 // this predicate also returns true. 34 func DefaultShouldRetry(err error) bool { 35 if temp, ok := err.(temporaryError); ok { 36 return temp.Temporary() 37 } 38 39 return false 40 } 41 42 // DefaultShouldRetryStatus is the default retry predicate. It returns false on all status codes 43 // aka. it will never retry 44 func DefaultShouldRetryStatus(status int) bool { 45 return false 46 } 47 48 // RetryOptions are the configuration options for a retry transactor 49 type RetryOptions struct { 50 // Logger is the go-kit logger to use. Defaults to logging.DefaultLogger() if unset. 51 Logger log.Logger 52 53 // Retries is the count of retries. If not positive, then no transactor decoration is performed. 54 Retries int 55 56 // Interval is the time between retries. If not set, DefaultRetryInterval is used. 57 Interval time.Duration 58 59 // Sleep is function used to wait out a duration. If unset, time.Sleep is used. 60 Sleep func(time.Duration) 61 62 // ShouldRetry is the retry predicate. Defaults to DefaultShouldRetry if unset. 63 ShouldRetry ShouldRetryFunc 64 65 // ShouldRetryStatus is the retry predicate. Defaults to DefaultShouldRetry if unset. 66 ShouldRetryStatus ShouldRetryStatusFunc 67 68 // Counter is the counter for total retries. If unset, no metrics are collected on retries. 69 Counter metrics.Counter 70 71 // UpdateRequest provides the ability to update the request before it is sent. default is noop 72 UpdateRequest func(*http.Request) 73 } 74 75 // RetryTransactor returns an HTTP transactor function, of the same signature as http.Client.Do, that 76 // retries a certain number of times. Note that net/http.RoundTripper.RoundTrip also is of this signature, 77 // so this decorator can be used with a RoundTripper or an http.Client equally well. 78 // 79 // If o.Retries is nonpositive, next is returned undecorated. 80 func RetryTransactor(o RetryOptions, next func(*http.Request) (*http.Response, error)) func(*http.Request) (*http.Response, error) { 81 if o.Retries < 1 { 82 return next 83 } 84 85 if o.Logger == nil { 86 o.Logger = logging.DefaultLogger() 87 } 88 89 if o.Counter == nil { 90 o.Counter = discard.NewCounter() 91 } 92 93 if o.ShouldRetry == nil { 94 o.ShouldRetry = DefaultShouldRetry 95 } 96 97 if o.ShouldRetryStatus == nil { 98 o.ShouldRetryStatus = DefaultShouldRetryStatus 99 } 100 101 if o.UpdateRequest == nil { 102 //noop 103 o.UpdateRequest = func(*http.Request) {} 104 } 105 106 if o.Interval < 1 { 107 o.Interval = DefaultRetryInterval 108 } 109 110 if o.Sleep == nil { 111 o.Sleep = time.Sleep 112 } 113 114 return func(request *http.Request) (*http.Response, error) { 115 if err := EnsureRewindable(request); err != nil { 116 return nil, err 117 } 118 var statusCode int 119 120 // initial attempt: 121 response, err := next(request) 122 if response != nil { 123 statusCode = response.StatusCode 124 } 125 126 for r := 0; r < o.Retries && ((err != nil && o.ShouldRetry(err)) || o.ShouldRetryStatus(statusCode)); r++ { 127 o.Counter.Add(1.0) 128 o.Sleep(o.Interval) 129 o.Logger.Log(level.Key(), level.DebugValue(), logging.MessageKey(), "retrying HTTP transaction", "url", request.URL.String(), logging.ErrorKey(), err, "retry", r+1, "statusCode", statusCode) 130 131 if err := Rewind(request); err != nil { 132 return nil, err 133 } 134 135 o.UpdateRequest(request) 136 response, err = next(request) 137 if response != nil { 138 statusCode = response.StatusCode 139 } 140 } 141 142 if err != nil { 143 o.Logger.Log(level.Key(), level.DebugValue(), logging.MessageKey(), "All HTTP transaction retries failed", "url", request.URL.String(), logging.ErrorKey(), err, "retries", o.Retries) 144 } 145 146 return response, err 147 } 148 }