Files
tailscale/util/coalescedop/coalescedop.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

67 lines
1.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package coalescedop provides a mechanism to serialize and coalesce
// calls to a function. At most one call is in flight at a time, and
// if additional calls are requested while one is running, exactly one
// more execution is scheduled after the current one completes.
//
// Unlike [singleflight.Group], which deduplicates concurrent callers
// onto a single in-flight call, [CoalescedOp] ensures that a new
// execution occurs after the current one finishes, so changes that
// happened during the in-flight call are picked up by the next run.
package coalescedop
import "sync"
// CoalescedOp serializes and coalesces calls to a function.
// At most one call is in flight at a time. If additional calls
// are requested while one is running, exactly one more execution
// is scheduled (not one per request).
//
// The zero value is not valid; use [New].
type CoalescedOp struct {
mu sync.Mutex
fn func()
running bool
pending bool
}
// New returns a CoalescedOp that calls fn.
func New(fn func()) *CoalescedOp {
return &CoalescedOp{fn: fn}
}
// Do requests that the operation be run. If no execution is in
// progress, a new goroutine is started to run the function. If an
// execution is already in progress, the function will be run exactly
// once more after the current execution completes.
//
// Do never blocks.
func (c *CoalescedOp) Do() {
c.mu.Lock()
if c.running {
c.pending = true
c.mu.Unlock()
return
}
c.running = true
c.mu.Unlock()
go c.run()
}
func (c *CoalescedOp) run() {
for {
c.fn()
c.mu.Lock()
if !c.pending {
c.running = false
c.mu.Unlock()
return
}
c.pending = false
c.mu.Unlock()
}
}