fix(monitor): 规则表并发迭代加快照, 防评估线程与 API 线程竞态

evaluate()/has_rule_type() 跑在行情轮询线程, 直接迭代 self._rules
时若 API 线程并发 add_rule/remove_rule 会抛 RuntimeError
(dictionary changed size during iteration), 该轮全部告警被上层
except 吞掉静默丢失。统一 list() 快照后迭代; set_rules 改为构建
完整 dict 后原子替换, 避免读到半装载状态; remove_rule 重建
_last_fire 同理。
This commit is contained in:
Gundy
2026-07-03 14:11:26 +08:00
committed by shy3130
parent 3f17e58908
commit 4e332c42a6
+15 -7
View File
@@ -350,11 +350,16 @@ class MonitorRuleEngine:
# ── 规则管理 ───────────────────────────────────────
def set_rules(self, rules: list[dict]) -> None:
"""批量设置规则 (覆盖)。用于启动时 reload。"""
self._rules = {}
"""批量设置规则 (覆盖)。用于启动时 reload。
先构建完整 dict 再原子替换 self._rules, 避免评估线程 (行情轮询)
读到装载了一半的规则表。
"""
new_rules: dict[str, dict] = {}
for r in rules:
if r.get("enabled") is not False:
self._rules[r["id"]] = r
new_rules[r["id"]] = r
self._rules = new_rules
logger.info("MonitorRuleEngine: 装载 %d 条规则", len(self._rules))
def add_rule(self, rule: dict) -> None:
@@ -365,8 +370,8 @@ class MonitorRuleEngine:
def remove_rule(self, rule_id: str) -> None:
self._rules.pop(rule_id, None)
# 清理对应的 cooldown 记录
self._last_fire = {k: v for k, v in self._last_fire.items() if k[0] != rule_id}
# 清理对应的 cooldown 记录 (list 快照: 评估线程可能并发写 _last_fire)
self._last_fire = {k: v for k, v in list(self._last_fire.items()) if k[0] != rule_id}
def clear(self) -> None:
self._rules.clear()
@@ -392,9 +397,10 @@ class MonitorRuleEngine:
"""是否存在指定类型的 (已启用) 规则。供 quote_service 判断是否需要注入特殊数据。"""
if not self._rules:
return False
# list() 快照: API 线程可能并发增删规则, 直接迭代 dict 会抛 RuntimeError
return any(
r.get("enabled", True) and r.get("type") == rtype
for r in self._rules.values()
for r in list(self._rules.values())
)
# ── 评估 ───────────────────────────────────────────
@@ -414,7 +420,9 @@ class MonitorRuleEngine:
# 每轮重置: 只保留本次 evaluate 产出的策略结果
self._latest_strategy_results = {}
for rule_id, rule in self._rules.items():
# list() 快照: 本方法跑在行情轮询线程, API 线程同时 add/remove 规则
# 会触发 "dictionary changed size during iteration", 整轮告警丢失
for rule_id, rule in list(self._rules.items()):
try:
events.extend(self._evaluate_rule(df, rule, now))
except Exception as e: