"""Настройка асинхронного движка SQLAlchemy и сеанса.""" from collections.abc import AsyncGenerator from typing import cast from sqlalchemy import text from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine, ) from sqlalchemy.pool import NullPool, QueuePool from core.config import get_settings settings = get_settings() engine: AsyncEngine = create_async_engine( settings.database_url, pool_pre_ping=True, # Параметры пула — в настройках (`core/config.py`, там же расчёт бюджета # соединений на инстанс). Дефолт SQLAlchemy 5 + 10 под нагрузкой # выгребался за секунды. pool_size=settings.db_pool_size, max_overflow=settings.db_max_overflow, pool_timeout=settings.db_pool_timeout, ) async_session_maker = async_sessionmaker(engine, expire_on_commit=False) async def get_session() -> AsyncGenerator[AsyncSession, None]: """Зависимость FastAPI, возвращающая `AsyncSession`.""" async with async_session_maker() as session: yield session # --- Проверка доступности БД вне основного пула (сессия 33) ---------------- # # Отдельный движок с `NullPool`: каждый вызов открывает новое соединение и # закрывает его сразу после — бюджет соединений не пересекается с # `engine.pool` (10 + 10 overflow × число воркеров uvicorn). Это единственный # способ отличить «БД лежит» от «основной пул занят под нагрузкой»: проверка # через `get_session()` в момент исчерпания пула сама встала бы в очередь на # `db_pool_timeout` и не смогла бы ответить, пока не появится случайно # освободившееся место — то есть не отличила бы два принципиально разных # состояния. Короткий `timeout` на соединение (не путать с `db_pool_timeout` # основного пула) — чтобы зависший, а не оборванный TCP (Postgres отвечает, # но не может продвинуться) не держал проверку до дефолтных 60 секунд asyncpg. _probe_engine: AsyncEngine = create_async_engine( settings.database_url, poolclass=NullPool, connect_args={"timeout": settings.db_probe_timeout_s}, ) async def check_db_up() -> bool: """`True`, если БД отвечает на `SELECT 1` по отдельному от основного пула соединению.""" try: async with _probe_engine.connect() as connection: await connection.execute(text("SELECT 1")) except Exception: # noqa: BLE001 return False return True def db_pool_checked_out() -> int: """Число соединений основного пула, занятых прямо сейчас — без обращения к БД. SQLAlchemy держит счётчик в памяти самого объекта пула (`engine.pool`), поэтому его можно прочитать в любой момент, даже когда все соединения заняты или БД недоступна — именно это нужно алерту на исчерпание пула (метрика не должна зависеть от того, что измеряет). Размер и лимит overflow — конфигурация (`Settings.db_pool_size`/`db_max_overflow`), их не нужно снимать с объекта пула отдельно. """ return cast(QueuePool, engine.pool).checkedout()