FastAPI/Starlette 长连接感知断开
内容提要
FastAPI/Starlette长连接感知断开。之前写过如何在服务器重启时感知长连接,最近发现折腾复杂了。测试代码发现,去掉try...except asyncio.CancelledError,代码也能正常运行,不会报错。但是如果有打开的数据库连接,不会中断也不会回收。加上try...except asyncio.CancelledError,能捕获错误并处理善后。不知道是uvicorn的特性还是ASGI都这样。
延伸解读
取消异常捕获的必要性
文章指出,在流式响应生成器中,即使不捕获 asyncio.CancelledError,代码也能正常运行且不报错。但若生成器内部持有数据库连接等资源,取消时不会自动中断或回收,导致协程挂起成为“僵尸”。因此,显式捕获 CancelledError 并执行清理是必要的,尤其对于长连接场景。
资源泄漏的隐蔽风险
由于 while True 循环持续 yield,当客户端断开时,ASGI 服务器会取消该协程。若不处理 CancelledError,协程可能静默停止,但已打开的数据库连接等资源不会被释放。这种泄漏不易察觉,可能逐渐耗尽连接池,影响服务稳定性。
实现归属尚不明确
作者不确定这种取消行为是 uvicorn 的特性还是 ASGI 的普遍规范。这意味着开发者不能假设所有 ASGI 服务器行为一致,在依赖取消信号进行资源清理时,需针对实际部署环境验证,或查阅 ASGI 规范以确认通用性。
Q&A
FastAPI/Starlette 如何感知长连接断开?
通过捕获 asyncio.CancelledError 异常来感知长连接断开。在异步生成器中添加 try...except asyncio.CancelledError 块,当连接断开时,该异常会被触发,从而执行善后处理。
为什么在 FastAPI/Starlette 中处理长连接断开需要 try...except asyncio.CancelledError?
因为如果不捕获 asyncio.CancelledError,虽然代码可能正常运行且不报错,但如果有打开的数据库连接等资源,它们不会被中断或回收,导致资源泄漏。捕获该异常可以执行清理操作,避免资源僵尸。
去掉 try...except asyncio.CancelledError 后代码还能运行吗?会有什么隐患?
代码仍能正常运行且不报错,但隐患是:如果异步生成器中有打开的数据库连接等资源,由于协程没有被继续消费,这些资源不会中断或回收,会一直挂在进程中成为僵尸。
在 FastAPI/Starlette 中,长连接断开时 asyncio.CancelledError 是如何被触发的?
当客户端断开连接时,服务器(如 uvicorn)会取消正在处理该请求的协程任务,从而在协程内部抛出 asyncio.CancelledError 异常。通过 try...except 捕获该异常即可感知断开。
这种长连接断开感知行为是 uvicorn 特有的还是 ASGI 通用的?
文章作者不确定这是 uvicorn 的特性还是 ASGI 都这样,但测试表明在 uvicorn 下有效。可能依赖于服务器实现,但 ASGI 规范中协程取消是常见模式。
如何简化 FastAPI/Starlette 长连接断开的处理代码?
可以去掉复杂的处理逻辑,只需在异步生成器中使用 try...except asyncio.CancelledError 包裹循环,并在 except 块中执行必要的清理(如关闭数据库连接),无需额外机制。