mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-18 22:34:54 +09:00
mapper: record initial-map peers only after delivery
A timed-out initial map left phantom lastSentPeers, skewing later diffs.
This commit is contained in:
committed by
Kristoffer Dalby
parent
7d845ef65e
commit
ad2693ff13
@@ -310,7 +310,12 @@ func (b *Batcher) AddNode(
|
|||||||
// and we want to avoid the race condition where the receiver isn't ready yet
|
// and we want to avoid the race condition where the receiver isn't ready yet
|
||||||
select {
|
select {
|
||||||
case c <- initialMap:
|
case c <- initialMap:
|
||||||
// Success
|
// Record sent peers only after confirmed delivery, mirroring the async
|
||||||
|
// path, and under workMu so a concurrent async bundle for this node
|
||||||
|
// cannot interleave its own lastSentPeers update.
|
||||||
|
nodeConn.workMu.Lock()
|
||||||
|
nodeConn.updateSentPeers(initialMap)
|
||||||
|
nodeConn.workMu.Unlock()
|
||||||
case <-time.After(5 * time.Second): //nolint:mnd
|
case <-time.After(5 * time.Second): //nolint:mnd
|
||||||
nlog.Error().Err(ErrInitialMapSendTimeout).Msg("initial map send timeout")
|
nlog.Error().Err(ErrInitialMapSendTimeout).Msg("initial map send timeout")
|
||||||
nlog.Debug().Caller().Dur("timeout.duration", 5*time.Second). //nolint:mnd
|
nlog.Debug().Caller().Dur("timeout.duration", 5*time.Second). //nolint:mnd
|
||||||
@@ -483,9 +488,11 @@ func (b *Batcher) worker(workerID int) {
|
|||||||
Uint64(zf.NodeID, w.nodeID.Uint64()).
|
Uint64(zf.NodeID, w.nodeID.Uint64()).
|
||||||
Str(zf.Reason, w.changes[0].Reason).
|
Str(zf.Reason, w.changes[0].Reason).
|
||||||
Msg("failed to generate map response for synchronous work")
|
Msg("failed to generate map response for synchronous work")
|
||||||
} else if result.mapResponse != nil {
|
|
||||||
nc.updateSentPeers(result.mapResponse)
|
|
||||||
}
|
}
|
||||||
|
// Peer tracking is recorded by the caller (AddNode) only
|
||||||
|
// after the initial map is actually delivered; recording it
|
||||||
|
// here, before delivery, would leave phantom lastSentPeers
|
||||||
|
// if the send times out.
|
||||||
|
|
||||||
nc.workMu.Unlock()
|
nc.workMu.Unlock()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package mapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSyncInitialMapNoPhantomPeersOnTimeout ensures the synchronous initial-map
|
||||||
|
// path does not record peers as sent until the map is actually delivered. When
|
||||||
|
// the AddNode channel send times out, the client received nothing, so
|
||||||
|
// lastSentPeers must stay empty; otherwise future computePeerDiff calculations
|
||||||
|
// miss peer additions or removals after reconnect.
|
||||||
|
func TestSyncInitialMapNoPhantomPeersOnTimeout(t *testing.T) {
|
||||||
|
testData, cleanup := setupBatcherWithTestData(t, NewBatcherAndMapper, 1, 2, normalBufferSize)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
batcher := testData.Batcher.Batcher
|
||||||
|
state := testData.State
|
||||||
|
|
||||||
|
peerNode := &testData.Nodes[0]
|
||||||
|
targetNode := &testData.Nodes[1]
|
||||||
|
|
||||||
|
state.Connect(peerNode.n.ID)
|
||||||
|
|
||||||
|
err := batcher.AddNode(peerNode.n.ID, peerNode.ch, tailcfg.CapabilityVersion(100), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("adding peer node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range peerNode.ch {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
state.Connect(targetNode.n.ID)
|
||||||
|
|
||||||
|
// Unbuffered channel that nobody reads: AddNode blocks on the initial-map
|
||||||
|
// send and times out.
|
||||||
|
unreadCh := make(chan *tailcfg.MapResponse)
|
||||||
|
|
||||||
|
err = batcher.AddNode(targetNode.n.ID, unreadCh, tailcfg.CapabilityVersion(100), nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected initial-map send timeout error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc, exists := batcher.nodes.Load(targetNode.n.ID)
|
||||||
|
if !exists || nc == nil {
|
||||||
|
t.Fatalf("expected node %d to be retained in b.nodes", targetNode.n.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nc.hasActiveConnections() {
|
||||||
|
t.Fatalf("expected node %d to have no active connections after timeout", targetNode.n.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var phantom []tailcfg.NodeID
|
||||||
|
|
||||||
|
nc.lastSentPeers.Range(func(id tailcfg.NodeID, _ struct{}) bool {
|
||||||
|
phantom = append(phantom, id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(phantom) != 0 {
|
||||||
|
t.Errorf("lastSentPeers must be empty after a failed initial-map delivery, got %d: %v",
|
||||||
|
len(phantom), phantom)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user