mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-03 15:43:59 +00:00
fix(cluster): revoke 在无 topology 条目时静默失效 + 停用状态跨节点不同步
上一提交(46e8bc3)上线后实测:`.106` 重启后,被用户停用的
`minecraft` worker **仍然被拉起**。继续挖出两处,均为静默失效型。
## 1. Handler.Revoke 被 RemoveTopology 的返回值挡住了
if claimed.Revoke {
if e.state.RemoveTopology(...) { // ← false 时整个块跳过
if e.Handler != nil { e.Handler.Revoke(...) }
}
}
停 worker 的唯一动作被关在「topology 里确实有条目」的条件里。而
RemoveTopology 对**不在 topology 的转发返回 false** —— 也就是说,越是
需要清理的僵尸转发(条目已丢、worker 还在),revoke 越是完全不做。
这正是 minecraft 的处境:停用请求到达时它已不在 topology ⇒ 返回 false
⇒ Revoke 不执行 ⇒ worker 永远不停 ⇒ 每 33 秒刷一次 connection refused。
修法:停 worker 与摘条目是**两件独立的事**,条目不在也照样停。
(审计日志 forward.remove 仍只在真的摘掉条目时写,这是对的。)
## 2. 停用状态是节点本地的,从不跨节点同步
`links` 表每节点各一份。实测同一时刻 `.60` 记 disabled=1、`.106` 记
disabled=0 —— 因为只有收到停用请求的那个节点写了 store,而**持有 worker
的 owner 往往是另一台机器**,它的副本仍是"启用"。Claim 只读本地 store
⇒ owner 认为该转发是启用的 ⇒ 又被拉起。
修法:task 已携带 Disabled,以它为准,RevokeFn 在本节点把它落库
(已有行改写;没有行则补一条 disabled 记录,防止日后在本节点被 claim
时复活)。
## 测试
- TestRevokeIdempotent 原先只断言"不 panic",正好漏掉这个 bug —— 它
允许「Handler.Revoke 从不调用」通过。现补上断言:拓扑里没有条目时
**仍必须调用** Handler.Revoke。
- 验证该测试有效:把 `&& removed` gate 加回去 → 如期变红;还原后变绿。
go build / go vet / go test ./... 全绿,gofmt 干净。
This commit is contained in:
@ -227,6 +227,50 @@ func main() {
|
||||
// stop/start: it wiped sibling forwards sharing the local or remote.
|
||||
RevokeFn: func(ctx context.Context, tk *cluster.Task) error {
|
||||
_ = st.SetLinkDisabled(tk.Local.Name, tk.Remote.Name, tk.Link.RemotePort, true)
|
||||
// The links table is node-local: only the node that received the
|
||||
// stop request has the flag persisted, while the node that OWNS the
|
||||
// forward is the one holding the worker (and is often a different
|
||||
// machine). Trusting only the local row meant the owner's copy could
|
||||
// still read disabled=false. The task now carries the flag, so use it
|
||||
// as the authority and write it here.
|
||||
ln, found, err := st.LinkByTriple(tk.Local.Name, tk.Remote.Name, tk.Link.RemotePort)
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case found:
|
||||
ln.Disabled = true
|
||||
// Persist via the same rewrite path the canvas uses, so the flag
|
||||
// survives a restart on THIS node too.
|
||||
links, err := st.ListLinks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range links {
|
||||
if links[i].Local == ln.Local && links[i].Remote == ln.Remote && links[i].RemotePort == ln.RemotePort {
|
||||
links[i].Disabled = true
|
||||
}
|
||||
}
|
||||
if err := st.ReplaceLinks(links); err != nil {
|
||||
return err
|
||||
}
|
||||
case tk.Link.Disabled:
|
||||
// No local row yet (this node never claimed the forward) but the
|
||||
// task asserts the stop — record it so a later claim on this node
|
||||
// cannot resurrect the forward.
|
||||
loc, rem := tk.Local, tk.Remote
|
||||
links, err := st.ListLinks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
links = append(links, store.Link{
|
||||
Local: loc.Name, Remote: rem.Name, RemotePort: tk.Link.RemotePort,
|
||||
OffsetX: tk.Link.OffsetX, OffsetY: tk.Link.OffsetY,
|
||||
Group: tk.Link.Group, Disabled: true,
|
||||
})
|
||||
if err := st.ReplaceLinks(links); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
key := process.WorkerKey(tk.Local.Name, tk.Remote.Name, tk.Link.RemotePort)
|
||||
if _, running := pm.Status(key); running {
|
||||
_ = pm.Stop(key)
|
||||
|
||||
Reference in New Issue
Block a user