Retry function handles fair usage limit
This commit is contained in:
@@ -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
|
break
|
||||||
}
|
|
||||||
time.Sleep(r.backoff(attempt))
|
|
||||||
} else if incr == 0 {
|
} else if incr == 0 {
|
||||||
time.Sleep(10 * time.Millisecond)
|
continue
|
||||||
} else {
|
|
||||||
// don't retry anymore
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user