From 58b600baf636bb236455158f0343493c0dce100c Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Fri, 7 Aug 2026 18:03:16 +0800 Subject: [PATCH] feat(node/tracker): implement initial refresh gate and bootstrap completion logic --- internal/node/tracker.go | 93 +++++++++++++++++++++++++++++++++------- main.go | 17 +++++++- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/internal/node/tracker.go b/internal/node/tracker.go index a352129..d6980f5 100644 --- a/internal/node/tracker.go +++ b/internal/node/tracker.go @@ -57,12 +57,12 @@ type Node struct { // StatusChange represents a node status transition. type StatusChange struct { - Event string // "Online" or "Offline" - UUID string - Name string - Message string - OldOnline bool - NewOnline bool + Event string // "Online" or "Offline" + UUID string + Name string + Message string + OldOnline bool + NewOnline bool } // StatusChangeCallback is called when a node's online status changes. @@ -71,11 +71,18 @@ type StatusChangeCallback func(change StatusChange) // Tracker maintains the in-memory state of all monitored nodes. // All methods are thread-safe. type Tracker struct { - mu sync.RWMutex - nodes map[string]*Node // uuid → Node - uuidToName map[string]string // uuid → name (from node list) - onlineSet map[string]bool // which uuids are currently online - callbacks []StatusChangeCallback + mu sync.RWMutex + nodes map[string]*Node // uuid → Node + uuidToName map[string]string // uuid → name (from node list) + onlineSet map[string]bool // which uuids are currently online + callbacks []StatusChangeCallback + + // Initial-refresh gating: status-change notifications are suppressed until + // every node from the Komari node list has delivered a status update, so the + // startup state is treated as a baseline rather than a flood of changes. + knownUUIDs map[string]bool // uuids from the Komari node list + reportedUUIDs map[string]bool // node-list uuids that have reported status + bootstrapDone bool // true once the initial refresh has finished } var globalTracker *Tracker @@ -83,9 +90,11 @@ var globalTracker *Tracker // InitTracker initializes the global node tracker. func InitTracker() { globalTracker = &Tracker{ - nodes: make(map[string]*Node), - uuidToName: make(map[string]string), - onlineSet: make(map[string]bool), + nodes: make(map[string]*Node), + uuidToName: make(map[string]string), + onlineSet: make(map[string]bool), + knownUUIDs: make(map[string]bool), + reportedUUIDs: make(map[string]bool), } postLog.Info("Node tracker initialized") } @@ -121,6 +130,13 @@ func (t *Tracker) UpdateNodeList(nodeNames map[string]string) { t.mu.Lock() defer t.mu.Unlock() + // The node list defines the set of nodes to wait for during the initial + // status refresh. + t.knownUUIDs = make(map[string]bool, len(nodeNames)) + for uuid := range nodeNames { + t.knownUUIDs[uuid] = true + } + t.uuidToName = make(map[string]string, len(nodeNames)) for uuid, name := range nodeNames { t.uuidToName[uuid] = name @@ -211,10 +227,35 @@ func (t *Tracker) UpdateStatus(onlineUUIDs []string, reports map[string]Report) } t.onlineSet = newOnlineSet + + // Track which node-list nodes have delivered a status update. The initial + // refresh is considered complete once every known node has reported at + // least once (either online or offline). + for _, uuid := range onlineUUIDs { + if t.knownUUIDs[uuid] { + t.reportedUUIDs[uuid] = true + } + } + for uuid := range reports { + if t.knownUUIDs[uuid] { + t.reportedUUIDs[uuid] = true + } + } + + // Changes detected while the initial refresh is still in progress represent + // the nodes' startup state rather than real transitions, so suppress them. + suppressChanges := !t.bootstrapDone + if !t.bootstrapDone && t.allKnownNodesReported() { + t.bootstrapDone = true + postLog.Info(fmt.Sprintf("All %d nodes refreshed their status; status notifications enabled", len(t.knownUUIDs))) + } t.mu.Unlock() // Fire callbacks for each change (outside the lock). for _, change := range changes { + if suppressChanges { + continue + } postLog.Info(fmt.Sprintf("Node status change: %s [%s] - %s", change.Name, change.UUID, change.Event)) t.fireCallbacks(change) } @@ -222,6 +263,29 @@ func (t *Tracker) UpdateStatus(onlineUUIDs []string, reports map[string]Report) return changes } +// allKnownNodesReported returns true when every node from the Komari node list +// has delivered at least one status update. Must be called with the lock held. +func (t *Tracker) allKnownNodesReported() bool { + for uuid := range t.knownUUIDs { + if !t.reportedUUIDs[uuid] { + return false + } + } + return true +} + +// CompleteBootstrap force-completes the initial status refresh gate. It is a +// safety net so that notifications cannot be blocked forever by a node that +// never reports a status. Calling it again is a no-op. +func (t *Tracker) CompleteBootstrap() { + t.mu.Lock() + defer t.mu.Unlock() + if !t.bootstrapDone { + t.bootstrapDone = true + postLog.Info("Initial node status refresh timeout reached; status notifications enabled") + } +} + // GetNode returns a copy of the node data for the given UUID. func (t *Tracker) GetNode(uuid string) (*Node, bool) { t.mu.RLock() @@ -300,4 +364,3 @@ func (t *Tracker) GetOfflineServers() []string { } return result } - diff --git a/main.go b/main.go index e51a1a6..a5e2f6c 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,8 @@ import ( "nukumizu-backend/database" "nukumizu-backend/internal/controller" "nukumizu-backend/internal/controller/pipes" - "nukumizu-backend/internal/komari" "nukumizu-backend/internal/controller/pipes/qq_napcat" + "nukumizu-backend/internal/komari" "nukumizu-backend/internal/node" "nukumizu-backend/postLog" "nukumizu-backend/utils" @@ -289,4 +289,19 @@ func startBackgroundTasks(_ *config.Config) { } }) } + + // Safety net for the initial refresh gate: if some node never reports a + // status, force-complete the initial refresh after a grace period so status + // notifications cannot stay blocked forever. + go func() { + defer func() { + if r := recover(); r != nil { + postLog.Error(fmt.Sprintf("Bootstrap timeout panic: %v", r)) + } + }() + time.Sleep(2 * time.Minute) // Add 2 minutes grace period to allow nodes to report status. + if tracker := node.GetTracker(); tracker != nil { + tracker.CompleteBootstrap() + } + }() }