vidconf_db_up проверяется отдельным от основного пула соединением (NullPool, короткий таймаут) — иначе в момент исчерпания пула проверка сама встала бы в очередь и не отличила бы «БД лежит» от «пул занят». vidconf_db_pool_* читаются синхронно из engine.pool, без единого запроса к БД. metrics_endpoint больше не виснет и не падает при недоступном основном пуле: критичные gauge'и считаются первыми и не зависят от него, а vidconf_pipeline_sessions (по-прежнему через Depends(get_session) — тестовый харнесс подменяет её на savepoint-сессию) обёрнут таймаутом и try/except.
79 lines
3.9 KiB
Python
79 lines
3.9 KiB
Python
"""Настройка асинхронного движка 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()
|