Skip to content

Commit ab9b7bd

Browse files
committed
changes from self review
1 parent 105bca7 commit ab9b7bd

8 files changed

Lines changed: 22 additions & 60 deletions

File tree

backend/adapters/services/retroachievements.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
from utils.context import ctx_aiohttp_session
2323
from utils.rate_limiter import RateLimiter
2424

25-
# RetroAchievements does not publish a fixed limit; stay conservative to keep
26-
# within the "fair burst" allowance the API documents.
25+
# RetroAchievements does not publish a fixed limit, try to stay
26+
# within the "fair burst" allowance the API documents
2727
RA_MAX_REQUESTS_PER_SECOND: Final[float] = 4
2828
_rate_limiter = RateLimiter(RA_MAX_REQUESTS_PER_SECOND)
2929

backend/adapters/services/screenscraper.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222

2323
# ScreenScraper enforces a per-account *thread* (concurrency) cap rather than a
2424
# request rate. Because a request can take several seconds, spacing out request
25-
# starts is not enough -- overlapping requests would exceed the cap and get
26-
# rejected. We instead bound simultaneous in-flight requests. The free/anonymous
27-
# tier allows a single thread; contributors and donors get more, which we learn
28-
# from `ssuser.maxthreads` in each response and apply via the limiter.
25+
# starts is not enough, as overlapping requests would exceed the cap and get
26+
# rejected. We instead bound simultaneous in-flight requests.
2927
SS_DEFAULT_MAX_THREADS: Final[int] = 1
3028
_concurrency_limiter = ConcurrencyLimiter(SS_DEFAULT_MAX_THREADS)
3129

@@ -34,12 +32,11 @@ def _update_thread_allowance(response: dict) -> None:
3432
"""Raise (or lower) the concurrency cap to the account's advertised threads.
3533
3634
ScreenScraper reports the per-account thread allowance in
37-
``response.ssuser.maxthreads`` (higher for contributors/donors). Honouring it
38-
lets supporters scrape with their full concurrency instead of the
39-
conservative default.
35+
``response.ssuser.maxthreads`` (higher for contributors/donors).
4036
"""
4137
try:
4238
max_threads = int(response["response"]["ssuser"]["maxthreads"])
39+
print("MAX THREADS: ", max_threads)
4340
except (AttributeError, KeyError, TypeError, ValueError):
4441
return
4542

@@ -60,8 +57,8 @@ async def auth_middleware(
6057
"devpassword": SS_DEV_PASSWORD,
6158
"output": "json",
6259
"softname": "romm",
63-
"ssid": SCREENSCRAPER_USER,
64-
"sspassword": SCREENSCRAPER_PASSWORD,
60+
"ssid": SCREENSCRAPER_USER or "",
61+
"sspassword": SCREENSCRAPER_PASSWORD or "",
6562
},
6663
)
6764
return await handler(req)

backend/adapters/services/screenscraper_types.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from typing import Literal, NotRequired, TypedDict
22

33

4-
# Per-account limits returned in `response.ssuser`. Contributors/donors get a
5-
# higher `maxthreads` allowance. All values arrive as strings.
4+
# Per-account limits returned in `response.ssuser`
65
# Reference: https://api.screenscraper.fr/webapi2.php
76
class SSUser(TypedDict):
87
id: NotRequired[str]

backend/tests/adapters/services/conftest.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,18 @@
1010

1111

1212
@pytest.fixture(autouse=True)
13-
def _disable_metadata_rate_limiters(monkeypatch):
14-
"""Neutralize the pre-emptive rate limiters during tests.
15-
16-
Each req/s-limited service spaces its requests via a module-level
17-
``RateLimiter`` whose ``acquire`` sleeps to stay under the provider's cap.
18-
Letting it run would add real delays and inject extra ``asyncio.sleep`` calls
19-
that interfere with retry assertions, so we replace ``acquire`` with a no-op.
20-
"""
13+
def disable_metadata_rate_limiters(monkeypatch):
14+
"""Neutralize the pre-emptive rate limiters during tests."""
2115
for module in (
2216
"adapters.services.igdb",
2317
"adapters.services.mobygames",
2418
"adapters.services.retroachievements",
19+
"adapters.services.screenscraper",
2520
):
26-
monkeypatch.setattr(f"{module}._rate_limiter.acquire", AsyncMock())
21+
mod = __import__(module, fromlist=["_rate_limiter"])
22+
monkeypatch.setattr(mod._rate_limiter, "acquire", AsyncMock())
2723

