问题描述
SDK 的异步调用路径中直接调用了基于同步 requests 且未设置 timeout 的方法。当到 api.dingtalk.com 的网络是"挂起"状态(TCP 半开,不报错也不返回)时,requests 会阻塞到 OS 层 TCP 超时(分钟级),导致整个 asyncio 事件循环冻结——进程内所有协程停止调度,且同步代码没有 await 点,task.cancel() 无法生效,进程表现为假死,只能强杀。
具体位置
1. DingTalkStreamClient.start() → open_connection()(dingtalk_stream/stream.py)
# stream.py:65 — async def start() 的事件循环里直接同步调用
connection = self.open_connection()
# stream.py:172 — open_connection() 内部,同步 requests 且无 timeout
response = requests.post(DingTalkStreamClient.OPEN_CONNECTION_API,
headers=request_headers,
data=request_body)
2. 各 async_* 卡片方法 → get_access_token()(card_replier.py → stream.py)
# card_replier.py — async_create_and_send_card / async_streaming /
# async_put_card_data 等异步方法开头均为:
access_token = self.dingtalk_client.get_access_token()
# stream.py:217 — get_access_token() 内部,同样是无 timeout 的 requests.post
response = requests.post(url, headers=request_headers, data=json.dumps(values))
值得注意的是,卡片接口都有 aiohttp 实现的异步双版本(streaming/async_streaming 等),唯独 open_connection 和 get_access_token 没有异步版本,形成了"异步链路中的同步阻塞点"。
影响
正常情况下无感(get_access_token 有约 2 小时缓存,open_connection 仅在(重)连时调用)。但触发点恰好都是服务最脆弱的时刻:启动建连、断线重连、token 过期刷新。一旦此时网络异常挂起,整个进程(包括与钉钉无关的其它协程)冻结数分钟,重连机制本身也失效。
复现
import asyncio
from dingtalk_stream import Credential, DingTalkStreamClient
async def main():
client = DingTalkStreamClient(Credential("fake", "fake"))
task = asyncio.create_task(client.start())
await asyncio.sleep(1)
task.cancel()
# 网络不通时 cancel 无法打断阻塞中的 requests.post,这里超时
await asyncio.wait_for(task, timeout=5)
asyncio.run(main())
在无法访问 api.dingtalk.com 的环境(或将该域名指向黑洞地址)下运行即可复现。
建议
- 为
open_connection / get_access_token 增加异步版本(SDK 已依赖 aiohttp),异步调用点切换过去;或最低成本地在调用点用 await asyncio.to_thread(...) 包装;
- 为所有
requests / aiohttp 调用设置显式 timeout。
环境
- dingtalk-stream 0.24.3
- Python 3.11,Windows 11(与平台无关,Linux 同样存在)
问题描述
SDK 的异步调用路径中直接调用了基于同步
requests且未设置timeout的方法。当到 api.dingtalk.com 的网络是"挂起"状态(TCP 半开,不报错也不返回)时,requests会阻塞到 OS 层 TCP 超时(分钟级),导致整个 asyncio 事件循环冻结——进程内所有协程停止调度,且同步代码没有 await 点,task.cancel()无法生效,进程表现为假死,只能强杀。具体位置
1.
DingTalkStreamClient.start()→open_connection()(dingtalk_stream/stream.py)2. 各
async_*卡片方法 →get_access_token()(card_replier.py→stream.py)值得注意的是,卡片接口都有 aiohttp 实现的异步双版本(
streaming/async_streaming等),唯独open_connection和get_access_token没有异步版本,形成了"异步链路中的同步阻塞点"。影响
正常情况下无感(
get_access_token有约 2 小时缓存,open_connection仅在(重)连时调用)。但触发点恰好都是服务最脆弱的时刻:启动建连、断线重连、token 过期刷新。一旦此时网络异常挂起,整个进程(包括与钉钉无关的其它协程)冻结数分钟,重连机制本身也失效。复现
在无法访问 api.dingtalk.com 的环境(或将该域名指向黑洞地址)下运行即可复现。
建议
open_connection/get_access_token增加异步版本(SDK 已依赖 aiohttp),异步调用点切换过去;或最低成本地在调用点用await asyncio.to_thread(...)包装;requests/aiohttp调用设置显式timeout。环境