WebSocket handler必须用async def,且Motor操作需在同事件循环中await;禁用to_list(None)、避免复用collection实例、连接字符串须含retryWrites=true。

WebSocket handler 必须用 async def,且不能直接 await Motor 操作
FastAPI 的 WebSocket endpoint 本身是异步的,但它的生命周期和 HTTP route 不同:没有 Depends 自动注入、不走中间件链、不支持常规依赖缓存。如果你在 websocket.accept() 后直接 await collection.find_one(...),会报 RuntimeError: Task attached to a different loop——因为 WebSocket 的协程可能被调度到非主事件循环线程(尤其在 uvicorn 多 worker 模式下)。
关键约束:
-
WebSocket实例不共享 FastAPI 的依赖注入上下文,Depends(get_db)在这里无效 - Motor 客户端必须是全局单例,且已通过
@app.on_event("startup")初始化完毕 - 所有数据库操作仍需
await,但必须确保它们运行在同一个 event loop 中——这意味着你不能在while True:循环里混用同步阻塞调用(比如time.sleep)
正确写法示例:
from fastapi import WebSocket, WebSocketDisconnect
from motor.motor_asyncio import AsyncIOMotorClient
<h1>全局 client,已在 startup 中初始化</h1><p>client = AsyncIOMotorClient("mongodb://localhost:27017")
db = client["notify_db"]
collection = db["events"]</p><p>@app.websocket("/ws/notifications")
async def websocket_notifications(websocket: WebSocket):
await websocket.accept()
try:
while True:</p><h1>✅ 正确:await Motor 查询,且在同一个协程中</h1><pre class="brush:php;toolbar:false;"> latest = await collection.find_one(
{"processed": False},
sort=[("created_at", -1)]
)
if latest:
await collection.update_one(
{"_id": latest["_id"]},
{"$set": {"processed": True}}
)
await websocket.send_json({"event": latest})
await asyncio.sleep(2) # ✅ 非阻塞等待
except WebSocketDisconnect:
pass
Motor 游标不能在 async for 外转成 list,否则 WebSocket 会卡住
WebSocket 场景下常见错误是想“一次性拉取所有未读通知”,然后用 for 推送。但 collection.find({"status": "pending"}) 返回的是 AsyncIOMotorCursor,若你写 list(cursor) 或 await cursor.to_list(length=None),在数据量稍大(比如上千条)时,会把整个结果集加载进内存,阻塞 WebSocket 协程,导致心跳超时断连。
更危险的是:如果管道里有 $lookup 或 $facet,而关联集合没建索引,查询延迟飙升,WebSocket 连接会在 await cursor.to_list(...) 这一步挂死数秒。
推荐做法:
- 用
async for doc in cursor.limit(50)流式推送,每条都await websocket.send_json(...) - 对每个
doc单独做await collection.update_one(...)标记已推送,避免重复 - 绝对不要在 WebSocket handler 里调用
to_list(length=None),哪怕加了 timeout 也不保险
多客户端连接时,别复用同一个 AsyncIOMotorClient 实例做状态绑定
有人会尝试在 WebSocket handler 里缓存一个“用户专属 collection 引用”,比如:
# ❌ 错误示范:把 collection 绑定到连接实例上
user_collection = db[f"notifications_{user_id}"] # 全局变量 or 类属性
这看似方便,实则危险:Motor 的 AsyncIOMotorDatabase 和 AsyncIOMotorCollection 对象本身无状态,但如果你在不同 WebSocket 连接间共享了同一 collection 实例,并发写入时可能因未加锁导致 update_one 覆盖彼此的 $set 字段;更隐蔽的问题是,某些 MongoDB 驱动版本在高并发下会复用游标 ID,造成数据错乱。
安全做法是每次需要时都重新获取 collection:
- 用
db.get_collection(f"notifications_{user_id}")动态获取(轻量,无网络开销) - 或封装为函数:
def get_user_collection(user_id: str) -> AsyncIOMotorCollection:,在 handler 内调用 - 禁止把
collection存为WebSocket实例的属性,或模块级变量
连接字符串末尾漏掉 ?retryWrites=true,WebSocket 会静默丢消息
这是最容易被忽略的配置项。Motor 默认开启 retryWrites,但仅当连接字符串显式声明才生效。如果漏掉,insert_one 或 update_one 在网络抖动时可能返回成功,实际却没写入——WebSocket 已经把“已发送”状态推给前端,后端日志也显示 result.acknowledged == True,但 MongoDB 日志里查不到对应文档。
验证方式:
- 连接字符串必须形如:
"mongodb://localhost:27017/mydb?retryWrites=true" - 检查
client.options.retry_writes是否为True(注意不是client.options.retry_reads) - 在
startup里加一次await client.admin.command("ping"),确认连接可通
WebSocket 场景下这种失败不可重试,因为连接是长时的,没有“下一次请求”来兜底。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











