fix(#231): repair test_rate_limit_semaphore regression + docstring sync

Re-review feedback (PR #260 head 8191a85):

1. tests/scrapers/test_nspd_bulk_client.py:360 — test referenced bulk_mod._SEMAPHORE
   which was renamed to _SEMAPHORE_LIMIT in previous commit. AttributeError на
   полном pytest run. Заменено: assert bulk_mod._SEMAPHORE_LIMIT == 3 (smoke).
   max_concurrent <= 3 уже валидирует throttling — capacity check был лишним.

2. app/services/scrapers/nspd_client.py docstrings (lines 483, 506) — обновлены
   ссылки NSPDBulkClient._SEMAPHORE(3) → NSPDBulkClient._sem (per-instance,
   capacity=3) чтобы соответствовать новой архитектуре.
This commit is contained in:
lekss361 2026-05-17 10:22:16 +03:00
parent 8191a85102
commit 570c987e78
2 changed files with 8 additions and 7 deletions

View file

@ -480,7 +480,7 @@ class NSPDClient:
asyncio.run(). Предназначен для вызова из синхронного кода (Celery
task, FastAPI sync handler).
Concurrency: NSPDBulkClient._SEMAPHORE(3) ограничивает параллельные
Concurrency: NSPDBulkClient._sem (per-instance, capacity=3) ограничивает параллельные
запросы. При grid_n=7 (49 ячеек) все 49 ячеек запускаются одним
gather; семафор пропускает не более 3 одновременно. Thread-safety:
каждый вызов get_features_in_bbox_grid создаёт новый event loop
@ -503,7 +503,7 @@ class NSPDClient:
Note:
Не делает live HTTP вызовы если вызван с mock NSPDBulkClient.
Rate-limit управляется семафором NSPDBulkClient._SEMAPHORE(3) +
Rate-limit управляется семафором NSPDBulkClient._sem (per-instance, capacity=3) +
asyncio.sleep(0.05) jitter не через self.rate_ms.
"""
# Импортируем здесь чтобы избежать circular import:

View file

@ -316,7 +316,7 @@ async def test_list_objects_in_building_parses(
@pytest.mark.asyncio
async def test_rate_limit_semaphore_max_3_concurrent() -> None:
"""Не более 3 одновременных запросов через _SEMAPHORE.
"""Не более 3 одновременных запросов через per-instance self._sem (PR #260).
Мокируем httpx.AsyncClient.get (нижний уровень) с задержкой, чтобы
реальный семафор работал. Считаем max in-flight внутри семафора.
@ -332,7 +332,7 @@ async def test_rate_limit_semaphore_max_3_concurrent() -> None:
async def slow_get(*args: Any, **kwargs: Any) -> httpx.Response:
nonlocal max_concurrent, current, call_count
# Фиксируем вход — уже внутри семафора (httpx.get вызывается после async with _SEMAPHORE)
# Фиксируем вход — уже внутри семафора (httpx.get вызывается после async with self._sem)
async with lock:
current += 1
call_count += 1
@ -353,11 +353,12 @@ async def test_rate_limit_semaphore_max_3_concurrent() -> None:
tasks = [client.search_by_quarter(f"66:41:{i:07d}") for i in range(6)]
await asyncio.gather(*tasks)
# _SEMAPHORE(3) → не более 3 одновременно внутри slow_get
# self._sem(3) → не более 3 одновременно внутри slow_get
assert max_concurrent <= 3, f"Expected ≤3 concurrent, got {max_concurrent}"
assert call_count == 6 # все 6 вызовов прошли
# Убедимся что _SEMAPHORE в модуле имеет нужное значение capacity
assert bulk_mod._SEMAPHORE._value >= 0 # семафор сброшен после всех задач
# NB: per-instance self._sem cleanup проверяется через max_concurrent <= 3 выше.
# Module-level _SEMAPHORE удалён в PR #260 (cross-loop binding fix); smoke на capacity:
assert bulk_mod._SEMAPHORE_LIMIT == 3
@pytest.mark.asyncio