mapper/batcher: replace connected map with per-node disconnectedAt

The Batcher's connected field (*xsync.Map[types.NodeID, *time.Time])
encoded three states via pointer semantics:

  - nil value:    node is connected
  - non-nil time: node disconnected at that timestamp
  - key missing:  node was never seen

This was error-prone (nil meaning 'connected' inverts Go idioms),
redundant with b.nodes + hasActiveConnections(), and required keeping
two parallel maps in sync. It also contained a bug in RemoveNode where
new(time.Now()) was used instead of &now, producing a zero time.

Replace the separate connected map with a disconnectedAt field on
multiChannelNodeConn (atomic.Pointer[time.Time]), tracked directly
on the object that already manages the node's connections.

Changes:
  - Add disconnectedAt field and helpers (markConnected, markDisconnected,
    isConnected, offlineDuration) to multiChannelNodeConn
  - Remove the connected field from Batcher
  - Simplify IsConnected from two map lookups to one
  - Simplify ConnectedMap and Debug from two-map iteration to one
  - Rewrite cleanupOfflineNodes to scan b.nodes directly
  - Remove the markDisconnectedIfNoConns helper
  - Update all tests and benchmarks

Fixes #3141
This commit is contained in:
Kristoffer Dalby 2026-03-14 14:06:52 +00:00
parent 60317064fd
commit 87b8507ac9
6 changed files with 148 additions and 191 deletions

View file

@ -305,7 +305,7 @@ func BenchmarkScale_ConnectedMap(b *testing.B) {
for _, n := range scaleCountsHeavy {
b.Run(strconv.Itoa(n), func(b *testing.B) {
batcher, _ := benchBatcher(n, 1)
batcher, channels := benchBatcher(n, 1)
defer func() {
close(batcher.done)
@ -315,8 +315,11 @@ func BenchmarkScale_ConnectedMap(b *testing.B) {
// 10% disconnected for realism
for i := 1; i <= n; i++ {
if i%10 == 0 {
now := time.Now()
batcher.connected.Store(types.NodeID(i), &now) //nolint:gosec
id := types.NodeID(i) //nolint:gosec
if mc, ok := batcher.nodes.Load(id); ok {
mc.removeConnectionByChannel(channels[id])
mc.markDisconnected()
}
}
}