1 commit
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 26c35a0c18 |
fix(migrations): ограничить ожидание лока в 250 и закрепить lock_timeout гейтом
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / frontend-tests (pull_request) Successful in 1m44s
CI / openapi-codegen-check (pull_request) Successful in 2m37s
CI Trade-In / backend-tests (pull_request) Successful in 4m28s
CI / backend-tests (pull_request) Successful in 15m57s
Миграция 250 (DROP INDEX на таблице в 1061 строку) 2026-08-07 встала на боевой БД: сам DROP берёт лок за миллисекунды, но ЖДАЛ его выдачи 29 минут за чужой аналитической psql-сессией, вторая попытка деплоя — ещё 16. Записи в _schema_migrations нет, схема не изменена — следующий деплой упёрся бы так же. Опасность не в простое деплоя: ждущий ACCESS EXCLUSIVE встаёт в очередь ПЕРЕД новыми запросами, поэтому за ним начинают ждать обычные SELECT приложения. - 250: SET LOCAL lock_timeout = '5s' сразу после BEGIN. Значение не наугад: снизу ограничено deadlock_timeout (1 s на проде) — автоотмена мешающего autovacuum срабатывает только после того, как ждущий отстоял эту секунду, так что 1-2 s гонялись бы с рутинным autovacuum; сверху 5 s — потолок простоя очереди приложения, против наблюдённых 1740 s это в 348 раз меньше. Проверено в форме запуска раннера (psql < файл, PostgreSQL 16.4, встречная сессия держит ACCESS SHARE): со строкой — отказ через 5 s и exit 3, без неё команда всё ещё висела в очереди на 15-й секунде. SET LOCAL доживает до DROP потому, что файл идёт одной psql-сессией и весь завёрнут в BEGIN/COMMIT. - scripts/check-migration-lock-timeout.py + шаг в ci.yml: новая миграция с блокирующим DDL обязана нести SET LOCAL lock_timeout, внутри транзакции и ДО первого DDL. Гейт бежит на каждом PR (обоих лэйнов), у него --selftest. Вариант «задать lock_timeout один раз в раннере» отвергнут замером, а не вкусом: session-wide значение обрывает CREATE INDEX CONCURRENTLY (тот ждёт параллельные транзакции через VirtualXactLock, и это ожидание тоже под lock_timeout) и оставляет невалидный индекс — то есть изготавливало бы ровно ту аварию, от которой заведена вторая проверка. Блокирующий DDL и CONCURRENTLY хотят противоположной политики → granularity = файл. - deploy.yml / deploy-tradein.yml: после цикла миграций — отказ, если в БД есть индексы с indisvalid=false (#2752). Оборванный CIC оставляет такой индекс молча: планировщик им не пользуется, а re-run миграции не чинит — CREATE INDEX CONCURRENTLY IF NOT EXISTS печатает «already exists, skipping» и выходит с кодом 0, после чего миграция помечается применённой. На проде таких индексов сейчас 0 (обе БД) — это профилактика. Refs #2752 |