Skip to content

fix(integration): SetpointDriftReconciler 透過 LeaderGate 阻擋 follower 寫#140

Open
csp0924 wants to merge 2 commits into
mainfrom
fix/reconciler-leader-gate-check
Open

fix(integration): SetpointDriftReconciler 透過 LeaderGate 阻擋 follower 寫#140
csp0924 wants to merge 2 commits into
mainfrom
fix/reconciler-leader-gate-check

Conversation

@csp0924

@csp0924 csp0924 commented May 13, 2026

Copy link
Copy Markdown
Owner

問題

Closed-loop probe 發現:SetpointDriftReconciler._reconcile_workCommandRouter.try_write_single 寫設備,但 CommandRouter 完全沒接 LeaderGate。HA cluster 下 follower-node 的 reconciler 會跟 leader 同時寫同一個共享物理設備(雙寫違反 single-writer invariant)。對照 WriteCommandManager.execute 是有 leader_gate 守護的,兩者不對稱。

Sandbox 證據(sandbox/manager_reconciler_race_leader_bypass_demo.py):dual reconciler 並行 → pre-fix follower writes = 1(應為 0)。

改動

SetpointDriftReconciler.__init__ 新增 leader_gate: LeaderGate | None = None(kwarg、預設 None → 向後相容)。_reconcile_work 開頭 early-return:

if self._leader_gate is not None and not self._leader_gate.is_leader:
    detail["paused"] = "not_leader"
    return

CommandRouter 保持中立(strategy path 仍需在 follower 跑 shadow compute),修在 reconciler 一層。

驗證

  • 新增 tests/integration/test_setpoint_drift_reconciler.py::TestLeaderGate 4 案(follower 不寫 / leader 正常寫 / leader_gate=None 向後相容 / 中途 promote 恢復寫)。Pre-fix 3 FAIL → post-fix 全 PASS。
  • Sandbox rerun: follower_writes == 0
  • 完整 regression: 4855 passed

HA cluster 場景下,CommandRouter.try_write_single 無 leader_gate 守門,
follower 節點的 SetpointDriftReconciler 仍會對共用設備發出寫入 →
破壞 single-writer invariant、產生 dual-write window。

修法(窄面 patch):
- SetpointDriftReconciler.__init__ 新增 leader_gate: LeaderGate | None = None
  關鍵字參數(向後相容,patch bump)。
- _reconcile_work 在入口判斷 leader_gate.is_leader:非 leader 直接早退,
  detail['paused'] = 'not_leader';不視為 unhealthy(被 gate 擋是正常運作)。

設計權衡:
- CommandRouter 保持中性(strategy path 在 follower 仍可跑 shadow-mode compute),
  只在 reconciler 本身守門。對齊 WriteCommandManager.execute 的 gate 早期拒絕設計。
- 未注入 gate(單節點)行為不變。

Sandbox 量化驗證:
- pre-fix: follower_writes=1(dual-write 確認存在)
- post-fix: follower_writes=0, detail['paused']='not_leader'
Copilot AI review requested due to automatic review settings May 13, 2026 00:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

此 PR 針對 HA/cluster 部署下的 single-writer invariant 問題,讓 SetpointDriftReconciler 在注入 LeaderGate 後,於 follower 節點直接暫停 reconcile-driven 寫入,避免 follower 與 leader 同時對共享實體設備雙寫;並補上對應的整合測試以驗證 follower/leader/無 gate/升格等情境。

Changes:

  • SetpointDriftReconciler 新增 leader_gate: LeaderGate | None = None 參數,並在 _reconcile_work 開頭以 is_leader 做 follower 早退。
  • 新增 tests/integration/test_setpoint_drift_reconciler.py::TestLeaderGate,覆蓋 follower 不寫、leader 正常寫、向後相容、升格後恢復寫入。

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
csp_lib/integration/setpoint_drift_reconciler.py 注入 LeaderGate 並在 follower 節點早退以阻止 reconciler-driven 寫入
tests/integration/test_setpoint_drift_reconciler.py 新增 LeaderGate 行為測試覆蓋,驗證 follower/leader/相容性/升格情境

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +133 to +136
# Leader gate:非 leader 直接早退,零寫入;不視為 unhealthy(被 gate 擋是正常運作)
if self._leader_gate is not None and not self._leader_gate.is_leader:
detail["paused"] = "not_leader"
return
LeaderGate 早退分支原本在初始化 detail["drift_count"] /
detail["devices_fixed"](cooldown 開啟時還有 skipped_by_cooldown)前就 return,
導致 follower 節點的 status.detail 缺這些 key。
examples/16_operator_pattern.py 等呼叫端用 status.detail["drift_count"]
直接 index → follower 上會 KeyError。

修法:把預設值寫入提前到 gate 檢查之前,無論 follower 早退或
迴圈中途 raise,detail 都能保持 drift_count=0 / devices_fixed=()
(cooldown 開啟時 skipped_by_cooldown=0)的契約。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants