9
9
"fmt"
10
10
"io"
11
11
"io/ioutil"
12
+ "net"
12
13
"net/http"
13
14
"net/url"
14
15
"regexp"
@@ -26,6 +27,9 @@ const Version = version.Client
26
27
var (
27
28
userAgent string
28
29
reGoVersion = regexp .MustCompile (`go(\d+\.\d+\..+)` )
30
+
31
+ defaultMaxRetries = 3
32
+ defaultRetryOnStatus = [... ]int {502 , 503 , 504 }
29
33
)
30
34
31
35
func init () {
@@ -46,6 +50,12 @@ type Config struct {
46
50
Password string
47
51
APIKey string
48
52
53
+ RetryOnStatus []int
54
+ DisableRetry bool
55
+ EnableRetryOnTimeout bool
56
+ MaxRetries int
57
+ RetryBackoff func (attempt int ) time.Duration
58
+
49
59
Transport http.RoundTripper
50
60
Logger Logger
51
61
}
@@ -58,6 +68,12 @@ type Client struct {
58
68
password string
59
69
apikey string
60
70
71
+ retryOnStatus []int
72
+ disableRetry bool
73
+ enableRetryOnTimeout bool
74
+ maxRetries int
75
+ retryBackoff func (attempt int ) time.Duration
76
+
61
77
transport http.RoundTripper
62
78
selector Selector
63
79
logger Logger
@@ -72,12 +88,26 @@ func New(cfg Config) *Client {
72
88
cfg .Transport = http .DefaultTransport
73
89
}
74
90
91
+ if len (cfg .RetryOnStatus ) == 0 {
92
+ cfg .RetryOnStatus = defaultRetryOnStatus [:]
93
+ }
94
+
95
+ if cfg .MaxRetries == 0 {
96
+ cfg .MaxRetries = defaultMaxRetries
97
+ }
98
+
75
99
return & Client {
76
100
urls : cfg .URLs ,
77
101
username : cfg .Username ,
78
102
password : cfg .Password ,
79
103
apikey : cfg .APIKey ,
80
104
105
+ retryOnStatus : cfg .RetryOnStatus ,
106
+ disableRetry : cfg .DisableRetry ,
107
+ enableRetryOnTimeout : cfg .EnableRetryOnTimeout ,
108
+ maxRetries : cfg .MaxRetries ,
109
+ retryBackoff : cfg .RetryBackoff ,
110
+
81
111
transport : cfg .Transport ,
82
112
selector : NewRoundRobinSelector (cfg .URLs ... ),
83
113
logger : cfg .Logger ,
@@ -88,41 +118,86 @@ func New(cfg Config) *Client {
88
118
//
89
119
func (c * Client ) Perform (req * http.Request ) (* http.Response , error ) {
90
120
var (
91
- dupReqBody io. Reader
92
- )
121
+ res * http. Response
122
+ err error
93
123
94
- // Get URL from the Selector
95
- //
96
- u , err := c .getURL ()
97
- if err != nil {
98
- // TODO(karmi): Log error
99
- return nil , fmt .Errorf ("cannot get URL: %s" , err )
100
- }
124
+ dupReqBodyForLog io.ReadCloser
125
+ )
101
126
102
127
// Update request
103
128
//
104
- c .setURL (u , req )
105
- c .setUserAgent (req )
106
- c .setAuthorization (u , req )
129
+ c .setReqUserAgent (req )
130
+
131
+ for i := 1 ; i <= c .maxRetries ; i ++ {
132
+ var (
133
+ nodeURL * url.URL
134
+ shouldRetry bool
135
+ )
136
+
137
+ // Get URL from the Selector
138
+ //
139
+ nodeURL , err = c .getURL ()
140
+ if err != nil {
141
+ // TODO(karmi): Log error
142
+ return nil , fmt .Errorf ("cannot get URL: %s" , err )
143
+ }
107
144
108
- // Duplicate request body for logger
109
- //
110
- if c .logger != nil && c .logger .RequestBodyEnabled () {
111
- if req .Body != nil && req .Body != http .NoBody {
112
- dupReqBody , req .Body , _ = duplicateBody (req .Body )
145
+ // Update request
146
+ //
147
+ c .setReqURL (nodeURL , req )
148
+ c .setReqAuth (nodeURL , req )
149
+
150
+ // Duplicate request body for logger
151
+ //
152
+ if c .logger != nil && c .logger .RequestBodyEnabled () {
153
+ if req .Body != nil && req .Body != http .NoBody {
154
+ dupReqBodyForLog , req .Body , _ = duplicateBody (req .Body )
155
+ }
113
156
}
114
- }
115
157
116
- // Set up time measures and execute the request
117
- //
118
- start := time .Now ().UTC ()
119
- res , err : = c .transport .RoundTrip (req )
120
- dur := time .Since (start )
158
+ // Set up time measures and execute the request
159
+ //
160
+ start := time .Now ().UTC ()
161
+ res , err = c .transport .RoundTrip (req )
162
+ dur := time .Since (start )
121
163
122
- // Log request and response
123
- //
124
- if c .logger != nil {
125
- c .logRoundTrip (req , res , dupReqBody , err , start , dur )
164
+ // Log request and response
165
+ //
166
+ if c .logger != nil {
167
+ c .logRoundTrip (req , res , dupReqBodyForLog , err , start , dur )
168
+ }
169
+
170
+ // Retry only on network errors, but don't retry on timeout errors, unless configured
171
+ //
172
+ if err != nil {
173
+ if err , ok := err .(net.Error ); ok {
174
+ if (! err .Timeout () || c .enableRetryOnTimeout ) && ! c .disableRetry {
175
+ shouldRetry = true
176
+ }
177
+ }
178
+ }
179
+
180
+ // Retry on configured response statuses
181
+ //
182
+ if res != nil && ! c .disableRetry {
183
+ for _ , code := range c .retryOnStatus {
184
+ if res .StatusCode == code {
185
+ shouldRetry = true
186
+ }
187
+ }
188
+ }
189
+
190
+ // Break if retry should not be performed
191
+ //
192
+ if ! shouldRetry {
193
+ break
194
+ }
195
+
196
+ // Delay the retry if a backoff function is configured
197
+ //
198
+ if c .retryBackoff != nil {
199
+ time .Sleep (c .retryBackoff (i ))
200
+ }
126
201
}
127
202
128
203
// TODO(karmi): Wrap error
@@ -139,7 +214,7 @@ func (c *Client) getURL() (*url.URL, error) {
139
214
return c .selector .Select ()
140
215
}
141
216
142
- func (c * Client ) setURL (u * url.URL , req * http.Request ) * http.Request {
217
+ func (c * Client ) setReqURL (u * url.URL , req * http.Request ) * http.Request {
143
218
req .URL .Scheme = u .Scheme
144
219
req .URL .Host = u .Host
145
220
@@ -154,7 +229,7 @@ func (c *Client) setURL(u *url.URL, req *http.Request) *http.Request {
154
229
return req
155
230
}
156
231
157
- func (c * Client ) setAuthorization (u * url.URL , req * http.Request ) * http.Request {
232
+ func (c * Client ) setReqAuth (u * url.URL , req * http.Request ) * http.Request {
158
233
if _ , ok := req .Header ["Authorization" ]; ! ok {
159
234
if u .User != nil {
160
235
password , _ := u .User .Password ()
@@ -180,7 +255,7 @@ func (c *Client) setAuthorization(u *url.URL, req *http.Request) *http.Request {
180
255
return req
181
256
}
182
257
183
- func (c * Client ) setUserAgent (req * http.Request ) * http.Request {
258
+ func (c * Client ) setReqUserAgent (req * http.Request ) * http.Request {
184
259
req .Header .Set ("User-Agent" , userAgent )
185
260
return req
186
261
}
0 commit comments