From 4e332c42a61c35c56a973b56aae15c4124e5ae72 Mon Sep 17 00:00:00 2001 From: Gundy Date: Thu, 2 Jul 2026 23:39:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(monitor):=20=E8=A7=84=E5=88=99=E8=A1=A8?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E8=BF=AD=E4=BB=A3=E5=8A=A0=E5=BF=AB=E7=85=A7?= =?UTF-8?q?,=20=E9=98=B2=E8=AF=84=E4=BC=B0=E7=BA=BF=E7=A8=8B=E4=B8=8E=20AP?= =?UTF-8?q?I=20=E7=BA=BF=E7=A8=8B=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 同理。 --- backend/app/strategy/monitor.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/app/strategy/monitor.py b/backend/app/strategy/monitor.py index d04a104..469200b 100644 --- a/backend/app/strategy/monitor.py +++ b/backend/app/strategy/monitor.py @@ -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: