CI Trade-In: test_fifth_concurrent_estimate_gets_fast_429 виснет на 120с — гонка на sleep(0.05) #3270

Closed
opened 2026-08-30 08:59:54 +00:00 by lekss361 · 0 comments
Owner

Красный CI на PR #3268, к содержимому PR отношения не имеет: 1 failed, 5186 passed, упавший — test_3082_estimate_concurrency_cap.py::test_fifth_concurrent_estimate_gets_fast_429, и упал не ассертом, а Failed: Timeout (>120.0s) from pytest-timeout.

Механизм

Тест занимает все _CONCURRENCY = 4 слота семафора четырьмя висящими запросами, потом шлёт пятый и ждёт от него быстрый 429. Ожидание того, что четвёрка успела дойти до acquire, сделано сном:

holders = [asyncio.create_task(client.post(...)) for _ in range(_CONCURRENCY)]
await asyncio.sleep(0.05)          # ← гонка
fifth = await client.post(...)

На нагруженном раннере 50 мс не хватает. Тогда пятый запрос застаёт свободный слот, входит в подменённый _slow_estimate и встаёт на await gate.wait() — а gate.set() стоит ниже по тому же корутину, за строкой, до которой управление уже не дойдёт. Дедлок навсегда, до pytest-timeout.

Отсюда и цена: не ассерт с понятным текстом на второй секунде, а две минуты простоя job'а и красный CI на постороннем PR.

Правка

Сон заменён на счётный барьер: _slow_estimate отпускает asyncio.Semaphore(0) при входе, тест дожидается ровно _CONCURRENCY входов. Вход в _slow_estimate означает, что слот уже захвачен, — это ровно то условие, которое сон пытался угадать по времени.

Дополнительно пятый запрос обёрнут в wait_for(10): если он всё-таки заберёт слот, тест упадёт понятным сообщением за 10 секунд, а не повиснет на 120.

Красный CI на PR #3268, к содержимому PR отношения не имеет: `1 failed, 5186 passed`, упавший — `test_3082_estimate_concurrency_cap.py::test_fifth_concurrent_estimate_gets_fast_429`, и упал не ассертом, а `Failed: Timeout (>120.0s) from pytest-timeout`. ## Механизм Тест занимает все `_CONCURRENCY = 4` слота семафора четырьмя висящими запросами, потом шлёт пятый и ждёт от него быстрый 429. Ожидание того, что четвёрка успела дойти до `acquire`, сделано сном: ```python holders = [asyncio.create_task(client.post(...)) for _ in range(_CONCURRENCY)] await asyncio.sleep(0.05) # ← гонка fifth = await client.post(...) ``` На нагруженном раннере 50 мс не хватает. Тогда пятый запрос застаёт свободный слот, входит в подменённый `_slow_estimate` и встаёт на `await gate.wait()` — а `gate.set()` стоит **ниже по тому же корутину**, за строкой, до которой управление уже не дойдёт. Дедлок навсегда, до `pytest-timeout`. Отсюда и цена: не ассерт с понятным текстом на второй секунде, а две минуты простоя job'а и красный CI на постороннем PR. ## Правка Сон заменён на счётный барьер: `_slow_estimate` отпускает `asyncio.Semaphore(0)` при входе, тест дожидается ровно `_CONCURRENCY` входов. Вход в `_slow_estimate` означает, что слот уже захвачен, — это ровно то условие, которое сон пытался угадать по времени. Дополнительно пятый запрос обёрнут в `wait_for(10)`: если он всё-таки заберёт слот, тест упадёт понятным сообщением за 10 секунд, а не повиснет на 120.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3270
No description provided.