Retry function handles fair usage limit

This commit is contained in:
Ben Adrian Sarmiento
2024-07-07 23:15:15 +02:00
parent 1373bbb975
commit f48f352816

View File

@@ -27,7 +27,7 @@ type HTTPClient struct {
maxRetries int maxRetries int
timeoutSecs int timeoutSecs int
rateLimitSleepSecs int rateLimitSleepSecs int
backoff func(attempt int) time.Duration backoff func(int, int) time.Duration
dnsCache cmap.ConcurrentMap[string, string] dnsCache cmap.ConcurrentMap[string, string]
hosts []string hosts []string
log *logutil.Logger log *logutil.Logger
@@ -65,7 +65,7 @@ func NewHTTPClient(
client: &http.Client{}, client: &http.Client{},
maxRetries: maxRetries, maxRetries: maxRetries,
timeoutSecs: timeoutSecs, timeoutSecs: timeoutSecs,
rateLimitSleepSecs: 4, rateLimitSleepSecs: 2,
backoff: backoffFunc, backoff: backoffFunc,
dnsCache: cmap.New[string](), dnsCache: cmap.New[string](),
hosts: hosts, hosts: hosts,
@@ -160,6 +160,7 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
if resp != nil && resp.StatusCode >= http.StatusBadRequest { if resp != nil && resp.StatusCode >= http.StatusBadRequest {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
if req.Host == "api.real-debrid.com" { if req.Host == "api.real-debrid.com" {
// api servers
if body != nil { if body != nil {
var errResp ApiErrorResponse var errResp ApiErrorResponse
jsonErr := json.Unmarshal(body, &errResp) jsonErr := json.Unmarshal(body, &errResp)
@@ -173,28 +174,20 @@ func (r *HTTPClient) Do(req *http.Request) (*http.Response, error) {
} }
} else { } else {
// download servers // download servers
errResp := DownloadErrorResponse{ err = &DownloadErrorResponse{
Message: resp.Header.Get("X-Error"), Message: resp.Header.Get("X-Error"),
Code: resp.StatusCode, Code: resp.StatusCode,
} }
err = &errResp
} }
} }
incr := r.shouldRetry(req, resp, err, r.rateLimitSleepSecs) incr := r.shouldRetry(req, resp, err, attempt, r.rateLimitSleepSecs)
if incr > 0 { if incr == -1 {
attempt += incr
if attempt > r.maxRetries {
err = fmt.Errorf("max retries exceeded: %w", err)
break
}
time.Sleep(r.backoff(attempt))
} else if incr == 0 {
time.Sleep(10 * time.Millisecond)
} else {
// don't retry anymore
break break
} else if incr == 0 {
continue
} }
attempt += incr
} }
return resp, err return resp, err
} }
@@ -247,77 +240,71 @@ func (r *HTTPClient) proxyDialer(proxyURL *url.URL) (proxy.Dialer, error) {
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
} }
func (r *HTTPClient) shouldRetry(req *http.Request, resp *http.Response, err error, rateLimitSleep int) int { // shouldRetry returns a number indicating whether the request should be retried
if strings.HasSuffix(req.URL.Path, "torrents/addMagnet") { // -1: don't retry
return -1 // don't retry to prevent duplicate torrents // 0: retry indefinitely
// 1: retry until maxRetries
func (r *HTTPClient) shouldRetry(req *http.Request, resp *http.Response, err error, attempts, rateLimitSleep int) int {
if attempts >= r.maxRetries {
return -1
} }
// assume that all addMagnet requests are always successful;
// don't retry to prevent duplicate torrents
if req.Host == "api.real-debrid.com" && strings.HasSuffix(req.URL.Path, "torrents/addMagnet") {
return -1
}
if apiErr, ok := err.(*ApiErrorResponse); ok { if apiErr, ok := err.(*ApiErrorResponse); ok {
switch apiErr.Code { switch apiErr.Code {
case -1: // Internal error case 5: // Slow down (retry infinitely)
return 1 case 34: // Too many requests (retry infinitely)
case 5: // Slow down (retry infinitely), default: 4 secs secs := r.backoff(attempts, rateLimitSleep)
time.Sleep(time.Duration(rateLimitSleep) * time.Second) r.log.Warnf("API rate limit reached, retrying in %d seconds", secs/time.Second)
return 0 time.Sleep(secs)
case 6: // Ressource unreachable
return 1
case 17: // Hoster in maintenance
return 1
case 18: // Hoster limit reached
return 1
case 25: // Service unavailable
return 1
case 34: // Too many requests (retry infinitely), default: 4 secs
time.Sleep(time.Duration(rateLimitSleep) * time.Second)
return 0 return 0
case 36: // Fair Usage Limit case 36: // Fair Usage Limit
time.Sleep(time.Duration(rateLimitSleep) * time.Second) secs := r.backoff(attempts, rateLimitSleep)
r.log.Warnf("Fair usage limit reached, retrying in %d seconds", secs/time.Second)
time.Sleep(secs)
return 0
case -1: // Internal error
return 1 return 1
default: default:
return -1 // don't retry return -1
} }
} else if downloadErr, ok := err.(*DownloadErrorResponse); ok { } else if downloadErr, ok := err.(*DownloadErrorResponse); ok {
switch downloadErr.Message { switch downloadErr.Message {
case "bytes_limit_reached": // 503 case "bytes_limit_reached": // 503
return -1 return -1
case "invalid_download_code": // 404 case "invalid_download_code": // 404
time.Sleep(time.Duration(rateLimitSleep) * time.Second) secs := r.backoff(attempts, rateLimitSleep)
r.log.Warnf("Invalid download code, retrying in %d seconds", secs/time.Second)
time.Sleep(r.backoff(attempts, rateLimitSleep))
return 1 return 1
default: default:
return 1 // retry return 1 // retry
} }
} }
if err != nil && strings.Contains(err.Error(), "timeout") {
return 1 // succesful requests
}
if resp != nil { if resp != nil {
if resp.StatusCode == http.StatusTooManyRequests {
// Too many requests: retry infinitely, default: 4 secs
time.Sleep(time.Duration(rateLimitSleep) * time.Second)
return 0
}
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError {
// other client errors: retry
return 1
}
if resp.StatusCode >= http.StatusInternalServerError {
// server errors: don't retry
return -1
}
okResponseCode := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusPartialContent okResponseCode := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusPartialContent
// if the request has a Range header but the server doesn't respond with a Content-Range header // if the request has a Range header but the server doesn't respond with a Content-Range header
hasRangeHeader := req.Header.Get("Range") != "" && !strings.HasPrefix(req.Header.Get("Range"), "bytes=0-") hasRangeHeader := req.Header.Get("Range") != "" && !strings.HasPrefix(req.Header.Get("Range"), "bytes=0-")
if hasRangeHeader && okResponseCode && resp.Header.Get("Content-Range") == "" { if okResponseCode && hasRangeHeader && resp.Header.Get("Content-Range") == "" {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
return 0 return 0
} }
return -1 // don't retry return -1
} }
return 1 return 1
} }
func backoffFunc(attempt int) time.Duration { func backoffFunc(attempt, base int) time.Duration {
maxDuration := 60 maxDuration := 60
backoff := int(math.Pow(2, float64(attempt))) backoff := int(math.Pow(float64(base), float64(attempt+1)))
if backoff > maxDuration { if backoff > maxDuration {
backoff = maxDuration backoff = maxDuration
} }