数据自愈超时机制失效分析claude 2
数据自愈超时机制失效分析
日期:2026-02-23
问题:HEALING_TIMEOUT_SECONDS=300 无效,服务启动时数据自愈运行 ~19 分钟(498 对配对)
一、现象
09:20:16 - 数据自愈启动 | 498 个配对 | timeout=300s
09:25:16 - app - ERROR - 数据库连接错误: 数据自愈超时 (300秒) ← 超时触发了,但...
09:25:16 - orchestrator - ERROR - 加载历史数据失败 - 数据库错误: 数据自愈超时
09:39:30 - 数据自愈仍在继续处理第 340+ 对 ← 19分钟后还未停止
预期:300s 后打印 "数据自愈超时,已完成 N 个 symbol" 并退出。
实际:报错一次后继续运行所有剩余配对。
二、机制原理
healing_timeout 使用 Unix signal.alarm():
# orchestrator.py:76
@contextmanager
def healing_timeout(seconds: int):
sigalrm = getattr(signal, "SIGALRM", None)
if sigalrm is not None:
def timeout_handler(signum, frame):
raise TimeoutError(f"数据自愈超时 ({seconds}秒)")
signal.signal(sigalrm, timeout_handler)
signal.alarm(seconds) # 一次性闹钟,只触发一次
try:
yield
finally:
signal.alarm(0)
关键约束:signal.alarm() 是一次性的——超时只触发一次 TimeoutError。如果这次 TimeoutError 被内层代码吞掉,后续代码永远不会再超时。
三、根本原因
Python 内置 TimeoutError 的继承链:
TimeoutError → OSError → Exception → BaseException
TimeoutError 是 Exception 的子类,因此所有 except Exception 都会捕获它。
被吞掉的完整调用链
healing_timeout(300) 触发 → TimeoutError 抛出
↓
_load_zscore_history (orchestrator.py:511)
except Exception as e: ← ① 捕获 TimeoutError
logger.error("加载历史数据失败 - 数据库错误")
raise RuntimeError("加载历史数据失败: ...") from e ← TimeoutError 被包装为 RuntimeError
↓
heal_and_prepare (orchestrator.py:212)
except RuntimeError as e: ← ② 捕获包装后的 RuntimeError
logger.error("数据自愈失败(数据库/SQL错误)")
return HealingResult(status='failed') ← 正常返回,不抛出异常
↓
_run_data_healing for 循环继续下一对配对 ← ③ TimeoutError 从未到达外层
外层 except TimeoutError (line 1863) ← 永远不会执行
备用触发路径(TimeoutError 在其他位置触发时)
| 位置 | 方法 | 行为 |
|---|---|---|
repair_executor.py:183 |
_repair_from_klines |
except Exception → return 0 |
repair_executor.py:299 |
_extract_kline_window |
except Exception → return None |
repair_executor.py:345 |
_compute_zscore |
except Exception → return None |
以上任意一处捕获后,signal.alarm 的一次性触发机会耗尽,剩余配对永远不会超时。
四、影响
| 指标 | 预期 | 实际 |
|---|---|---|
| 数据自愈耗时 | ≤300s | ~1140s(19 分钟) |
| 服务启动延迟 | ~5 分钟 | ~20 分钟 |
| 超时日志 | 300s 后打印已完成数量 | 打印一次错误后继续 |
| WebSocket 启动 | ~5 分钟后 | ~20 分钟后 |
五、修复方案
原则:让系统级中断(TimeoutError、KeyboardInterrupt)穿透所有内层 except Exception,不增加新抽象。
修复模式(在每个受影响的 except Exception 之前添加):
except (TimeoutError, KeyboardInterrupt):
raise # 直接穿透,不处理
except Exception as e:
# 原有逻辑不变
...
需修改的位置(共 5 处)
src/utils/data_healing/orchestrator.py
# 行 511 — _load_zscore_history
# 修改前
except Exception as e:
logger.error(f"加载历史数据失败 - 数据库错误: {e}", exc_info=True)
raise RuntimeError(f"加载历史数据失败: {e}") from e
# 修改后
except (TimeoutError, KeyboardInterrupt):
raise
except Exception as e:
logger.error(f"加载历史数据失败 - 数据库错误: {e}", exc_info=True)
raise RuntimeError(f"加载历史数据失败: {e}") from e
src/utils/data_healing/repair_executor.py
# 行 183 — _repair_from_klines
except (TimeoutError, KeyboardInterrupt):
raise
except Exception as e:
logger.error(f"Level 1修复失败: {e}", exc_info=True)
return 0
# 行 299 — _extract_kline_window
except (TimeoutError, KeyboardInterrupt):
raise
except Exception as e:
logger.debug(f"提取窗口失败: {e}")
return None
# 行 345 — _compute_zscore
except (TimeoutError, KeyboardInterrupt):
raise
except Exception as e:
logger.debug(f"zscore计算失败: {e}")
return None
src/services/realtime_kline_service_base.py
# 行 1860 — _run_data_healing for 循环
except (TimeoutError, KeyboardInterrupt):
raise
except Exception as e:
failed += 1
self.logger.warning(f"自愈异常: {symbol} | {e}")
六、修复后行为
09:20:16 - 数据自愈启动 | 498 个配对 | timeout=300s
...(处理约 150-200 对)...
09:25:16 - ⚠️ 数据自愈超时 (300s),已完成 N 个 symbol ← 正确退出
09:25:17 - 订阅数量: 行情=687 交易=3
09:25:17 - ✅ 实时K线分析服务初始化完成
09:25:20 - 增强型WebSocket管理器初始化完成 ← WebSocket 正常启动
服务启动时间从 ~20 分钟缩短至 ~5 分钟。
七、注意事项
signal.alarm仅 Unix/Linux/macOS 有效,Windows 下SIGALRM=None,无超时保护(现有行为不变)repair_executor.py:370(_insert_records)的except Exception末尾已有raise,无需修改- 修复不影响正常自愈流程,仅改变异常穿透路径