from collections.abc import Generator from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from app.core.config import settings engine = create_engine( settings.database_url, pool_pre_ping=True, future=True, # #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError — # через них в GlitchTip уезжали ключ шифрования кук и сами куки # (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все # сайты вызова разом, включая будущие. # НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением) # и сырые psycopg-подключения мимо движков — это отдельный класс. hide_parameters=True, # #3408 п.1. Потолок пула обязан быть НЕ МЕНЬШЕ суммы потолков одновременности, # которые сам же процесс и объявляет: 4 оценки (`api/v1/trade_in` # `_ESTIMATE_CONCURRENCY`) + 4 подсказки (`api/public/mera` `_SUGGEST_CONCURRENCY`) # + 8 фоновых догрузок (`services/estimator` `_MAX_DEFERRED_REFRESH_TASKS`) = 16. # Дефолт SQLAlchemy 5+10=15 меньше этой суммы, то есть исчерпать пул можно # штатной работой, не абузом. Гейт — tests/test_3408_pool_ceiling.py. # # Инвариант «пул >= суммы объявленных потолков» — это ПОЛ, а не гарантия: # сумма считает по одному коннекту на объявленную единицу работы, а коннект # держит и всё остальное — любая ручка с `Depends(get_db)` тоже. Глобального # обработчика `sqlalchemy.exc.TimeoutError` в app/main.py нет, поэтому на # исчерпанном пуле соседние ручки отдают 500 (после ожидания `pool_timeout`), # а не медленный 200. # # pool_size оставлен дефолтным (5): это ПОСТОЯННО открытые коннекты, а в покое # прод держит 5-6 (замер 11.09). Растёт только overflow — коннекты пика, # которые пул закрывает сам. Потолок процесса: 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) class Base(DeclarativeBase): pass def get_db() -> Generator[Session, None, None]: db = SessionLocal() try: yield db finally: db.close()