fix(mera): pool_timeout 30→5 с — отдельным коммитом, с триггером отката
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
Единственная правка ветки, которая меняет РЕЖИМ ОТКАЗА при исчерпании пула: было «медленно» (ждём коннект до 30 с), стало «быстро с ошибкой» (5 с и `sqlalchemy.exc.TimeoutError` → 500, глобального обработчика в app/main.py нет). И едет она во все сервисы образа — backend, scraper, tgbot (tradein-mvp/docker-compose.prod.yml), для скраппера и бота обоснования в коде нет: за 29 ч логов исчерпания пула не было ни разу, проверить новое значение на проде пока не на чем. Поэтому коммит последний в ветке: ветку можно мержить без него, а на проде — откатить одной командой (`git revert`). Обоснование самого значения: чекаут коннекта нельзя прервать `asyncio.wait_for`, он занимает поток `asyncio.to_thread` целиком, а пул потоков конечен (min(32, cpu+4)) — исчерпанный пул коннектов превращается в исчерпанный пул потоков. 5 с короче самого короткого бюджета источника (8 с Yandex/Cian/ house_meta; geocode 12 с, IMV 20 с — длиннее): занятый пул деградирует ОДИН источник, а не весь запрос. ТРИГГЕР ОТКАТА (вернуть 30 с) записан в комментарии рядом со значением: любое `QueuePool limit ... timed out` в логах бэкенда ЛИБО рост failed+zombie в `scrape_runs` после деплоя. Правка комментария по ревью (L2): «вчетверо больше любого бюджета внешнего источника (8 с)» было неточно — бюджеты 8 / 12 / 20 с, перечислены явно. Гейт `test_pool_checkout_wait_shorter_than_source_budget` переехал сюда же (в коммите без `pool_timeout` он был бы красным) и читает публичный `engine.pool.timeout()` вместо приватного `pool._timeout`. Refs #3083, #3408
This commit is contained in:
parent
cf71825c27
commit
ae6d28d5e2
2 changed files with 36 additions and 0 deletions
|
|
@ -35,6 +35,22 @@ engine = create_engine(
|
||||||
# которые пул закрывает сам. Потолок процесса: 5 + 15 = 20; воркер один
|
# которые пул закрывает сам. Потолок процесса: 5 + 15 = 20; воркер один
|
||||||
# (docker-compose.prod.yml, uvicorn без --workers), Postgres max_connections=100.
|
# (docker-compose.prod.yml, uvicorn без --workers), Postgres max_connections=100.
|
||||||
max_overflow=15,
|
max_overflow=15,
|
||||||
|
# #3408 п.2. Дефолтные 30 с ожидания коннекта длиннее ЛЮБОГО бюджета внешнего
|
||||||
|
# источника в эстиматоре: 8 с (Yandex / Cian / house_meta), 12 с (geocode),
|
||||||
|
# 20 с (Avito IMV — `estimate_avito_imv_timeout_s`, config.py:852). Сам чекаут
|
||||||
|
# прервать `asyncio.wait_for` не может: он занимает поток `asyncio.to_thread`
|
||||||
|
# целиком, а пул потоков конечен (min(32, cpu+4)) — исчерпанный пул коннектов
|
||||||
|
# так превращается в исчерпанный пул потоков. 5 с короче самого КОРОТКОГО
|
||||||
|
# бюджета: занятый пул деградирует ОДИН источник, а не весь запрос.
|
||||||
|
#
|
||||||
|
# Отдельный коммит в конце ветки намеренно (ревью PR #3444): это единственная
|
||||||
|
# правка, которая меняет режим отказа с «медленно» на «быстро с ошибкой», и
|
||||||
|
# едет она во ВСЕ сервисы образа — backend, scraper, tgbot
|
||||||
|
# (tradein-mvp/docker-compose.prod.yml). За 29 ч логов исчерпания пула не было
|
||||||
|
# ни разу, то есть новое значение на проде пока не на чем проверить.
|
||||||
|
# ТРИГГЕР ОТКАТА на 30 с: любое `QueuePool limit ... timed out` в логах
|
||||||
|
# бэкенда ЛИБО рост failed+zombie в `scrape_runs` после деплоя.
|
||||||
|
pool_timeout=5,
|
||||||
)
|
)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,23 @@ def test_pool_ceiling_covers_declared_concurrency() -> None:
|
||||||
f"({_ESTIMATE_CONCURRENCY} оценок + {_SUGGEST_CONCURRENCY} подсказок + "
|
f"({_ESTIMATE_CONCURRENCY} оценок + {_SUGGEST_CONCURRENCY} подсказок + "
|
||||||
f"{_MAX_DEFERRED_REFRESH_TASKS} фоновых догрузок) — штатная работа исчерпает пул"
|
f"{_MAX_DEFERRED_REFRESH_TASKS} фоновых догрузок) — штатная работа исчерпает пул"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pool_checkout_wait_shorter_than_source_budget() -> None:
|
||||||
|
"""Ожидание коннекта короче бюджета внешнего источника.
|
||||||
|
|
||||||
|
Иначе занятый пул съедает весь бюджет запроса (и поток `asyncio.to_thread`,
|
||||||
|
которых тоже конечное число) вместо того, чтобы деградировать один источник.
|
||||||
|
Сравниваем с самым КОРОТКИМ бюджетом (8 с Yandex/Cian): geocode 12 с и IMV
|
||||||
|
20 с длиннее, их этот же потолок покрывает с запасом.
|
||||||
|
"""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
budget = min(
|
||||||
|
settings.estimate_yandex_valuation_timeout_s,
|
||||||
|
settings.estimate_cian_valuation_timeout_s,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert engine.pool.timeout() < budget, (
|
||||||
|
f"pool_timeout {engine.pool.timeout()}с >= бюджета источника {budget}с"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue