test(ci): развести «нагрузку не создать» и «защита сломана» в двух флапающих гейтах (#2783) #2789
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2789
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/auth-flood-test-flake"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Два гейта красили ЧУЖИЕ PR-ы по таймингу раннера. Болезнь одна, диагнозы разные — правки тоже.
1.
tradein/test_auth_api.py::test_login_flood_capped_by_rate_…— подводило ПРЕДУСЛОВИЕУтверждение про потолок верное; красный давало
assert len(codes) > connections— «каждое из ста соединений успело сходить дважды за секунду». Это мерило скорости раннера, а не нагрузки: на занятом первый круг из ста запросов сам съедает всю секунду (deadlineпроверяется ПЕРЕД запросом), выходит ровно 100 ответов, и невыполненное предусловие даёт красный, неотличимый от настоящей поломки потолка.Замер частоты (2026-08-07, 8 ядер):
Предусловие — первое, что ломается при росте нагрузки, поэтому в CI и вылезло именно оно.
Что сделано. Предусловие стало содержательным и завязано на ТОТ ЖЕ порог, с которым сверяется вердикт: флуд обязан предлагать больше
ceiling × 1.5запросов/с. Пока он предлагает меньше, следующее утверждение зелено даже на системе вовсе без потолка — то есть измерения нет. Красный при этом говорит «раннер не потянул», а не «потолок сломан». В падавших прогонах флуд предлагал 55-80 запросов/с против порога 30/с — мерить БЫЛО на чем, и теперь эти прогоны зелёные по делу, а не по снисхождению.Остальные четыре утверждения не тронуты.
Проверка на способность краснеть. Сломал потолок в проде-коде (
verify_slots_saturated→return False, пулmax_workers=64мимо настройки):И то же самое ПОД НАГРУЗКОЙ, на прогонах, где раньше падало предусловие (
100 за 1.49с) — 4 из 6 красные на вердикте о потолке. Новое предусловие поломку не маскирует.2.
backend/tests/services/test_weather_cache.py— подводило САМО утверждениеТест требовал «16 потоков → ровно один сетевой вызов», а
weather_cache.py:279-292прямым текстом обещает обратное: сетевой вызов вынесен ЗА lock сознательно (#1370, иначе всеanalyzeсериализуются на время httpx даже для разных координат), и «cold-start на ОДИН ключ может породить несколько параллельных запросов… приемлемо». Зелёным тест был по везению GIL. Комментарий# Микро-задержка — окно для других потоковв теле мока при этом врал: задержки там не было.Замер (200 штормов подряд, тот же путь, что в тесте):
switchinterval(5мс), вхолостую1×199,2×1 → ~0.5% прогонов красные1×199,2×1 → те же ~0.5%setswitchinterval(1e-6)(потоки реально чередуются)То есть утверждение ложно почти всегда, когда гонка вообще случается. ~0.5% на прогон — это и есть «раз в двести PR-ов красим чужой диф» (#2781).
Что сделано — вариант (1) из issue: тест приведён к фактическому контракту. Код НЕ тронут: поведение объявлено приемлемым в #1370 с обоснованием, лишние запросы бывают только на cold-start одного ключа и идемпотентны. Настоящий single-flight (per-key lock) — отдельная задача с отдельным обоснованием, а не побочный эффект правки теста.
Тест теперь утверждает то, что код правда гарантирует:
1 ≤ n ≤ 16— не больше числа участников (повторный фетч = поломка);time.sleep(0.05)в моке делает гонку неслучайной: замер — 30 штормов из 30 дают ровно 16 вызовов, то есть тест теперь мерит худший случай уступки, а не везение планировщика, и верхняя граница проверяется на самой границе.Проверка на способность краснеть — три поломки кэша, три разных красных:
assert [] == [(56.84, 60.59)]assert [(56.84, 61.09)] == [(56.84, 60.59)]после шторма кэш обязан отвечать без сети, а вызовов стало 17 против 16Test plan
tradein-mvp/backend: 20 прогонов вхолостую — 20 зелёных; 15 под load average ~185 — 0 падений на предусловии (было 10 из 15)tradein-mvp/backend: весьtests/test_auth_api.py— 48 passedbackend: 20 прогоновTestConcurrencySafe— 20 зелёных; весьtests/services/test_weather_cache.py— 20 passedCI / backend-tests+ci-tradein)Чего в этом PR НЕТ
худший сторонний запрос >500мс,проб <10) — 5 из 15 после правки. Это отдельное утверждение с отдельным диагнозом: под таким раннером «API встаёт» неотличимо от «машина встала». Сознательно не трогал — при загрузке, на которой ловилось предусловие (load ~90), эти утверждения не падали ни разу за 20 прогонов.Refs #2783