@@ -24,6 +24,7 @@ import (
2424 "encoding/json"
2525 "errors"
2626 "fmt"
27+ "math"
2728 "math/rand"
2829 "net/http"
2930 "net/url"
@@ -33,6 +34,7 @@ import (
3334 "reflect"
3435 "runtime"
3536 "sort"
37+ "strconv"
3638 "strings"
3739 "sync"
3840 "sync/atomic"
@@ -148,15 +150,52 @@ func insertCloudflareHeaders(req *http.Request) {
148150 }
149151}
150152
153+ // retryBackoff performs exponential backoff based on the attempt number and limited
154+ // by the provided minimum and maximum durations.
155+ //
156+ // It also tries to parse Retry-After response header when a http.StatusTooManyRequests
157+ // (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
158+ // seconds the server states it may be ready to process more requests from this client.
159+ func calcBackoff (min , max time.Duration , attemptNum int , resp * http.Response ) time.Duration {
160+ if resp != nil {
161+ if resp .StatusCode == http .StatusTooManyRequests || resp .StatusCode == http .StatusServiceUnavailable {
162+ if s , ok := resp .Header ["Retry-After" ]; ok {
163+ if sleep , err := strconv .ParseInt (s [0 ], 10 , 64 ); err == nil {
164+ return time .Second * time .Duration (sleep )
165+ }
166+ }
167+ }
168+ }
169+
170+ mult := math .Pow (2 , float64 (attemptNum )) * float64 (min )
171+ sleep := time .Duration (mult )
172+ if float64 (sleep ) != mult || sleep > max {
173+ sleep = max
174+ }
175+
176+ return sleep
177+ }
178+
151179func (r * requestHandler ) RoundTrip (req * http.Request ) (resp * http.Response , err error ) {
180+ defer func () {
181+ if r := recover (); r != nil {
182+ if resp != nil && resp .Body != nil {
183+ resp .Body .Close ()
184+ resp .Body = nil
185+ }
186+
187+ err = fmt .Errorf ("http client panic: %s" , r )
188+ }
189+ }()
190+
152191 insertCloudflareHeaders (req )
153192
154193 resp , err = r .Transport .RoundTrip (req )
155194
156- delay := 500 * time .Millisecond
157195 attempts := 1
158196 retry := true
159197
198+ const minDelay = 500 * time .Millisecond
160199 const maxDelay = 5 * time .Second
161200 const maxAttempts = 10
162201
@@ -180,26 +219,35 @@ func (r *requestHandler) RoundTrip(req *http.Request) (resp *http.Response, err
180219 r .downloader .stats .WebseedBytesDownload .Add (resp .ContentLength )
181220 retry = false
182221
183- case http .StatusInternalServerError , http .StatusBadGateway :
222+ // the first two statuses here have been observed from cloudflare
223+ // during testing. The remainder are generally understood to be
224+ // retriable http responses, calcBackoff will use the Retry-After
225+ // header if its availible
226+ case http .StatusInternalServerError , http .StatusBadGateway ,
227+ http .StatusRequestTimeout , http .StatusTooEarly ,
228+ http .StatusTooManyRequests , http .StatusServiceUnavailable ,
229+ http .StatusGatewayTimeout :
230+
184231 r .downloader .stats .WebseedServerFails .Add (1 )
185232
233+ if resp .Body != nil {
234+ resp .Body .Close ()
235+ resp .Body = nil
236+ }
237+
186238 attempts ++
187- delayTimer := time .NewTimer (delay )
239+ delayTimer := time .NewTimer (calcBackoff ( minDelay , maxDelay , attempts , resp ) )
188240
189241 select {
190242 case <- delayTimer .C :
191243 // Note this assumes the req.Body is nil
192244 resp , err = r .Transport .RoundTrip (req )
193245 r .downloader .stats .WebseedTripCount .Add (1 )
194246
195- if err == nil && delay < maxDelay {
196- delay = delay + (time .Duration (rand .Intn (200 - 75 )+ 75 )* delay )/ 100
197- }
198-
199247 case <- req .Context ().Done ():
200248 err = req .Context ().Err ()
201249 }
202- retry = attempts > maxAttempts
250+ retry = attempts < maxAttempts
203251
204252 default :
205253 r .downloader .stats .WebseedBytesDownload .Add (resp .ContentLength )
0 commit comments