state: reuse peer adjacency for payload-only writes

A write that cannot move visibility carries the previous adjacency
forward; policy and user changes rebuild it explicitly.

Updates #3417

(cherry picked from commit e3c4c81b18)
This commit is contained in:
Kristoffer Dalby committed 2026-09-23 21:19:22 +02:00
1 parent 938c2bd753
commit 237dc74e73
3 files changed
+369 -25

No files matched your search

+100 -24
View File
@@ -89,6 +89,13 @@ var (
Name: "nodestore_queue_depth",
Help: "Current depth of NodeStore write queue",
})
// Bounded labels only: no node IDs or free-form reasons.
nodeStoreSnapshotBuilds = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: prometheusNamespace,
Name: "nodestore_snapshot_builds_total",
Help: "Snapshot builds by how peer adjacency was obtained: recomputed through the policy or reused from the previous snapshot.",
}, []string{"peers"})
)
// NodeStore is a thread-safe store for nodes.
@@ -124,7 +131,7 @@ func NewNodeStore(allNodes types.Nodes, peersFunc PeersFunc, batchSize int, batc
nodes[n.ID] = *n
}
snap := snapshotFromNodes(nodes, peersFunc, nil)
snap := snapshotFromNodes(nodes, peersFunc, nil, false, false)
store := &NodeStore{
peersFunc: peersFunc,
@@ -193,6 +200,22 @@ type work struct {
multiUpdates map[types.NodeID]UpdateNodeFunc
}
// updateChanges reports whether an in-place update moved a peer-visibility
// input (which also re-elects routes) or only a route-election input.
// Peer visibility depends on what the policy reads; election additionally
// depends on online and health state, treating unknown online as offline.
func updateChanges(pre, post *types.Node) (bool, bool) {
preView, postView := pre.View(), post.View()
if postView.HasPolicyChange(preView) || postView.HasNetworkChanges(preView) {
return true, true
}
wasOnline := pre.IsOnline != nil && *pre.IsOnline
isOnline := post.IsOnline != nil && *post.IsOnline
return false, wasOnline != isOnline || pre.Unhealthy != post.Unhealthy
}
// PutNode adds or updates a node in the store.
// If the node already exists, it will be replaced.
// If the node does not exist, it will be added.
@@ -450,6 +473,12 @@ func (s *NodeStore) applyBatch(batch []work) {
// NodeView for that work.
setErrResults := make(map[*work]error)
// relationChanged forces a peersFunc run; electionChanged forces a
// route re-election. put/del/setName/rebuildPeerMaps are treated as
// relation-changing; updateMulti compares the node before and after.
relationChanged := false
electionChanged := false
for i := range batch {
w := &batch[i]
switch w.op {
@@ -461,6 +490,9 @@ func (s *NodeStore) applyBatch(batch []work) {
if w.nodeResult != nil {
nodeResultRequests[w.nodeID] = append(nodeResultRequests[w.nodeID], w)
}
relationChanged = true
electionChanged = true
case updateMulti:
for id, fn := range w.multiUpdates {
n, exists := nodes[id]
@@ -469,6 +501,7 @@ func (s *NodeStore) applyBatch(batch []work) {
}
oldGivenName := n.GivenName
pre := n.Clone()
fn(&n)
if n.GivenName != oldGivenName {
@@ -476,6 +509,10 @@ func (s *NodeStore) applyBatch(batch []work) {
}
nodes[id] = n
relation, election := updateChanges(pre, &n)
relationChanged = relationChanged || relation
electionChanged = electionChanged || election
}
case del:
delete(nodes, w.nodeID)
@@ -483,6 +520,9 @@ func (s *NodeStore) applyBatch(batch []work) {
if w.nodeResult != nil {
nodeResultRequests[w.nodeID] = append(nodeResultRequests[w.nodeID], w)
}
relationChanged = true
electionChanged = true
case setName:
n, exists := nodes[w.nodeID]
if !exists {
@@ -518,15 +558,28 @@ func (s *NodeStore) applyBatch(batch []work) {
n.GivenName = w.name
nodes[w.nodeID] = n
nodeResultRequests[w.nodeID] = append(nodeResultRequests[w.nodeID], w)
relationChanged = true
electionChanged = true
case rebuildPeerMaps:
// rebuildPeerMaps doesn't modify nodes, it just forces the snapshot rebuild
// below to recalculate peer relationships using the current peersFunc
rebuildOps = append(rebuildOps, w)
relationChanged = true
electionChanged = true
}
}
prev := s.data.Load()
newSnap := snapshotFromNodes(nodes, s.peersFunc, prev.routes)
// A relation change recomputes adjacency; anything else reuses the
// previous peer IDs and re-elects routes only if an election input moved.
if relationChanged {
nodeStoreSnapshotBuilds.WithLabelValues("recomputed").Inc()
} else {
nodeStoreSnapshotBuilds.WithLabelValues("reused").Inc()
}
newSnap := snapshotFromNodes(nodes, s.peersFunc, prev, !relationChanged, !electionChanged)
s.data.Store(&newSnap)
// Update node count gauge
@@ -610,13 +663,17 @@ func resolveGivenName(nodes map[types.NodeID]types.Node, self types.NodeID, base
}
}
// snapshotFromNodes builds the index maps and primary-route table for
// a new [Snapshot]. prevRoutes carries forward the previous primary
// assignment so a still-valid choice survives unrelated batches.
// snapshotFromNodes builds a Snapshot from nodes. With reusePeers the
// previous peer-ID adjacency is carried over unchanged; with reuseRoutes
// the previous route election is. ListPeers resolves adjacency IDs
// through this snapshot's fresh views, so a reused adjacency never
// serves stale node payloads. prev may be nil only when both reuse
// flags are false.
func snapshotFromNodes(
nodes map[types.NodeID]types.Node,
peersFunc PeersFunc,
prevRoutes map[netip.Prefix]types.NodeID,
prev *Snapshot,
reusePeers, reuseRoutes bool,
) Snapshot {
timer := prometheus.NewTimer(nodeStoreSnapshotBuildDuration)
defer timer.ObserveDuration()
@@ -630,7 +687,34 @@ func snapshotFromNodes(
nodeViewsByID[n.ID] = nv
}
routes, isPrimaryRoute := electPrimaryRoutes(nodes, prevRoutes)
var (
routes map[netip.Prefix]types.NodeID
isPrimaryRoute map[types.NodeID]bool
)
if reuseRoutes {
routes, isPrimaryRoute = prev.routes, prev.isPrimaryRoute
} else {
// Carrying the previous assignment forward lets a still-valid
// primary survive unrelated batches.
var prevRoutes map[netip.Prefix]types.NodeID
if prev != nil {
prevRoutes = prev.routes
}
routes, isPrimaryRoute = electPrimaryRoutes(nodes, prevRoutes)
}
var peerIDsByNode map[types.NodeID][]types.NodeID
if reusePeers {
peerIDsByNode = prev.peersByNode
} else {
peersTimer := prometheus.NewTimer(nodeStorePeersCalculationDuration)
peerIDsByNode = peersFunc(allNodes)
peersTimer.ObserveDuration()
}
newSnap := Snapshot{
nodesByID: nodes,
@@ -638,24 +722,12 @@ func snapshotFromNodes(
allNodes: allNodes,
nodesByNodeKey: make(map[key.NodePublic]types.NodeView),
nodesByMachineKey: make(map[key.MachinePublic]map[types.UserID]types.NodeView),
// peersByNode is most likely the most expensive operation,
// it will use the list of all nodes, combined with the
// current policy to precalculate which nodes are peers and
// can see each other.
peersByNode: func() map[types.NodeID][]types.NodeID {
peersTimer := prometheus.NewTimer(nodeStorePeersCalculationDuration)
defer peersTimer.ObserveDuration()
return peersFunc(allNodes)
}(),
nodesByUser: make(map[types.UserID][]types.NodeView),
routes: routes,
isPrimaryRoute: isPrimaryRoute,
peersByNode: peerIDsByNode,
nodesByUser: make(map[types.UserID][]types.NodeView),
routes: routes,
isPrimaryRoute: isPrimaryRoute,
}
// Build nodesByUser, nodesByNodeKey, and nodesByMachineKey maps
for _, n := range nodes {
nodeView := nodeViewsByID[n.ID]
userID := n.TypedUserID()
@@ -1022,7 +1094,11 @@ func (s *NodeStore) RebuildPeerMaps() {
rebuildResult: result,
}
s.writeQueue <- w
select {
case s.writeQueue <- w:
case <-s.stopped:
return
}
<-result
}