diff --git a/tradein-mvp/backend/app/core/db.py b/tradein-mvp/backend/app/core/db.py index 408d47ba..1214a3eb 100644 --- a/tradein-mvp/backend/app/core/db.py +++ b/tradein-mvp/backend/app/core/db.py @@ -35,6 +35,22 @@ engine = create_engine( # которые пул закрывает сам. Потолок процесса: 5 + 15 = 20; воркер один # (docker-compose.prod.yml, uvicorn без --workers), Postgres max_connections=100. 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) diff --git a/tradein-mvp/backend/tests/test_3408_pool_ceiling.py b/tradein-mvp/backend/tests/test_3408_pool_ceiling.py index 254340db..2c220834 100644 --- a/tradein-mvp/backend/tests/test_3408_pool_ceiling.py +++ b/tradein-mvp/backend/tests/test_3408_pool_ceiling.py @@ -49,3 +49,23 @@ def test_pool_ceiling_covers_declared_concurrency() -> None: f"({_ESTIMATE_CONCURRENCY} оценок + {_SUGGEST_CONCURRENCY} подсказок + " 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}с" + )