fix: start/stop for ring-only forwards + group sync via ring topology

Three issues fixed:

1. Start/stop returned 404 for forwards owned by other nodes:
   findLinkByTriple only checked local SQLite store. Added
   findLinkFromTopology fallback — startForward/stopForward now look up
   ring topology entries when the local store doesn't have the link.

2. Group labels didn't sync through the ring:
   handleForwardsAssign only updated local SQLite, never the ring
   topology. Added State.UpdateTopologyGroup + Engine.UpdateTopologyGroup
   — handleForwardsAssign now updates both. Added topologySync callback
   called after every e.state = tk.State (OnToken) and e.state = s
   (AdoptState) to re-apply local store group overrides onto the freshly
   adopted topology, so they survive state adoption and propagate via
   the next token forward. handleForwardsGroupDelete also clears ring
   topology entries.

3. Status-merge branch omitted Group field:
   ring-only forwards always showed '未分组'. Added Group: t.Link.Group
   to the topology-merge forwardStatus.

Verified: 4-node cluster, 5 forwards, group assigned on node-a
propagates to all nodes within one token cycle; stop from non-owning
node succeeds (HTTP 200) and worker stops on the owning node.
This commit is contained in:
2026-08-19 21:52:28 +08:00
parent 465a86c755
commit 2552c14faf
4 changed files with 110 additions and 39 deletions

View File

@ -44,6 +44,12 @@ type Engine struct {
// every token cycle (OnToken) and on AdoptState. Cleared (pass "") on
// detachAsStandalone — an explicit leave must NOT auto-rejoin.
peerPersist func(peersJSON string) error
// topologySync is called right after e.state = tk.State (OnToken) or
// e.state = s (AdoptState) so the host can re-apply local store overrides
// (e.g. group labels) onto the freshly adopted topology. Without this,
// group changes made via HTTP handlers between token cycles are overwritten
// by the next state adoption and never propagate to other nodes.
topologySync func()
state State
// myAddr maps our Node ID to the address peers dial.
@ -219,6 +225,12 @@ func (e *Engine) OnToken(ctx context.Context, tk *Token) (*Token, error) {
if e.state.PendingTasks == nil {
e.state.PendingTasks = map[string]*Task{}
}
// Re-apply local store overrides (group labels, disabled flags) onto
// the freshly adopted topology so they survive state adoption and
// propagate to all nodes via the next token forward.
if e.topologySync != nil {
e.topologySync()
}
for id, t := range localPending {
e.state.PendingTasks[id] = t
}
@ -695,6 +707,9 @@ func (e *Engine) AdoptState(s State) {
for id, t := range kept {
e.state.PendingTasks[id] = t
}
if e.topologySync != nil {
e.topologySync()
}
e.state.UpsertNode(Node{ID: e.ID, Addr: e.myAddr, Alive: true,
Load: e.loadSnapshot(), Version: e.Version, Cache: e.Cache, NodeKey: e.nodeKey})
if e.Log != nil {
@ -780,6 +795,13 @@ func (e *Engine) HasTask(local, remote string, port int) bool {
return false
}
// UpdateTopologyGroup sets the group label on a topology entry. The change
// propagates to all nodes via the next token cycle. Used by the forwards
// assign handler so group labels sync through the ring.
func (e *Engine) UpdateTopologyGroup(local, remote string, port int, group string) bool {
return e.state.UpdateTopologyGroup(local, remote, port, group)
}
// IsLeader reports whether this node is the current ring leader.
func (e *Engine) IsLeader() bool { return e.state.LeaderID == e.ID }
@ -796,6 +818,12 @@ func (e *Engine) SetKeyPersist(fn func(key string) error) { e.keyPersist = fn }
// to durable storage (store.SetClusterPeers). Called once from main.go.
func (e *Engine) SetPeerPersist(fn func(peersJSON string) error) { e.peerPersist = fn }
// SetTopologySync installs the callback used to re-apply local store overrides
// (group labels, disabled flags) onto the ring topology after each state
// adoption. Called once from main.go. Without this, group changes made via
// HTTP handlers are overwritten by the next e.state = tk.State.
func (e *Engine) SetTopologySync(fn func()) { e.topologySync = fn }
// persistPeers extracts all alive peers (addr + nodeKey, excluding self)
// from the current ring state and persists them via the peerPersist callback.
// Called on every token cycle (OnToken) and on AdoptState so a crashed node