现场是这样的。脚本里每个请求都 asyncio.run(handle()),引擎在模块级创建,第二次调用开始报 RuntimeError: Task got Future attached to a different loop,环境是 SQLAlchemy 2.0.30 加 asyncpg 0.29,把引擎挪进函数里只能好一次。
中间走过弯路,把「连接池里的连接被跨循环复用」当成主因,改完现象没变。
真正麻烦的地方在于,几个原因的现象几乎一样。引擎绑定了第一次创建时的事件循环;asyncio.run 每次都新建并关闭循环;pytest 的 loop 与 TestClient 用的不是同一个。
上来先用两次 asyncio.run 稳定复现
python -c "import asyncio;asyncio.run(asyncio.sleep(0));asyncio.run(asyncio.sleep(0));print('ok')"
随后改成单入口只 run 一次
python -c "import asyncio;async def main(): await asyncio.gather(*[asyncio.sleep(0) for _ in range(3)]);asyncio.run(main())"
或改用 NullPool 每次新建连接
python -c "from sqlalchemy.pool import NullPool;print(NullPool)"
末了确认连接归还与引擎释放
python check_pool.py --dsn postgresql+asyncpg://u:p@localhost/db --calls 100
改之前先确认了最近有没有人动过配置,避免白查一遍。
别只看报错消失,验证要多做一步。连续调用 100 次不再出现 different loop,连接数稳定在池上限内,退出时没有待关闭警告。
提醒一点。NullPool 只是绕过问题,长期方案是把事件循环和引擎的生命周期对齐,别在两个循环之间共享池。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。
