Files
webui4frpc/internal
JianFeeeee 918ca5d5ba fix(cluster): 停用状态以「环」为权威,消除长期离线导致的永久分歧
承接用户指出的遗留:links 表是每节点本地副本,靠「store→环上行 + 环→store
下行」双向 sync 收敛,但两边都可能被覆盖。其中「本地 store 权威」这条规则有
硬伤,本次改掉。

## 先复现,再动手

写探针验证「入环 token 会不会冲掉本地未传播的决定」,结果比预期严重:

    after adopting a stale token: known=true disabled=false
    LOCAL DISABLE WAS WIPED by an incoming token

OnToken/AdoptState 是 `e.state = tk.State` **整体替换**。两次 token 之间做出的
停用决定,只要下一轮到达的 token 是「决定之前」捕获的,就会被整个洗掉 ——
决定永远传不出去,转发照旧运行。Group 之所以看起来没这个问题,是因为它每次
adoption 都被 `SetTopologySync` 从 store 重新推上去,而 disabled 没有对应的
「尚未传播」保护。

## 改法:环权威 + 本地决定带「未确认」标记

1. **环权威**:adoption 时把环上的 disabled 写穿本地 store(write-through,
   不是监听器),任何 peer 的决定都在一个 token 周期内落地。这终结了旧规则
   「各人信自己那份」造成的永久分歧。顺序上只有单向要求:引擎先重新断言本地
   未确认决定,再跑 host sync,所以读环绝不会覆盖用户刚做的操作。

2. **未确认决定受保护**(RingEngine.localDisabled):`UpdateTopologyDisabled`
   记下决定,adoption 后由 `reconcileLocalDisabled` 重新断言到刚采纳的 state 上,
   于是它会随下一轮 token 传出去。环报回同值时删除该键(全cluster已一致);
   转发从 topology 消失时一并清扫,map 不会无限增长。**false 同样受保护** ——
   重新启用也需要传播,丢掉它会把转发永久留在停用态。

3. `TopologyDisabled()` 对未确认决定短路返回本地值,避免状态页与用户刚做的
   操作相反。

## 测试(又抓到一个假绿)

新增 4 条:停用/启用跨 adoption 存活、确认后停止断言(让位给 peer 的后续决定)、
追踪表不累积。

★ `TestLocalDisableSurvivesAdoption` **第一版是假绿**:它断言
`TopologyDisabled()`,而该访问器会短路到本地决定,于是「即便即将转发的 state
仍是 enabled」它也报 true。改成断言**下游节点会看到什么**(用一个 peer 引擎
AdoptState 本引擎的 state)后,去掉重新断言如期变红:

    the forwarded token carries disabled=false, want true

双向验证通过,这个教训要记:测「声明」而不是测「实际传播的状态」,等于没测。

go build / go vet / go test ./... 全绿,gofmt 干净。
2026-09-26 11:44:28 +08:00
..