МЕРА: у запросов к БД появился потолок по времени и по ожиданию блокировки (#3463) #3508
Merged
bot-backend
merged 2 commits from 2026-09-12 16:38:35 +00:00
fix/3463-db-statement-timeout into main
2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 70bb5a3a8f |
tradein: тот же потолок второму движку + тесты, которые ловят испорченное значение (#3463)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m45s
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Правки по deep-ревью PR #3508. 1. HIGH. `app/core/auth_db.py` строил второй движок БЕЗ `connect_args`, а моё обоснование пропуска было ложным: на проде `IDENTITY_STORE=auth` во всех трёх сервисах образа и `AUTH_DB_PASSWORD` задан (сверено `printenv` в контейнерах), то есть реестр живой. Путь горячий: `core/rbac.py` резолвит session-cookie в middleware, синхронно на event loop'е, на каждом запросе с cookie — значит `ACCESS EXCLUSIVE` на `auth.sessions` вешал бы не четыре слота `/estimate`, а весь uvicorn-воркер (он один), включая `/health`. Потолок — та же константа `DB_CONNECT_ARGS`: одна на оба движка, а не защита на одном и мина на втором. Срабатывание безопасно — вызов уже под `except Exception` с фолбэком. 2. MEDIUM. Испорченный `options` (`statement_timeout=30000zz`) проходил ЗЕЛЁНЫМ: статическая проверка искала ПОДСТРОКУ (а `…=30000` — префикс испорченного), а живая глушила отказ коннекта голым `except` → skip → запись в allowlist. На проде это `FATAL: invalid value for parameter` на КАЖДОМ коннекте, то есть полный отказ продукта при зелёном сьюте. Теперь: сравнение `options` на РАВЕНСТВО, и `_live_engine` сначала пробует коннект БЕЗ `connect_args` — сервера нет это пропуск, а «сервер есть, наши options он не принял» это падение. 3. LOW. У проверки согласованности был пол и не было крыши: `300_000` (пять минут) зеленел. Добавлена симметричная граница `<= 2 ×` самого длинного объявленного бюджета. 4. LOW. Три факта в комментариях исправлены: * `pg_stat_statements` НЕ опора по планировщику — вытесняет записи с calls=1 (`dealloc` вырос за десять минут, из топа пропал `REFRESH MATERIALIZED VIEW` 30.85 с). Основная опора — `scrape_runs`; * самый длинный set-based statement через движок — матч ГАР→houses: 2.07 с с городским фильтром и 6.46 с без. Запас ~3×, а не 7×; * `idle in transaction` 29 с — это tgbot (`services/tgbot/bridge.py`, транзакция поверх long-poll Telegram; сверено 3 пробами: одна и та же сессия, `SELECT value FROM tg_support_state …`), а не свипы. Решение не ставить потолок на простой от этого только крепче. Мутационная проверка (обе лэйны краснеют на каждой): испорченный `options`, `_STATEMENT_TIMEOUT_MS = 300_000`, снятый `connect_args`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| a8503d3a58 |
tradein: потолок на запрос и на ожидание блокировки для движка БД (#3463)
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 5m9s
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 12s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
На боевой БД `statement_timeout`, `lock_timeout` и `idle_in_transaction_session_timeout` равны 0, а у движка `app/core/db.py` не было `connect_args` вовсе. После #3444/#3449 шаги БД на пути `/estimate` идут через обёртку, которая при отмене по бюджету ДОЖИДАЕТСЯ своего потока (иначе он остаётся сиротой в общей `Session`) — ожидание верное, но его верхняя граница равна длительности самого запроса, а у запроса границы не было. Один `ACCESS EXCLUSIVE` на таблице → четыре повисших запроса → `_ESTIMATE_CONCURRENCY` исчерпан → `/estimate` отдаёт 429 всем остальным. Потолок ставится на КОННЕКТЕ (libpq `options`), а не в обёртке: таймаут в обёртке вернул бы ровно ту сироту, ради которой писался #3449. statement_timeout = 30 с: выше самого длинного ОБЪЯВЛЕННОГО бюджета `/estimate` (20 с, `estimate_avito_imv_timeout_s`) в 1.5 раза и в 7 раз выше самого долгого ЗАМЕРЕННОГО запроса через этот движок (4.27 с, `pg_stat_statements` на проде за 16 суток), но конечен. lock_timeout = 5 с: та же величина, что у миграций проекта, и больше `deadlock_timeout` (1 с на проде). `idle_in_transaction_session_timeout` намеренно не трогаем: тем же движком живёт планировщик, а его свипы держат транзакцию открытой всё время внешнего HTTP (замер: живая сессия `idle in transaction` 29 с). Задачи планировщика проверены, а не предположены: у `listing_source_snapshot` свой `SET LOCAL statement_timeout = 900000`, и тест доказывает, что `SET LOCAL` ПЕРЕКРЫВАЕТ сессионный потолок и не течёт за свою транзакцию. Самая долгая чисто-БД задача по `scrape_runs` за 14 суток укладывается в 9.7 с целиком; единственный запрос длиннее 20 с на всей БД (`REFRESH MATERIALIZED VIEW CONCURRENTLY`, 30.85 с) идёт мимо движка — по своему сырому psycopg-соединению. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |