asyncio、pytest与SQLAlchemy交互的测试异常问题咨询
我有一个Python 3.8程序,运行无限循环的协程,通过create_task调度后台协程(不等待),这些协程使用SQLAlchemy的AsyncEngine写入数据库。触发异常时,会取消循环协程并通过gather等待剩余后台任务,程序运行正常。
使用pytest和pytest-asyncio编写测试用例,通过打桩强制触发异常以执行gather逻辑。单个测试可通过,批量运行时先出现SQLAlchemy错误:cannot perform operation: another operation is in progress;关闭连接池后该错误消失,但批量运行过多测试时会超时,卡在await asyncio.gather(*outstanding_tasks),剩余任务处于pending状态。
疑问:
- 为何测试会共享
AsyncEngine或其连接池?是否与同进程运行有关? - 为何大量测试时部分任务无法调度?即使调高超时时间也无效。
注:测试用例为参数化,行为非确定性,偶尔全部测试可通过。
1. 测试共享AsyncEngine/连接池的原因
是的,这完全和pytest的同进程运行机制有关。pytest默认在同一个Python进程中运行所有测试用例,如果你的AsyncEngine是全局初始化的(比如模块级变量、单例模式),所有测试都会复用同一个引擎实例及其连接池。
SQLAlchemy的AsyncEngine连接池本身是协程安全的,但批量测试时,前一个测试未完全关闭的连接可能被后一个测试复用,当连接还处于未完成的操作状态时,就会抛出cannot perform operation: another operation is in progress错误。即使手动关闭连接池,如果测试结束时没有彻底清理引擎实例(比如未调用await engine.dispose()),残留的连接状态依然会影响后续测试。
另外,pytest-asyncio默认会为每个测试创建新的事件循环,但如果引擎跨测试共享,不同事件循环中的协程尝试复用同一个连接池,也会导致冲突。
2. 大量测试时任务无法调度的原因
这种pending状态通常是因为后台协程被挂起且无法被唤醒,常见场景包括:
- 连接池资源耗尽:即使关闭了连接池,如果测试中创建的后台协程持有未释放的数据库连接,连接池关闭后这些协程会一直等待获取连接,永远无法完成,导致
gather持续阻塞。 - 事件循环清理不彻底:pytest-asyncio批量测试时,可能存在事件循环残留任务的情况。前一个测试的未取消任务占用了事件循环的调度资源,导致新测试的后台任务无法被调度执行。
- 参数化测试的并发冲突:参数化测试会快速创建大量测试实例,如果每个测试都生成大量后台任务,事件循环的调度队列可能被占满,部分任务无法及时调度;加上任务本身持有数据库连接等资源,就会陷入永久等待。
- 协程取消处理不当:强制触发异常并取消循环协程时,如果后台协程没有正确捕获
asyncio.CancelledError,可能会处于半终止状态,既不完成也不抛出异常,导致gather无法结束。
针对性建议
- 每个测试用例独立初始化
AsyncEngine,测试结束后立即调用await engine.dispose()彻底销毁引擎和连接池,避免跨测试共享。 - 使用
pytest.fixture的scope="function"级别管理引擎实例,确保每个测试拿到全新的引擎。 - 后台协程中添加
try/except捕获CancelledError,确保协程能正确退出,避免残留pending状态。 - 批量测试时,限制每个测试的后台任务数量,或者在测试结束前主动取消所有未完成的后台任务,再执行
gather等待。
内容的提问来源于stack exchange,提问作者danben

