diff --git a/tradein-mvp/backend/tests/test_3082_estimate_concurrency_cap.py b/tradein-mvp/backend/tests/test_3082_estimate_concurrency_cap.py index 3d389f24..c951e5b9 100644 --- a/tradein-mvp/backend/tests/test_3082_estimate_concurrency_cap.py +++ b/tradein-mvp/backend/tests/test_3082_estimate_concurrency_cap.py @@ -85,8 +85,11 @@ async def test_fifth_concurrent_estimate_gets_fast_429( ) gate = asyncio.Event() + # Счётчик входов в оценку: слот к этому моменту уже захвачен эндпоинтом. + entered = asyncio.Semaphore(0) async def _slow_estimate(*args, **kwargs) -> AggregatedEstimate: + entered.release() await gate.wait() return _canned_estimate() @@ -102,10 +105,24 @@ async def test_fifth_concurrent_estimate_gets_fast_429( asyncio.create_task(client.post("/api/v1/trade-in/estimate", json=payload)) for _ in range(_CONCURRENCY) ] - # Дать держателям дойти до acquire и занять все слоты. - await asyncio.sleep(0.05) + # Дождаться, что все держатели ВОШЛИ в оценку, то есть слоты заняты. + # Сон вместо этого — гонка (#3270): на нагруженном раннере пятый + # запрос успевал захватить свободный слот и навсегда вис на + # gate.wait(), потому что gate.set() стоит ниже по этому же корутину. + for i in range(_CONCURRENCY): + try: + await asyncio.wait_for(entered.acquire(), timeout=10) + except TimeoutError: # pragma: no cover — диагностика + gate.set() + pytest.fail(f"до оценки дошли {i} держателей из {_CONCURRENCY}") - fifth = await client.post("/api/v1/trade-in/estimate", json=payload) + try: + fifth = await asyncio.wait_for( + client.post("/api/v1/trade-in/estimate", json=payload), timeout=10 + ) + except TimeoutError: # pragma: no cover — диагностика вместо 120с виса + gate.set() + pytest.fail("5-й запрос повис вместо быстрого 429 — слот оказался свободен") assert fifth.status_code == 429, ( f"5-й конкурентный запрос прошёл ({fifth.status_code}) — " "потолка одновременности нет"