mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-09 08:10:06 +09:00
mapper: gate broadcast sends until a connection's initial map is delivered
AddNode registers the connection before sending the initial map, so a batched delta could land first and become the stream's first frame; Tailscale clients then kill the poll with "initial MapResponse lacked Node" and a retry loop under steady change traffic leaves the netmap empty. Skip connections awaiting their initial map and retry the change next tick — the in-flight map may predate it, so it cannot be dropped. Retries are prepended to keep patch order.
This commit is contained in:
committed by
Kristoffer Dalby
parent
7918187e7a
commit
f4eeb94b1c
@@ -1,11 +1,76 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types/change"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TestUnreadyConnectionDefersBroadcastsUntilInitialMap pins the ordering fix
|
||||
// for the "initial MapResponse lacked Node" client failure: a connection
|
||||
// registered by [Batcher.AddNode] but still waiting for its initial map must
|
||||
// not receive broadcast deltas — a delta as the stream's first frame makes the
|
||||
// Tailscale client tear down the poll. The change is requeued, not dropped,
|
||||
// because the in-flight initial map may have been generated from a snapshot
|
||||
// older than the change.
|
||||
func TestUnreadyConnectionDefersBroadcastsUntilInitialMap(t *testing.T) {
|
||||
testData, cleanup := setupBatcherWithTestData(t, NewBatcherAndMapper, 1, 2, normalBufferSize)
|
||||
defer cleanup()
|
||||
|
||||
batcher := testData.Batcher.Batcher
|
||||
peerNode := &testData.Nodes[0]
|
||||
targetNode := &testData.Nodes[1]
|
||||
|
||||
// Register the target's connection by hand in the state AddNode leaves it
|
||||
// in between registering the channel and delivering the initial map.
|
||||
nc := newMultiChannelNodeConn(targetNode.n.ID, batcher.mapper)
|
||||
entry := &connectionEntry{
|
||||
id: "test-unready",
|
||||
c: targetNode.ch,
|
||||
version: tailcfg.CapabilityVersion(100),
|
||||
created: time.Now(),
|
||||
}
|
||||
entry.pendingInitial.Store(true)
|
||||
nc.addConnection(entry)
|
||||
batcher.nodes.Store(targetNode.n.ID, nc)
|
||||
|
||||
// A broadcast lands while the initial map is still in flight: nothing may
|
||||
// reach the channel, and the caller must be told to retry
|
||||
// (the worker prepends such changes back onto pending).
|
||||
retryChange := change.NodeAdded(peerNode.n.ID)
|
||||
|
||||
err := nc.change(retryChange)
|
||||
if !errors.Is(err, errNoReadyConnections) {
|
||||
t.Fatalf("change on unready connection: want errNoReadyConnections, got %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-targetNode.ch:
|
||||
t.Fatalf("unready connection received a frame before its initial map: %+v", resp)
|
||||
default:
|
||||
}
|
||||
|
||||
// Once the initial map is delivered, the retried change goes out.
|
||||
entry.pendingInitial.Store(false)
|
||||
|
||||
err = nc.change(retryChange)
|
||||
if err != nil {
|
||||
t.Fatalf("change on ready connection: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-targetNode.ch:
|
||||
if len(resp.PeersChanged) == 0 {
|
||||
t.Fatalf("expected PeersChanged delta after readiness, got %+v", resp)
|
||||
}
|
||||
default:
|
||||
t.Fatal("ready connection did not receive the retried change")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user