ManageApiClient 单例与守护线程的竞态条件仍需根因修复

Author: openrzCreated Sep 2, 2026Updated Sep 2, 2026
Labelsenhancement

背景

PR #3336 已经把守护线程入口(generate_and_save_chat_summary / generate_and_save_chat_title)加了 if not ManageApiClient._instance: return None 判空,缓解了服务关闭时的 NoneType 崩溃。但这只是把窗口缩小,没有从根上消除竞态。

当前残留问题

1. TOCTOU 窗口未消除

python
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:298generate_title_task 守护线程
  • connection.py:322save_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:锁住单例访问

python
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.Eventset 记录"正在执行 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