test(tradein): гонка на sleep(0.05) вешала тест потолка оценок на 120с (#3270) #3271

Merged
lekss361 merged 1 commit from fix/3270-estimate-concurrency-test-race into main 2026-08-30 09:08:40 +00:00
Owner

Закрывает #3270. Разблокирует #3268, где этот тест покраснел без всякой связи с содержимым PR.

Что было

test_3082_estimate_concurrency_cap.py::test_fifth_concurrent_estimate_gets_fast_429 занимает все четыре слота семафора висящими запросами и ждёт от пятого быстрый 429. Ожидание, что четвёрка успела дойти до acquire, было сделано сном на 50 мс.

На нагруженном раннере 50 мс не хватает. Тогда пятый запрос застаёт свободный слот, входит в подменённую оценку и встаёт на await gate.wait() — а gate.set() стоит ниже по тому же корутину, за строкой, до которой управление уже не дойдёт. Не ассерт, а вечный дедлок: Failed: Timeout (>120.0s) from pytest-timeout, две минуты простоя job'а и красный CI на постороннем PR.

Правка

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

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

Проверка

Локально гонку по времени не воспроизвести — она зависит от загрузки раннера. Поэтому условие подано напрямую: держателей на одного меньше, чем слотов, то есть в момент пятого запроса слот гарантированно свободен.

структура результат
старая (sleep(0.05)) висит до pytest-timeout, диагностики ноль
новая (барьер) падает за 12с: до оценки дошли 3 держателей из 4

Штатный прогон файла — 2 passed. ruff check + ruff format --check чисто.

Правка трогает только тест; поведение эндпоинта не менялось.

Закрывает #3270. Разблокирует #3268, где этот тест покраснел без всякой связи с содержимым PR. ## Что было `test_3082_estimate_concurrency_cap.py::test_fifth_concurrent_estimate_gets_fast_429` занимает все четыре слота семафора висящими запросами и ждёт от пятого быстрый 429. Ожидание, что четвёрка успела дойти до `acquire`, было сделано сном на 50 мс. На нагруженном раннере 50 мс не хватает. Тогда пятый запрос застаёт свободный слот, входит в подменённую оценку и встаёт на `await gate.wait()` — а `gate.set()` стоит **ниже по тому же корутину**, за строкой, до которой управление уже не дойдёт. Не ассерт, а вечный дедлок: `Failed: Timeout (>120.0s) from pytest-timeout`, две минуты простоя job'а и красный CI на постороннем PR. ## Правка Сон заменён счётным барьером: подменённая оценка отпускает `asyncio.Semaphore(0)` при входе, тест дожидается ровно `_CONCURRENCY` входов. Вход в оценку означает, что слот уже захвачен эндпоинтом (`_estimate_slots.acquire()` на `trade_in.py:516` идёт до вызова), — это ровно то условие, которое сон пытался угадать по времени. Оба ожидания обёрнуты в `wait_for(10)` с внятным сообщением: если условие всё-таки не сложится, тест скажет, сколько держателей дошло, за десять секунд, а не повиснет на сто двадцать. ## Проверка Локально гонку по времени не воспроизвести — она зависит от загрузки раннера. Поэтому условие подано напрямую: держателей на одного меньше, чем слотов, то есть в момент пятого запроса слот гарантированно свободен. | структура | результат | |---|---| | старая (`sleep(0.05)`) | висит до `pytest-timeout`, диагностики ноль | | новая (барьер) | падает за 12с: `до оценки дошли 3 держателей из 4` | Штатный прогон файла — `2 passed`. `ruff check` + `ruff format --check` чисто. Правка трогает только тест; поведение эндпоинта не менялось.
lekss361 added 1 commit 2026-08-30 09:03:08 +00:00
test(tradein): гонка на sleep(0.05) вешала тест потолка оценок на 120с (#3270)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
1bf9c4b632
Тест занимал все слоты семафора четырьмя висящими запросами и ждал, что
они дошли до acquire, обычным сном на 50 мс. На нагруженном раннере этого
не хватало: пятый запрос заставал свободный слот, входил в подменённую
оценку и вставал на gate.wait() — а gate.set() стоит ниже по тому же
корутину. Дедлок до pytest-timeout, две минуты простоя job'а и красный CI
на постороннем PR (#3268).

Сон заменён счётным барьером: подменённая оценка отпускает семафор при
входе, тест дожидается ровно _CONCURRENCY входов. Вход означает, что слот
уже захвачен, — это то самое условие, которое сон угадывал по времени.

Проверено пробой с намеренно свободным слотом (держателей на одного
меньше): старая структура висит до таймаута, новая падает за 12с с
понятным сообщением.
lekss361 merged commit 86621dd578 into main 2026-08-30 09:08:40 +00:00
lekss361 deleted branch fix/3270-estimate-concurrency-test-race 2026-08-30 09:08:40 +00:00
Sign in to join this conversation.
No reviewers
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#3271
No description provided.