fix(session): cap Retry-After on the requests retry policies

A server-sent Retry-After could park a run for up to six hours.
This commit is contained in:
imSp4rky
2026-10-13 19:59:14 -06:00
parent 5ff2bef550
commit 9ef7a8bd06
5 changed files with 31 additions and 5 deletions
+15 -1
View File
@@ -6,7 +6,9 @@ from types import SimpleNamespace
import pytest
from unshackle.core.session import RnetSession
from unshackle.core.providers.imdb import IMDBProvider
from unshackle.core.service import Service
from unshackle.core.session import MAX_BACKOFF, RnetSession
@pytest.fixture
@@ -67,3 +69,15 @@ def test_exponential_backoff_without_retry_after(session):
def test_no_response_still_backs_off(session):
assert session.get_sleep_time(None, 30) == session.max_backoff
def test_service_session_caps_retry_after():
retry = Service.get_session().get_adapter("https://example.invalid/").max_retries
assert retry.respect_retry_after_header
assert retry.retry_after_max == MAX_BACKOFF
def test_provider_session_gives_up_on_a_rate_limit():
retry = IMDBProvider().session.get_adapter("https://example.invalid/").max_retries
assert not retry.respect_retry_after_header
assert 429 not in retry.status_forcelist
+1 -1
View File
@@ -61,7 +61,7 @@ class MetadataProvider(metaclass=ABCMeta):
retry = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET", "POST"],
respect_retry_after_header=False,
)
+7 -1
View File
@@ -38,6 +38,7 @@ from unshackle.core.console import console, prompt_user
from unshackle.core.constants import AnyTrack
from unshackle.core.credential import Credential
from unshackle.core.proxies.resolve import is_loopback
from unshackle.core.session import MAX_BACKOFF
from unshackle.core.titles import Title_T, Titles_T, remap_titles
from unshackle.core.titles.episode import Episode, Series
from unshackle.core.titles.movie import Movie, Movies
@@ -921,7 +922,12 @@ class RemoteService:
self._session.mount(
"https://",
HTTPAdapter(
max_retries=Retry(total=5, backoff_factor=0.2, status_forcelist=[429, 500, 502, 503, 504]),
max_retries=Retry(
total=5,
backoff_factor=0.2,
status_forcelist=[429, 500, 502, 503, 504],
retry_after_max=MAX_BACKOFF,
),
pool_maxsize=64,
pool_block=True,
),
+1
View File
@@ -431,6 +431,7 @@ class Service(metaclass=ABCMeta):
status_forcelist=STATUS_FORCELIST,
allowed_methods=RETRY_METHODS,
respect_retry_after_header=True,
retry_after_max=MAX_BACKOFF,
),
pool_connections=POOL_MAX_SIZE,
pool_maxsize=POOL_MAX_SIZE,
+7 -2
View File
@@ -31,7 +31,7 @@ from unshackle.core.downloaders import requests
from unshackle.core.drm import DRM_T, ClearKeyCENC, PlayReady, Widevine
from unshackle.core.drm.verify import decrypt_track
from unshackle.core.events import events
from unshackle.core.session import RnetSession, utf8_text
from unshackle.core.session import MAX_BACKOFF, RnetSession, utf8_text
from unshackle.core.tracks import resume
from unshackle.core.utilities import get_boxes, log_event, try_ensure_utf8
from unshackle.core.utils.subprocess import ffprobe
@@ -78,7 +78,12 @@ def direct_session(session: Union[Session, "RnetSession"], proxy: Optional[str]
new.mount(
"https://",
HTTPAdapter(
max_retries=Retry(total=5, backoff_factor=0.2, status_forcelist=[429, 500, 502, 503, 504]),
max_retries=Retry(
total=5,
backoff_factor=0.2,
status_forcelist=[429, 500, 502, 503, 504],
retry_after_max=MAX_BACKOFF,
),
pool_maxsize=64,
pool_block=True,
),