mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-03 15:43:59 +00:00
上一提交(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 干净。
83 lines
3.1 KiB
Go
83 lines
3.1 KiB
Go
package cluster
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"webui4frpc/internal/store"
|
|
)
|
|
|
|
// TestRevokeTaskRemovesTopology: a revoke task published to the ring removes
|
|
// the forward from topology, calls Handler.Revoke, and logs forward.remove.
|
|
func TestRevokeTaskRemovesTopology(t *testing.T) {
|
|
revoked := false
|
|
eng := NewEngine("n1", "n1:7500", "u", "p", "0.71.0", nil,
|
|
&fakeHandler{load: Load{MemPct: 5, NetPct: 5},
|
|
revoke: func(ctx context.Context, tk *Task) error { revoked = true; return nil }},
|
|
func(ctx context.Context, next string, tk *Token) error { return nil },
|
|
"n1:7500", true, "")
|
|
|
|
// establish a forward
|
|
eng.state.AddPending(store.Local{Name: "web"}, store.Remote{Name: "frps1"}, store.Link{RemotePort: 18081})
|
|
// lowest-load is n1 (only node): it claims + builds topology
|
|
if _, err := eng.OnToken(context.Background(), &Token{Cycle: 1, State: eng.state}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(eng.state.TopologyList()) != 1 {
|
|
t.Fatalf("topology after claim = %+v", eng.state.TopologyList())
|
|
}
|
|
|
|
// publish a revoke task pointing at the same forward
|
|
eng.state.AddRevoke(store.Local{Name: "web"}, store.Remote{Name: "frps1"}, store.Link{RemotePort: 18081})
|
|
if _, err := eng.OnToken(context.Background(), &Token{Cycle: 2, State: eng.state}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(eng.state.TopologyList()) != 0 {
|
|
t.Fatalf("topology after revoke = %+v", eng.state.TopologyList())
|
|
}
|
|
if !revoked {
|
|
t.Fatal("Handler.Revoke was not called")
|
|
}
|
|
// log should contain forward.remove
|
|
var sawRemove bool
|
|
for _, e := range eng.Log.Snapshot() {
|
|
if e.Kind == LogForwardRemove {
|
|
sawRemove = true
|
|
}
|
|
}
|
|
if !sawRemove {
|
|
t.Fatalf("log missing forward.remove: %+v", eng.Log.Snapshot())
|
|
}
|
|
}
|
|
|
|
// TestRevokeIdempotent: revoking an already-missing forward does not error.
|
|
//
|
|
// It must ALSO still call Handler.Revoke. The handler is what actually stops
|
|
// the per-forward frpc worker; the topology entry is only bookkeeping. Gating
|
|
// the handler on RemoveTopology()'s return value (as this test used to permit)
|
|
// turned every revoke of an already-absent forward into a silent no-op — the
|
|
// worker kept running, which is how a forward the user had stopped kept
|
|
// dialling a local service that was intentionally down, forever.
|
|
func TestRevokeIdempotent(t *testing.T) {
|
|
revoked := 0
|
|
eng := NewEngine("n1", "n1:7500", "u", "p", "0.71.0", nil,
|
|
&fakeHandler{load: Load{MemPct: 5, NetPct: 5},
|
|
revoke: func(ctx context.Context, tk *Task) error { revoked++; return nil }},
|
|
func(ctx context.Context, next string, tk *Token) error { return nil },
|
|
"n1:7500", true, "")
|
|
|
|
eng.state.AddRevoke(store.Local{Name: "ghost"}, store.Remote{Name: "frps1"}, store.Link{RemotePort: 1})
|
|
if _, err := eng.OnToken(context.Background(), &Token{Cycle: 1, State: eng.state}); err != nil {
|
|
t.Fatalf("revoke missing: %v", err)
|
|
}
|
|
// no topology entry, no panic
|
|
if len(eng.state.TopologyList()) != 0 {
|
|
t.Fatal("should be empty")
|
|
}
|
|
// The stop side-effect must have happened even though there was no entry.
|
|
if revoked != 1 {
|
|
t.Fatalf("Handler.Revoke called %d times, want 1 — a revoke with no topology entry "+
|
|
"must still stop the worker, otherwise stopped forwards keep running", revoked)
|
|
}
|
|
}
|