21c1b0a81c
download/migration/scan each defined an identical _async_session_factory() (fresh per-invocation async engine — async connections are event-loop-bound so each asyncio.run() task needs its own engine, unlike the process-wide _sync_engine). Moved it to tasks/_async_session.py; the 3 files import it and drop their now-orphaned sqlalchemy.ext.asyncio / get_config imports (migration keeps AsyncSession for a type hint). Call-site try/finally dispose left as-is to avoid re-indenting the critical task bodies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 lines
988 B
Python
22 lines
988 B
Python
"""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 ..config import get_config
|
|
|
|
|
|
def async_session_factory():
|
|
"""Return ``(sessionmaker, engine)`` bound to a fresh async engine."""
|
|
cfg = get_config()
|
|
engine = create_async_engine(cfg.database_url, future=True, pool_pre_ping=True)
|
|
return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False), engine
|