Files
tailscale/util/coalescedop/coalescedop_test.go
T
Nick Khyl ab2380a631 net/dns,util/coalescedop: serialize and coalesce DNS flush calls on Windows
Previously, ipconfig /flushdns and ipconfig /registerdns were called
from multiple sites without coordination: the DNS manager's SetDNS
(in a fire-and-forget goroutine), the router's winRouter.Set (synchronously
on every route change), the link change handler, and session unlock.
These could fire concurrently or in rapid succession, spawning multiple
ipconfig processes and putting excessive pressure on the Dnscache service.

In this PR, we introduce a new util/coalescedop.CoalescedOp type that provides
"one in-flight, at most one pending" semantics: Do() is non-blocking,
at most one execution runs at a time, and concurrent/rapid successive
requests coalesce into a single follow-up execution.

We then use CoalescedOp for both ipconfig /flushdns and ipconfig /registerdns,
replacing the fire-and-forget goroutine in windowsManager.SetDNS
and in the session unlock handler in tailscaled.

We also remove the redundant dns.Flush() call from winRouter.Set, since every
meaningful router.Set is followed by dns.Set in Reconfig, which already
flushes.

Updates #21001

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-09-30 10:44:22 -07:00

92 lines
1.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package coalescedop_test
import (
"sync/atomic"
"testing"
"testing/synctest"
"time"
"tailscale.com/util/coalescedop"
)
func TestSingleCall(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var n atomic.Int32
done := make(chan struct{})
op := coalescedop.New(func() {
n.Add(1)
close(done)
})
op.Do()
<-done
if got := n.Load(); got != 1 {
t.Errorf("n=%d; want 1", got)
}
})
}
func TestCoalescesWhileRunning(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var n atomic.Int32
release := make(chan struct{})
op := coalescedop.New(func() {
n.Add(1)
<-release
})
// Start the first execution; it blocks on release.
op.Do()
// synctest.Test advances the fake clock only once all
// goroutines in the bubble are blocked, so this Sleep
// reliably waits for the goroutine to be parked on <-release.
time.Sleep(time.Millisecond)
// Request several more while the first is running.
op.Do()
op.Do()
op.Do()
// Release the first execution.
release <- struct{}{}
time.Sleep(time.Millisecond) // wait for coalesced run to block on <-release
// Release the second (coalesced) execution.
release <- struct{}{}
time.Sleep(time.Millisecond) // wait for run() to finish
if got := n.Load(); got != 2 {
t.Errorf("n=%d; want 2 (one original + one coalesced)", got)
}
})
}
func TestNoPendingIfNotRequested(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var n atomic.Int32
done := make(chan struct{}, 1)
op := coalescedop.New(func() {
n.Add(1)
done <- struct{}{}
})
op.Do()
<-done
time.Sleep(time.Millisecond) // wait for run() to finish
// No more executions should happen.
if got := n.Load(); got != 1 {
t.Errorf("n=%d; want 1", got)
}
// A new Do after completion should run again.
op.Do()
<-done
if got := n.Load(); got != 2 {
t.Errorf("n=%d; want 2", got)
}
})
}