"""Пул коннектов не меньше суммы потолков одновременности процесса (#3408 п.1). Процесс сам объявляет, сколько одновременной работы он допускает: 4 оценки, 4 подсказки, 8 фоновых догрузок. Каждая из этих единиц работы держит СВОЮ сессию. Если сумма больше пула, исчерпать пул можно штатной нагрузкой — и упереться не в ресурс, а в `QueuePool limit ... timed out`, причём на публичной ручке. На origin/main сумма 16 против дефолта SQLAlchemy 5+10=15 — тест красный. Гейт нужен не ради текущих чисел, а ради будущих: поднять `_ESTIMATE_CONCURRENCY` или `_MAX_DEFERRED_REFRESH_TASKS`, не поднимая пула, станет видно здесь. Добавится воркер uvicorn (#3083) — числа per-process не меняются, но суммарный потолок коннектов умножается на число воркеров; это отдельное решение, не это. """ from __future__ import annotations import os from typing import Any os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from app.api.public.mera import _SUGGEST_CONCURRENCY from app.api.v1.trade_in import _ESTIMATE_CONCURRENCY from app.core.db import engine from app.services.estimator import _MAX_DEFERRED_REFRESH_TASKS def _max_overflow(pool: Any) -> int: """`max_overflow` у QueuePool публичного геттера не имеет (в отличие от `size()` / `timeout()`), поэтому читаем приватное поле — но с явным падением: молча переименуется в новой SQLAlchemy → гейт бы просто перестал что-либо проверять (AttributeError упал бы ошибкой теста, а вот подмена значения на дефолт — нет). """ assert hasattr(pool, "_max_overflow"), ( f"{type(pool).__name__} больше не хранит `_max_overflow` — SQLAlchemy сменила " "API пула, потолок коннектов читать больше нечем: почини гейт, а не удаляй его" ) return int(pool._max_overflow) def test_pool_ceiling_covers_declared_concurrency() -> None: declared = _ESTIMATE_CONCURRENCY + _SUGGEST_CONCURRENCY + _MAX_DEFERRED_REFRESH_TASKS ceiling = engine.pool.size() + _max_overflow(engine.pool) assert ceiling >= declared, ( f"пул {ceiling} меньше суммы потолков одновременности {declared} " 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}с" )