数据自愈超时机制失效分析claude 4
数据自愈模块修复执行报告
日期:2026-02-23
提交范围:src/utils/data_healing/ × 2 文件 + src/services/realtime_kline_service_base.py
变更统计:+50 行 / −111 行,净减少 61 行
一、问题背景
服务启动时数据自愈运行约 19 分钟(498 对配对),HEALING_TIMEOUT_SECONDS=300 完全无效。
根本原因:signal.alarm() 触发的 TimeoutError 是 Exception 的子类(继承链:TimeoutError → OSError → Exception)。闹钟一次性触发后,只要有任意一个 except Exception 先捕获,剩余迭代就失去超时保护,进程永不终止。
同期审计发现数据自愈模块存在大量冗余设计,一并清理。
二、修复项目
2.1 超时穿透修复(P0)
修复模式:在每个内层 except Exception 之前插入 except (TimeoutError, KeyboardInterrupt): raise,让系统级中断穿透所有内层捕获,直达外层 except TimeoutError。
修改位置(共 5 处):
| 文件 | 方法 | 原行为 |
|---|---|---|
orchestrator.py:511 |
_load_zscore_history |
except Exception 将 TimeoutError 包装为 RuntimeError,被上层 except RuntimeError 捕获后静默返回,循环继续 |
repair_executor.py:183 |
_repair_from_klines |
except Exception 捕获后 return 0,循环继续 |
repair_executor.py:301 |
_extract_kline_window |
except Exception 捕获后 return None,当前 missing_time 跳过,循环继续 |
repair_executor.py:349 |
_compute_zscore(已内联) |
同上,return None |
realtime_kline_service_base.py:1860 |
_run_data_healing for 循环 |
except Exception 兜底捕获,TimeoutError 永远无法到达外层 except TimeoutError |
修复后调用链:
signal.alarm(300) 触发 → TimeoutError 抛出
↓ 穿透所有内层 except Exception(因为先有 except (TimeoutError, KeyboardInterrupt): raise)
外层 except TimeoutError(realtime_kline_service_base.py:1863)
→ 打印 "数据自愈超时 (300s),已完成 N 个 symbol"
→ 立即返回,进入正常 WebSocket 启动流程
效果:服务启动时间从 ~19 分钟缩短为 ~5 分钟。
2.2 冗余字段清理(P1)
Diagnosis dataclass — 删除 2 个未读字段
| 字段 | 问题 | 处理 |
|---|---|---|
is_continuous: bool |
_diagnose() 设置后从未被读取;_final_assessment() 忽略该值,重新调用 checker.check_continuity() |
删除 |
staleness_minutes: float |
_diagnose() 设置后从未被读取 |
删除 |
保留字段:is_healthy、gap_targets、stale_targets、shortfall_targets、completeness_pct、record_count(均被 heal_and_prepare() 实际读取)
HealingResult dataclass — 删除 2 个未读字段
| 字段 | 问题 | 处理 |
|---|---|---|
warmup_mode: bool |
完全复制 quality.warmup_required;调用方(_run_data_healing)仅读取 result.status 和 result.quality.completeness_pct,从未访问此字段 |
删除 |
repair_summary: dict |
仅在内部追加迭代记录,调用方从未读取;HealingResult.__str__() 也不包含此字段 |
删除 |
保留字段:status、data、quality、iterations_used
repair_summary 内部跟踪逻辑 — 整体删除
随 HealingResult.repair_summary 字段删除,同步清理 heal_and_prepare() 中:
repair_summary = {'iterations': [], 'total_repaired': 0, 'final_status': 'unknown'}初始化repair_summary['iterations'].append({...})迭代追加repair_summary['total_repaired'] += repaired_count累计repair_summary['final_status'] = status/'failed'设置_final_assessment()的repair_summary: dict参数
净删除:~15 行死代码
2.3 转发方法内联(P1)
将 repair_executor.py 中 4 个"只包装一个外部调用"的方法内联到 _repair_from_klines():
| 方法 | 原行数 | 调用次数 | 处理 |
|---|---|---|---|
_compute_zscore() |
17 | 1 | 内联;except Exception 同步改为 fail_count += 1; continue,消除 return None + 外层 if zscore is None 双重判断 |
_compute_correlation() |
11 | 1 | 内联 |
_build_analysis_record() |
26 | 1 | 内联;record = ... 中间变量同步消除 |
_insert_records() |
12 | 1 | 内联;原方法 raise 后会被外层 except Exception 二次捕获并记录,产生双重日志,内联后修复 |
RepairExecutor 方法数:11 → 7(repair、_repair_from_klines、_find_kline_gaps、_fill_kline_gaps、_generate_complete_timeline、_extract_kline_window、__init__)
附带修复:_insert_records 原设计为"记录错误 + re-raise",但外层 except Exception 捕获后再次记录,造成同一次 batch_insert 失败打印两条错误日志。内联后统一由一个 except Exception 处理,日志去重。
三、变更统计
| 修改文件 | +行 | −行 | 净变化 |
|---|---|---|---|
src/utils/data_healing/orchestrator.py |
2 | 36 | −34 |
src/utils/data_healing/repair_executor.py |
44 | 79 | −35 |
src/services/realtime_kline_service_base.py |
2 | 0 | +2 |
| 合计 | 48 | 115 | −67 行 |
方法数变化:
| 模块 | 修改前 | 修改后 |
|---|---|---|
RepairExecutor |
11 | 7(−4) |
DataHealingOrchestrator |
16 | 16(不变;_final_assessment 签名简化) |
dataclass 字段数变化:
| 类 | 修改前 | 修改后 |
|---|---|---|
Diagnosis |
8 | 6(−2) |
HealingResult |
6 | 4(−2) |
四、行为变化对照
超时行为
| 场景 | 修复前 | 修复后 |
|---|---|---|
_load_zscore_history 触发 TimeoutError |
包装为 RuntimeError → 静默返回,循环继续 |
直接穿透到外层 except TimeoutError |
_repair_from_klines 触发 TimeoutError |
return 0,循环继续 |
穿透 |
_extract_kline_window 触发 TimeoutError |
return None,当前配对继续 |
穿透 |
for 循环内任意位置触发 TimeoutError |
except Exception 兜底,消化 |
穿透 |
| 服务启动耗时 | ~19 分钟(498 对全量) | ~5 分钟(约 150~200 对后超时) |
日志行为
| 场景 | 修复前 | 修复后 |
|---|---|---|
batch_insert 失败 |
"批量写入失败" + "Level 1修复失败"(双条) | "批量写入失败"(单条) |
| 超时退出 | 打印一次数据库错误,继续运行 | "数据自愈超时 (300s),已完成 N 个 symbol" |
正常自愈流程
无变化。所有修改均为异常路径或死字段,正常处理路径逻辑完全一致。
五、验证
python -c "import ast; ast.parse(open('src/utils/data_healing/orchestrator.py').read()); print('OK')"
# → OK
python -c "import ast; ast.parse(open('src/utils/data_healing/repair_executor.py').read()); print('OK')"
# → OK
python -c "
from src.utils.data_healing.orchestrator import Diagnosis, HealingResult
from dataclasses import fields
print([f.name for f in fields(Diagnosis)])
# → ['is_healthy', 'gap_targets', 'stale_targets', 'shortfall_targets', 'completeness_pct', 'record_count']
print([f.name for f in fields(HealingResult)])
# → ['status', 'data', 'quality', 'iterations_used']
"
grep -c "except (TimeoutError, KeyboardInterrupt)" \
src/utils/data_healing/orchestrator.py \
src/utils/data_healing/repair_executor.py \
src/services/realtime_kline_service_base.py
# → 1 / 3 / 1 = 5 处
六、遗留事项
本次未执行(中优先级,需测试验证):
completeness_pct重复计算:ContinuityChecker.check_continuity()与QualityAssessor.assess()各自独立计算同一指标,调用链上每轮执行两次。合并路径:由check_continuity()返回,assess()直接复用。_final_assessment()重复评估:_diagnose()计算的is_healthy未被复用,_final_assessment()重新调用整个评估流程。可让_final_assessment()接收最终诊断结果而非重算。Diagnosis中间层:Diagnosis整体可考虑拆解,将三类 target 列表直接合并后传给executor.repair(),消除 ~110 行中间层。- SRP 违反:
DataHealingOrchestrator(557 行 / 16 方法)混合编排、诊断、目标生成、数据加载、评估 5 个职责;RepairExecutor仍混合 K 线管理与 zscore 计算。
详见 数据自愈模块冗余分析.md。