mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-21 07:44:52 +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
@@ -23,6 +23,16 @@ import (
|
||||
// after this error because the data was never delivered to any client.
|
||||
var errNoActiveConnections = errors.New("no active connections")
|
||||
|
||||
// errNoReadyConnections is returned by [multiChannelNodeConn.send] when the
|
||||
// node's only connections are still waiting for their initial map
|
||||
// ([Batcher.AddNode] has registered them but not yet delivered the first full
|
||||
// response). Sending a delta now would make it the stream's first frame, which
|
||||
// Tailscale clients reject ("initial MapResponse lacked Node") — tearing down
|
||||
// the poll. Unlike [errNoActiveConnections], the change must be retried: the
|
||||
// in-flight initial map may have been generated from a snapshot older than
|
||||
// the change, so dropping it would lose the update.
|
||||
var errNoReadyConnections = errors.New("no connections ready for updates")
|
||||
|
||||
// connectionEntry represents a single connection to a node.
|
||||
type connectionEntry struct {
|
||||
id string // unique connection ID
|
||||
@@ -32,6 +42,13 @@ type connectionEntry struct {
|
||||
stop func()
|
||||
lastUsed atomic.Int64 // Unix timestamp of last successful send
|
||||
closed atomic.Bool // Indicates if this connection has been closed
|
||||
|
||||
// pendingInitial is set by [Batcher.AddNode] while this
|
||||
// connection's initial map is still in flight, and cleared once it
|
||||
// is delivered. Broadcast sends skip such connections so a delta
|
||||
// can never become the stream's first frame ahead of the initial
|
||||
// map. The zero value means the connection is ready.
|
||||
pendingInitial atomic.Bool
|
||||
}
|
||||
|
||||
// multiChannelNodeConn manages multiple concurrent connections for a single node.
|
||||
@@ -225,6 +242,17 @@ func (mc *multiChannelNodeConn) appendPending(changes ...change.Change) {
|
||||
mc.pendingMu.Unlock()
|
||||
}
|
||||
|
||||
// prependPending puts changes at the head of the pending list, ahead of
|
||||
// anything queued since. Used to retry changes that could not be
|
||||
// delivered yet (initial map in flight): they were emitted before the
|
||||
// currently pending ones, and order matters for stateful patches like
|
||||
// online/offline.
|
||||
func (mc *multiChannelNodeConn) prependPending(changes ...change.Change) {
|
||||
mc.pendingMu.Lock()
|
||||
mc.pending = append(changes, mc.pending...)
|
||||
mc.pendingMu.Unlock()
|
||||
}
|
||||
|
||||
// drainPending atomically removes and returns all pending changes.
|
||||
// Returns nil if there are no pending changes.
|
||||
func (mc *multiChannelNodeConn) drainPending() []change.Change {
|
||||
@@ -260,11 +288,27 @@ func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error {
|
||||
return errNoActiveConnections
|
||||
}
|
||||
|
||||
// Copy the slice so we can release the read lock before sending.
|
||||
snapshot := make([]*connectionEntry, len(mc.connections))
|
||||
copy(snapshot, mc.connections)
|
||||
// Copy only connections whose initial map has been delivered.
|
||||
// A connection still awaiting its initial map receives one
|
||||
// (generated from the current snapshot) from [Batcher.AddNode];
|
||||
// pushing this update at it now would deliver a delta as the
|
||||
// stream's first frame.
|
||||
snapshot := make([]*connectionEntry, 0, len(mc.connections))
|
||||
|
||||
for _, conn := range mc.connections {
|
||||
if !conn.pendingInitial.Load() {
|
||||
snapshot = append(snapshot, conn)
|
||||
}
|
||||
}
|
||||
mc.mutex.RUnlock()
|
||||
|
||||
if len(snapshot) == 0 {
|
||||
mc.log.Trace().
|
||||
Msg("send: connections present but none ready, requeue")
|
||||
|
||||
return errNoReadyConnections
|
||||
}
|
||||
|
||||
mc.log.Trace().
|
||||
Int("total_connections", len(snapshot)).
|
||||
Msg("send: broadcasting")
|
||||
|
||||
Reference in New Issue
Block a user