wgengine,util/execqueue: wait for in-flight linkChange before closing

ExecQueue.Shutdown does not wait for a function that is already
executing, so Close could tear down magicConn, dns, wgdev, and tundev
while a queued linkChange was still using them, panicking during
shutdown. Add ExecQueue.ShutdownAndWait, which discards queued
functions that have not started and waits for the in-flight one, and
use it in Close with a bounded context before tearing anything down.
The eventbus client is closed first and is the queue's only producer,
so no new work can arrive after the drain.

Updates #17641

Change-Id: I0350bcb59c1ee4b0dcac88cf66b93828466c8c98
Signed-off-by: Adel-Ayoub <adelayoub.maaziz@gmail.com>
This commit is contained in:
Adel-Ayoub
2026-07-07 06:01:08 -07:00
committed by Brad Fitzpatrick
parent 3d52c3f03e
commit 2051c5f358
4 changed files with 130 additions and 3 deletions
+8 -3
View File
@@ -1222,9 +1222,14 @@ func (e *userspaceEngine) RequestStatus() {
func (e *userspaceEngine) Close() {
e.eventClient.Close()
// TODO(cmol): Should we wait for it too?
// Same question raised in appconnector.go.
e.linkChangeQueue.Shutdown()
// eventClient.Close waited for the ChangeDelta subscriber, the sole
// producer for linkChangeQueue, to return, so no new work can be
// queued. Discard queued linkChanges and wait for an in-flight one
// to finish before closing the subsystems it uses.
// See tailscale/tailscale#17641.
drainCtx, drainCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer drainCancel()
e.linkChangeQueue.ShutdownAndWait(drainCtx)
e.mu.Lock()
if e.closing {
e.mu.Unlock()