feat(metrics): метрики доступности БД и занятости пулов БД/Redis
vidconf_db_up проверяется отдельным от основного пула соединением (NullPool, короткий таймаут) — иначе в момент исчерпания пула проверка сама встала бы в очередь и не отличила бы «БД лежит» от «пул занят». vidconf_db_pool_* читаются синхронно из engine.pool, без единого запроса к БД. metrics_endpoint больше не виснет и не падает при недоступном основном пуле: критичные gauge'и считаются первыми и не зависят от него, а vidconf_pipeline_sessions (по-прежнему через Depends(get_session) — тестовый харнесс подменяет её на savepoint-сессию) обёрнут таймаутом и try/except.
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
"""Настройка асинхронного движка 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
|
||||
|
||||
@@ -31,3 +34,45 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user