mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-23 03:37:19 +09:00
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>
92 lines
1.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|