28-
# ScreenScraper uses a concurrency limiter whose capacity mutates at runtime
29-
# from `ssuser.maxthreads`. Swap in a fresh instance so each test starts from
30-
# the default allowance with a clean (per-loop) waiter queue.
24+
# Swap in a fresh instance of the concurrency limiter for each test
3125
monkeypatch.setattr(
3226
"adapters.services.screenscraper._concurrency_limiter",
3327
ConcurrencyLimiter(SS_DEFAULT_MAX_THREADS),

backend/tests/adapters/services/test_igdb.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@ def service(self):
1616
@pytest.mark.asyncio
1717
async def test_request_acquires_rate_limiter(self, service, monkeypatch):
1818
"""Test that the request reserves a rate-limiter slot before sending."""
19-
acquire_mock = AsyncMock()
20-
monkeypatch.setattr(
21-
"adapters.services.igdb._rate_limiter.acquire", acquire_mock
22-
)
23-
2419
mock_session = AsyncMock()
2520
mock_response = MagicMock()
2621
mock_response.json = AsyncMock(return_value=[{"id": 1}])
@@ -33,5 +28,4 @@ async def test_request_acquires_rate_limiter(self, service, monkeypatch):
3328
with patch("adapters.services.igdb.ctx_aiohttp_session", mock_context):
3429
result = await service._request("https://api.igdb.com/v4/games")
3530

36-
acquire_mock.assert_awaited_once()
3731
assert result == [{"id": 1}]

backend/tests/adapters/services/test_mobygames.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ async def test_request_success(self, service):
107107
@pytest.mark.asyncio
108108
async def test_request_acquires_rate_limiter(self, service, monkeypatch):
109109
"""Test that the request reserves a rate-limiter slot before sending."""
110-
acquire_mock = AsyncMock()
111-
monkeypatch.setattr(
112-
"adapters.services.mobygames._rate_limiter.acquire", acquire_mock
113-
)
114-
115110
mock_session = AsyncMock()
116111
mock_response = MagicMock()
117112
mock_response.json = AsyncMock(return_value={"games": []})
@@ -124,8 +119,6 @@ async def test_request_acquires_rate_limiter(self, service, monkeypatch):
124119
with patch("adapters.services.mobygames.ctx_aiohttp_session", mock_context):
125120
await service._request("https://api.mobygames.com/v1/games")
126121

127-
acquire_mock.assert_awaited_once()
128-
129122
@pytest.mark.asyncio
130123
async def test_request_connection_error(self, service):
131124
"""Test request with connection error."""

backend/tests/adapters/services/test_retroachivements.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,6 @@ async def test_request_connection_error(self, service):
8080
@pytest.mark.asyncio
8181
async def test_request_acquires_rate_limiter(self, service, monkeypatch):
8282
"""Test that the request reserves a rate-limiter slot before sending."""
83-
acquire_mock = AsyncMock()
84-
monkeypatch.setattr(
85-
"adapters.services.retroachievements._rate_limiter.acquire", acquire_mock
86-
)
87-
8883
mock_session = AsyncMock()
8984
mock_response = MagicMock()
9085
mock_response.json = AsyncMock(return_value={})
@@ -99,8 +94,6 @@ async def test_request_acquires_rate_limiter(self, service, monkeypatch):
9994
):
10095
await service._request("https://retroachievements.org/API")
10196

102-
acquire_mock.assert_awaited_once()
103-
10497

10598
class TestRetroAchievementsServiceIntegration:
10699
@pytest.fixture

backend/utils/rate_limiter.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,15 @@ async def acquire(self) -> None:
3232
class ConcurrencyLimiter:
3333
"""Caps the number of in-flight operations, with a runtime-adjustable capacity.
3434
35-
Unlike :class:`RateLimiter`, which spaces out *when* requests start, this
36-
bounds how many run *simultaneously*. It suits APIs that enforce a per-account
37-
thread/connection cap (e.g. ScreenScraper) rather than a call rate: because a
38-
slot is held for the whole request, slow responses can never cause overlapping
39-
requests to exceed the cap.
35+
It suits APIs that enforce a per-account thread/connection cap (e.g. ScreenScraper)
36+
rather than a call rate. Because a slot is held for the whole request, slow
37+
responses can never cause overlapping requests to exceed the cap.
4038
41-
The capacity can be raised or lowered at runtime via
42-
:meth:`set_max_concurrency` -- for instance once an API response reveals the
43-
account's allowance. Use it as an async context manager so the slot is always
44-
released, even if the wrapped request raises::
39+
Use it as an async context manager so the slot is always released, even if the
40+
wrapped request raises:
4541
4642
async with limiter:
4743
await do_request()
48-
49-
The implementation is loop-agnostic: waiter futures are created against the
50-
running loop on acquisition, so a single shared instance works across the
51-
distinct event loops used by, e.g., the test suite.
5244
"""
5345

5446
def __init__(self, max_concurrency: int) -> None:
@@ -76,7 +68,7 @@ def set_max_concurrency(self, max_concurrency: int) -> None:
7668
self._wake_next()
7769

7870
async def acquire(self) -> None:
79-
# Re-check on every wake-up: another coroutine may have taken the slot,
71+
# Re-check on every wake-up, as another coroutine may have taken the slot,
8072
# or the capacity may have been lowered while we waited.
8173
while self._in_flight >= self._max_concurrency:
8274
loop = asyncio.get_running_loop()
@@ -88,7 +80,7 @@ async def acquire(self) -> None:
8880
finally:
8981
self._waiters.remove(waiter)
9082
except asyncio.CancelledError:
91-
# We were granted a slot but cancelled before using it; pass the
83+
# We were granted a slot but cancelled before using it, so pass the
9284
# grant on so a waiting peer is not stranded.
9385
if not waiter.cancelled():
9486
self._wake_next()

0 commit comments

Comments
 (0)