fix: wait out a 429 and keep the pass going

A single 429 from git.libretro.com on cemu's tree ended profile_sync --all
with a traceback and no report for the other 646 profiles. upstream now
waits a 429 out (Retry-After up to 120 s, else 5, 20, 60 s), as
base_scraper already did, and stops asking a host that outlasts the
waits. profile_sync reports that profile as rate limited, fails the run,
and carries on with the other forges.
This commit is contained in:
Abdessamad Derraz committed 2026-10-23 15:02:48 +02:00
1 parent f2faf4d9ed
commit d458cf653a
4 files changed
+198 -53

No files matched your search

+34
View File
@@ -4163,6 +4163,40 @@ class TestRealignProse(unittest.TestCase):
self.assertIn("(deep.c:14)", self.path.read_text())
class TestRateLimitEndsOneProfile(unittest.TestCase):
"""git.libretro.com answered 429 on cemu's tree and the pass over 647
profiles stopped with a traceback and no report at all."""
def test_the_pass_goes_on_and_fails(self):
from unittest import mock
def build(name, *_args, **_kwargs):
if name == "cemu":
raise profile_sync.upstream.RateLimitError(
"https://git.libretro.com/x: HTTP 429", "git.libretro.com"
)
return ProfileReport(name=name, counts={"CHANGED": 1})
out, err = io.StringIO(), io.StringIO()
saved = sys.argv
try:
sys.argv = ["profile_sync.py", "--all", "--json"]
with mock.patch.object(profile_sync, "load_emulator_profiles", return_value={}), \
mock.patch.object(profile_sync, "select_profiles",
return_value={"cemu": {}, "after": {}}), \
mock.patch.object(profile_sync, "_check_quota"), \
mock.patch.object(profile_sync, "build_report", side_effect=build), \
contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
with self.assertRaises(SystemExit) as caught:
profile_sync.main()
finally:
sys.argv = saved
self.assertEqual(caught.exception.code, 1)
by_name = {r["name"]: r for r in json.loads(out.getvalue())}
self.assertEqual(by_name["cemu"]["skipped"], "rate limited: git.libretro.com")
self.assertEqual(by_name["after"]["needs_review"], 1)
class TestOutputModesApplyTheirFlags(unittest.TestCase):
"""--changed-only narrows --json too. Extras are refused off the text report."""
+44 -3
View File
@@ -237,9 +237,11 @@ class TestFetchRetry(unittest.TestCase):
self._orig = (urllib.request.urlopen, upstream._sleep)
self.slept: list[float] = []
upstream._sleep = self.slept.append
upstream._exhausted_hosts.clear()
def tearDown(self):
urllib.request.urlopen, upstream._sleep = self._orig
upstream._exhausted_hosts.clear()
def _serve(self, outcomes):
self.calls = 0
@@ -283,11 +285,50 @@ class TestFetchRetry(unittest.TestCase):
self.assertIsNone(upstream._fetch("https://host/x"))
self.assertEqual(self.calls, 1)
def test_rate_limit_is_not_retried(self):
self._serve([_http_error(429)])
@staticmethod
def _limited(retry_after: str | None = None) -> urllib.error.HTTPError:
import email.message # noqa: PLC0415
headers = email.message.Message()
if retry_after is not None:
headers["Retry-After"] = retry_after
return _http_error(429, headers)
def test_rate_limit_is_waited_out(self):
# git.libretro.com answered 429 on cemu's tree a few hundred
# requests into a pass, and the whole pass stopped.
self._serve([self._limited("7"), self._limited(), b"payload"])
self.assertEqual(upstream._fetch("https://host/x"), b"payload")
self.assertEqual(self.slept, [7.0, upstream.RATE_LIMIT_WAITS[1]])
def test_lasting_rate_limit_mutes_the_host_only(self):
self._serve([self._limited()])
with self.assertRaises(upstream.RateLimitError) as caught:
upstream._fetch("https://host/x")
self.assertEqual(caught.exception.host, "host")
self.assertEqual(self.calls, len(upstream.RATE_LIMIT_WAITS) + 1)
# The same host is not asked again, another one still is.
with self.assertRaises(upstream.RateLimitError):
upstream._fetch("https://host/y")
self.assertEqual(self.calls, len(upstream.RATE_LIMIT_WAITS) + 1)
self._serve([b"other"])
self.assertEqual(upstream._fetch("https://other/x"), b"other")
def test_long_retry_after_is_not_waited(self):
self._serve([self._limited(str(int(upstream.RATE_LIMIT_WAIT_CAP) + 1))])
with self.assertRaises(upstream.RateLimitError):
upstream._fetch("https://host/x")
self.assertEqual(self.calls, 1)
self.assertEqual((self.calls, self.slept), (1, []))
def test_exhausted_github_quota_is_not_waited(self):
import email.message # noqa: PLC0415
headers = email.message.Message()
headers["X-RateLimit-Remaining"] = "0"
self._serve([_http_error(403, headers)])
with self.assertRaises(upstream.RateLimitError):
upstream._fetch("https://host/x")
self.assertEqual((self.calls, self.slept), (1, []))
class TestGoneUpstream(unittest.TestCase):