mapper, change: coalesce duplicate policy recomputes per tick
Deduplicate broadcast policy changes within a tick so peers don't recompute their netmap more than needed during a reconnect storm. Updates #3293
This commit is contained in:
parent
7706552c99
commit
cffdb77c8b
4 changed files with 197 additions and 0 deletions
|
|
@ -250,6 +250,41 @@ func FilterForNode(nodeID types.NodeID, rs []Change) []Change {
|
|||
return result
|
||||
}
|
||||
|
||||
// IsBroadcastPolicyChange reports whether r is a tailnet-wide policy recompute
|
||||
// with no per-node payload. A recompute reads the current snapshot, so every
|
||||
// such change is interchangeable and same-tick duplicates are redundant. A
|
||||
// targeted or self-update ([Change.OriginNode]) recompute is per-node, so it is
|
||||
// not one of these.
|
||||
func (r Change) IsBroadcastPolicyChange() bool {
|
||||
return r.RequiresRuntimePeerComputation && !r.IsTargetedToNode() && r.OriginNode == 0
|
||||
}
|
||||
|
||||
// DedupePolicyChanges keeps the first broadcast policy change in a tick and
|
||||
// drops the rest: each rebuilds a node's whole netmap from the same snapshot, so
|
||||
// the repeats are wasted work. Order and all other changes are preserved.
|
||||
func DedupePolicyChanges(changes []Change) []Change {
|
||||
if len(changes) < 2 {
|
||||
return changes
|
||||
}
|
||||
|
||||
out := make([]Change, 0, len(changes))
|
||||
seen := false
|
||||
|
||||
for _, r := range changes {
|
||||
if r.IsBroadcastPolicyChange() {
|
||||
if seen {
|
||||
continue
|
||||
}
|
||||
|
||||
seen = true
|
||||
}
|
||||
|
||||
out = append(out, r)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func uniqueNodeIDs(ids []types.NodeID) []types.NodeID {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -296,6 +296,99 @@ func TestChange_Merge(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChange_IsBroadcastPolicyChange(t *testing.T) {
|
||||
originUpdate := PolicyChange()
|
||||
originUpdate.OriginNode = 7
|
||||
|
||||
targeted := PolicyChange()
|
||||
targeted.TargetNode = 7
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
c Change
|
||||
want bool
|
||||
}{
|
||||
{name: "policy change", c: PolicyChange(), want: true},
|
||||
{name: "self-update recompute", c: originUpdate, want: false},
|
||||
{name: "targeted recompute", c: targeted, want: false},
|
||||
{name: "online patch", c: NodeOnline(1), want: false},
|
||||
{name: "full update", c: FullUpdate(), want: false},
|
||||
{name: "derp map", c: DERPMap(), want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, tt.c.IsBroadcastPolicyChange())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDedupePolicyChanges(t *testing.T) {
|
||||
// originRecompute is a runtime recompute carrying node-specific payload
|
||||
// (OriginNode), so it is not the canonical broadcast PolicyChange and must
|
||||
// never be coalesced away.
|
||||
originRecompute := PolicyChange()
|
||||
originRecompute.OriginNode = 7
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
changes []Change
|
||||
want []Change
|
||||
}{
|
||||
{
|
||||
name: "nil is a no-op",
|
||||
changes: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single policy change is unchanged",
|
||||
changes: []Change{PolicyChange()},
|
||||
want: []Change{PolicyChange()},
|
||||
},
|
||||
{
|
||||
name: "identical policy changes collapse to one",
|
||||
changes: []Change{PolicyChange(), PolicyChange(), PolicyChange()},
|
||||
want: []Change{PolicyChange()},
|
||||
},
|
||||
{
|
||||
name: "peer patches survive between collapsed policy changes",
|
||||
changes: []Change{
|
||||
NodeOnline(1), PolicyChange(), NodeOnline(2), PolicyChange(), NodeOffline(3),
|
||||
},
|
||||
want: []Change{
|
||||
NodeOnline(1), PolicyChange(), NodeOnline(2), NodeOffline(3),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NodeAdded is preserved, not treated as a recompute",
|
||||
changes: []Change{PolicyChange(), NodeAdded(5), PolicyChange()},
|
||||
want: []Change{PolicyChange(), NodeAdded(5)},
|
||||
},
|
||||
{
|
||||
name: "recompute carrying OriginNode is kept alongside the canonical one",
|
||||
changes: []Change{PolicyChange(), originRecompute, PolicyChange()},
|
||||
want: []Change{PolicyChange(), originRecompute},
|
||||
},
|
||||
{
|
||||
name: "non-canonical recomputes are not collapsed",
|
||||
changes: []Change{originRecompute, originRecompute},
|
||||
want: []Change{originRecompute, originRecompute},
|
||||
},
|
||||
{
|
||||
name: "changes without any recompute are unchanged",
|
||||
changes: []Change{NodeOnline(1), DERPMap()},
|
||||
want: []Change{NodeOnline(1), DERPMap()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DedupePolicyChanges(tt.changes)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChange_Constructors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue