ManageApiClient 单例与守护线程的竞态条件仍需根因修复
背景
PR #3336 已经把守护线程入口(generate_and_save_chat_summary / generate_and_save_chat_title)加了 if not ManageApiClient._instance: return None 判空,缓解了服务关闭时的 NoneType 崩溃。但这只是把窗口缩小,没有从根上消除竞态。
当前残留问题
1. TOCTOU 窗口未消除
async def generate_and_save_chat_summary(session_id: str) -> Optional[Dict]:
if not ManageApiClient._instance: # ← 检查
return None
try:
return await ManageApiClient._instance._execute_async_request( # ← 使用
POST, f/agent/chat-summary/{session_id}/save,
)主线程 safe_close() 仍可能在两行之间把 _instance 置为 None,触发同样的 AttributeError: 'NoneType' object has no attribute '_execute_async_request'。只是触发概率显著降低。
2. 其他同模式函数未覆盖
main/xiaozhi-server/config/manage_api_client.py 中:
| 函数 | 行号 | 现状 |
|---|---|---|
get_server_config |
167 | 裸用,无判空 |
get_agent_models |
176 | 裸用,无判空 |
get_correct_words |
190 | 裸用,仅 try/except |
generate_and_save_chat_summary |
199 | PR #3336 已加判空 |
generate_and_save_chat_title |
211 | PR #3336 已加判空 |
report |
223 | 已有 if not ManageApiClient._instance |
lookup_address_book |
249 | 已有 if not ManageApiClient._instance |
目前这些函数都在同步请求流程中调用、未暴露崩溃,但属于同模式的脆弱代码。
3. safe_close() 不感知守护线程
main/xiaozhi-server/core/connection.py:
connection.py:298—generate_title_task守护线程connection.py:322—save_memory_task守护线程(间接调用generate_and_save_chat_summary)
两条都是 threading.Thread(target=..., daemon=True).start() fire-and-forget,主线程 manage_api_http_safe_close() 直接 ManageApiClient._instance = None,没有任何协调机制。
期望根因方案(任选其一或组合)
方案 A:锁住单例访问
class ManageApiClient:
_instance_lock = threading.Lock()
@classmethod
def safe_close(cls):
with cls._instance_lock:
# ... 关闭 client ...
cls._instance = None
@classmethod
def _get_instance(cls):
with cls._instance_lock:
return cls._instance所有公开入口改用 _get_instance(),把整个调用链放在同一把锁下,避免 TOCTOU。
方案 B:safe_close 等待守护线程退出
- 维护一个
threading.Event或set记录"正在执行 manage api 调用"的守护线程 safe_close()在置_instance = None之前,先join(timeout=...)或等Event置位- 守护线程入口在
safe_close启动后直接return,不再发起新请求
方案 C:补齐剩余函数判空
若评估认为 A/B 改动过大,至少把剩下三个函数也加 if not ManageApiClient._instance: return None,把同模式的脆弱点消除。
验收
- 单元测试:构造
safe_close()在入口与_execute_async_request之间发生的场景(可通过 mock/patch 控制时序),验证不会再抛AttributeError。 - 关闭服务时 daemon 线程不再向 stderr 打印
'NoneType' object has no attribute '_execute_async_request'。
关联
- PR #3336 — 当前的最小修复
main/xiaozhi-server/core/connection.py守护线程启动处
Source: xinnan-tech/xiaozhi-esp32-server