"""Per-invocation async session factory for Celery task modules. Async engine connections are bound to the event loop. Each Celery task runs its async body under a fresh ``asyncio.run()`` loop, so it needs its own engine created (and disposed) within that loop — a process-wide async engine would reuse loop-bound connections across tasks and raise "attached to a different loop". So unlike the process-wide sync engine in ``_sync_engine.py``, this returns a fresh engine per call; the caller disposes it (``await engine.dispose()``) when its loop ends. """ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from ..config import get_config def async_session_factory(*, server_settings: dict | None = None): """Return ``(sessionmaker, engine)`` bound to a fresh async engine. ``server_settings`` (optional) are applied by asyncpg as per-connection GUCs on connect. Because NullPool opens a fresh real connection per checkout, every transaction in the task inherits them — used to set ``lock_timeout`` so a statement blocked on a lock fails fast instead of hanging to the Celery hard limit (operator-flagged normalize_tags timeout 2026-06-07). """ cfg = get_config() # NullPool: this engine lives for ONE task (created + disposed per # asyncio.run loop), so intra-task connection pooling buys nothing and # actively bit us — download_source releases its phase-1 connection # before a multi-minute gallery-dl subprocess, and a *pooled* idle # connection would be reaped by the server and handed back dead to # phase 3 (asyncpg ConnectionDoesNotExistError, Anduo #40014). NullPool # opens a fresh real connection on each checkout, so phase 3 always # reconnects clean; pre_ping is then redundant. connect_args = {} if server_settings: connect_args["server_settings"] = { k: str(v) for k, v in server_settings.items() } engine = create_async_engine( cfg.database_url, future=True, poolclass=NullPool, connect_args=connect_args, ) return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False), engine