from collections.abc import AsyncGenerator from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase from app.core.config import settings from app.core.database_tls import ( build_database_connect_args, database_engine_options, normalize_database_url, ) def _build_connect_args() -> dict: """Backward-compatible wrapper around the shared TLS policy.""" return build_database_connect_args(settings.DATABASE_URL) def _normalize_database_url(url: str) -> str: """Strip ONLY the ``ssl`` query param from the SQLAlchemy URL. aiomysql receives SSL via ``connect_args`` (see ``_build_connect_args``); a leftover ``ssl=true`` query param makes the driver choke. The previous ``url.split("?ssl=true")[0]`` only worked when ``ssl=true`` was the sole / trailing query param: - ``?charset=utf8mb4&ssl=true`` → NOT stripped (driver got the bad param) because the literal substring ``?ssl=true`` wasn't present; - ``?ssl=true&charset=utf8mb4`` → silently dropped ``charset`` too. Parse the query string (only the part after the first ``?`` — never touch scheme / userinfo / host / path) and drop only the ``ssl`` key, preserving every other connection param. Behaviour is identical to the old code for the current ``?ssl=true``-alone / no-query formats. """ return normalize_database_url(url) _db_url, _connect_args = database_engine_options(settings.DATABASE_URL) # How long a coroutine waits for a pooled connection before the checkout # raises ``TimeoutError: QueuePool limit ... connection timed out``. # 30 s is SQLAlchemy's default; stated explicitly because the number is a # decision the request-admission logic is sized against, not a default to # be discovered while reading a stack trace: the cost fan-out gate sheds a # read at ``cost_router._COST_FANOUT_ACQUIRE_TIMEOUT_SECONDS`` (20 s), i.e. # BEFORE a queued reader could reach a pool timeout. Any change here has to # stay above that admission timeout, or overload stops surfacing as an # honest 503 on the cost surface and starts surfacing as a pool-timeout 500 # on whatever request happens to check out next — login included. _POOL_TIMEOUT_SECONDS = 30 engine = create_async_engine( _db_url, echo=False, pool_pre_ping=True, pool_size=10, max_overflow=20, pool_timeout=_POOL_TIMEOUT_SECONDS, connect_args=_connect_args, ) async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) class Base(DeclarativeBase): pass async def get_db() -> AsyncGenerator[AsyncSession, None]: async with async_session_factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise