mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(downloads): handle http status 429 rate-limit retry (#1749)
* feat(downloads): handle rate-limit retry * feat: abort if greater than max time 7200 seconds
This commit is contained in:
parent
ca2d956e02
commit
737184a985
1 changed files with 62 additions and 1 deletions
|
@ -4,11 +4,13 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,6 +27,19 @@ import (
|
||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RetriableError is a custom error that contains a positive duration for the next retry
|
||||||
|
type RetriableError struct {
|
||||||
|
Err error
|
||||||
|
RetryAfter time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns error message and a Retry-After duration
|
||||||
|
func (e *RetriableError) Error() string {
|
||||||
|
return fmt.Sprintf("%s (retry after %v)", e.Err.Error(), e.RetryAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = (*RetriableError)(nil)
|
||||||
|
|
||||||
type DownloadService struct {
|
type DownloadService struct {
|
||||||
log zerolog.Logger
|
log zerolog.Logger
|
||||||
repo domain.ReleaseRepo
|
repo domain.ReleaseRepo
|
||||||
|
@ -147,7 +162,24 @@ func (s *DownloadService) downloadTorrentFile(ctx context.Context, indexer *doma
|
||||||
}
|
}
|
||||||
defer tmpFile.Close()
|
defer tmpFile.Close()
|
||||||
|
|
||||||
errFunc := retry.Do(retryableRequest(httpClient, req, r, tmpFile), retry.Delay(time.Second*3), retry.Attempts(3), retry.MaxJitter(time.Second*1))
|
errFunc := retry.Do(
|
||||||
|
retryableRequest(httpClient, req, r, tmpFile),
|
||||||
|
retry.Attempts(3),
|
||||||
|
retry.MaxJitter(time.Second*1),
|
||||||
|
//retry.Delay(time.Second*3),
|
||||||
|
retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
|
||||||
|
s.log.Error().Err(err).Msg("http call encountered error")
|
||||||
|
|
||||||
|
var retriable *RetriableError
|
||||||
|
if errors.As(err, &retriable) {
|
||||||
|
s.log.Debug().Msgf("http call rate-limited, retry after %v", retriable.RetryAfter)
|
||||||
|
return retriable.RetryAfter
|
||||||
|
}
|
||||||
|
return time.Second * 3
|
||||||
|
// apply a default exponential back off strategy
|
||||||
|
//return retry.BackOffDelay(n, err, config)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
return errFunc
|
return errFunc
|
||||||
}
|
}
|
||||||
|
@ -179,6 +211,7 @@ func retryableRequest(httpClient *http.Client, req *http.Request, r *domain.Rele
|
||||||
|
|
||||||
case http.StatusMethodNotAllowed:
|
case http.StatusMethodNotAllowed:
|
||||||
return retry.Unrecoverable(errors.New("unrecoverable error downloading torrent (%s) file (%s) from '%s' - status code: %d. Check if the request method is correct", r.TorrentName, r.DownloadURL, r.Indexer.Name, resp.StatusCode))
|
return retry.Unrecoverable(errors.New("unrecoverable error downloading torrent (%s) file (%s) from '%s' - status code: %d. Check if the request method is correct", r.TorrentName, r.DownloadURL, r.Indexer.Name, resp.StatusCode))
|
||||||
|
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
return errors.New("torrent %s not found on %s (%d) - retrying", r.TorrentName, r.Indexer.Name, resp.StatusCode)
|
return errors.New("torrent %s not found on %s (%d) - retrying", r.TorrentName, r.Indexer.Name, resp.StatusCode)
|
||||||
|
|
||||||
|
@ -188,6 +221,34 @@ func retryableRequest(httpClient *http.Client, req *http.Request, r *domain.Rele
|
||||||
case http.StatusInternalServerError:
|
case http.StatusInternalServerError:
|
||||||
return errors.New("server error (%d) encountered while downloading torrent (%s) file (%s) - check indexer keys for %s", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name)
|
return errors.New("server error (%d) encountered while downloading torrent (%s) file (%s) - check indexer keys for %s", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name)
|
||||||
|
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
// check Retry-After header if it contains seconds to wait for the next retry
|
||||||
|
after := resp.Header.Get("Retry-After")
|
||||||
|
if after == "" {
|
||||||
|
delay := 3
|
||||||
|
return &RetriableError{
|
||||||
|
Err: errors.New("rate-limit reached (%d) while downloading torrent (%s) file (%s) indexer (%s), retrying in %d seconds...", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name, delay),
|
||||||
|
RetryAfter: time.Duration(delay) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if retryAfter, e := strconv.ParseInt(after, 10, 32); e == nil {
|
||||||
|
// the server returns 0 to inform that the operation cannot be retried
|
||||||
|
if retryAfter <= 0 {
|
||||||
|
return retry.Unrecoverable(errors.New("rate-limit reached (%d) while downloading torrent (%s) file (%s) indexer (%s)", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name))
|
||||||
|
}
|
||||||
|
if retryAfter > 7200 {
|
||||||
|
return retry.Unrecoverable(errors.New("rate-limit reached (%d) while downloading torrent (%s) file (%s) indexer (%s) retry-after %d seconds is higher than allowed limit of 2h, aborting", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name, retryAfter))
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimitErr := errors.New("rate-limit reached (%d) while downloading torrent (%s) file (%s) indexer (%s), retrying in %d seconds", resp.StatusCode, r.TorrentName, r.DownloadURL, r.Indexer.Name, retryAfter)
|
||||||
|
|
||||||
|
return &RetriableError{
|
||||||
|
Err: rateLimitErr,
|
||||||
|
RetryAfter: time.Duration(retryAfter) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return retry.Unrecoverable(errors.New("unexpected status code %d: check indexer keys for %s", resp.StatusCode, r.Indexer.Name))
|
return retry.Unrecoverable(errors.New("unexpected status code %d: check indexer keys for %s", resp.StatusCode, r.Indexer.Name))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue