fix(#231): switch search_by_quarter area layers to grid-walk (Sub-PR B) #260

Merged
lekss361 merged 3 commits from fix/231-nspd-search-by-quarter-grid into main 2026-05-17 07:26:09 +00:00
2 changed files with 8 additions and 7 deletions
Showing only changes of commit 570c987e78 - Show all commits

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