fix: isolate feishu websocket event loops

This commit is contained in:
jxxghp
2026-08-24 12:54:06 +08:00
parent 9fede7fb2d
commit af9a141f34
3 changed files with 128 additions and 12 deletions
+62 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import threading
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
@@ -9,7 +10,7 @@ ensure_optional_stub("psutil")
ensure_optional_stub("dateparser")
ensure_optional_stub("Pinyin2Hanzi", is_pinyin=lambda value: False)
from app.modules.feishu.feishu import Feishu
from app.modules.feishu.feishu import Feishu, lark_ws_client_module
def _build_feishu_client() -> Feishu:
@@ -30,6 +31,66 @@ async def _wait_forever() -> None:
await asyncio.Future()
def test_parallel_ws_clients_keep_independent_sdk_event_loops() -> None:
"""多个飞书配置并发启动时不得覆盖彼此的 SDK 事件循环。"""
clients = [_build_feishu_client(), _build_feishu_client()]
barrier = threading.Barrier(2)
constructed = threading.Event()
construction_lock = threading.Lock()
fake_clients = []
class _ConcurrentWsClient:
"""模拟真实 SDK 通过模块级 loop 与 _select 驱动长连接。"""
def __init__(self, *_args, **_kwargs):
"""登记实例并准备关停路径需要的 SDK 私有状态。"""
self._auto_reconnect = True
self._conn = None
self._conn_url = ""
self._conn_id = ""
self._service_id = ""
self._lock = asyncio.Lock()
self.started = threading.Event()
self.observed_loop = None
with construction_lock:
fake_clients.append(self)
if len(fake_clients) == 2:
constructed.set()
def start(self) -> None:
"""强制两个线程同时解析 SDK 全局,再等待各自停止信号。"""
barrier.wait(timeout=2)
async def run_until_stopped() -> None:
"""记录真实运行循环,并调用 SDK 的模块级阻塞选择。"""
self.observed_loop = asyncio.get_running_loop()
self.started.set()
await lark_ws_client_module._select()
lark_ws_client_module.loop.run_until_complete(run_until_stopped())
with patch(
"app.modules.feishu.feishu.lark.ws.Client",
_ConcurrentWsClient,
):
try:
for client in clients:
client._start_ws_client()
assert constructed.wait(timeout=2)
assert all(fake.started.wait(timeout=2) for fake in fake_clients)
observed_loops = [fake.observed_loop for fake in fake_clients]
assert len(set(observed_loops)) == 2
assert all(loop is not None for loop in observed_loops)
finally:
for client in clients:
client.stop()
assert all(
client._ws_thread is None or not client._ws_thread.is_alive()
for client in clients
)
def test_shutdown_ws_client_cancels_sdk_tasks_before_quiet_disconnect():
"""飞书关机清理应先消费后台任务,再静默关闭 WebSocket 连接。"""
client = _build_feishu_client()