Files
tailscale/util/execqueue/execqueue.go
Adel-Ayoub 2051c5f358 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>
2026-07-07 06:01:08 -07:00

158 lines
3.2 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package execqueue implements an ordered asynchronous queue for executing functions.
package execqueue
import (
"context"
"errors"
"tailscale.com/syncs"
)
type ExecQueue struct {
mu syncs.Mutex
ctx context.Context // context.Background + closed on Shutdown
cancel context.CancelFunc // closes ctx
closed bool
inFlight bool // whether a goroutine is running q.run
doneWaiter chan struct{} // non-nil if waiter is waiting, then closed
queue []func()
}
func (q *ExecQueue) Add(f func()) {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed {
return
}
q.initCtxLocked()
if q.inFlight {
q.queue = append(q.queue, f)
} else {
q.inFlight = true
go q.run(f)
}
}
// RunSync waits for the queue to be drained and then synchronously runs f.
// It returns an error if the queue is closed before f is run or ctx expires.
func (q *ExecQueue) RunSync(ctx context.Context, f func()) error {
q.mu.Lock()
q.initCtxLocked()
shutdownCtx := q.ctx
q.mu.Unlock()
ch := make(chan struct{})
q.Add(f)
q.Add(func() { close(ch) })
select {
case <-ch:
return nil
case <-ctx.Done():
return ctx.Err()
case <-shutdownCtx.Done():
return errExecQueueShutdown
}
}
func (q *ExecQueue) run(f func()) {
f()
q.mu.Lock()
for len(q.queue) > 0 && !q.closed {
f := q.queue[0]
q.queue[0] = nil
q.queue = q.queue[1:]
q.mu.Unlock()
f()
q.mu.Lock()
}
q.inFlight = false
q.queue = nil
if q.doneWaiter != nil {
close(q.doneWaiter)
q.doneWaiter = nil
}
q.mu.Unlock()
}
// Shutdown asynchronously signals the queue to stop.
func (q *ExecQueue) Shutdown() {
q.mu.Lock()
defer q.mu.Unlock()
q.closed = true
if q.cancel != nil {
q.cancel()
}
}
// ShutdownAndWait signals the queue to stop, discards any queued
// functions that have not started, and waits for the currently
// executing function, if any, to complete or ctx to expire.
//
// It must not be called while holding a lock that a queued function
// may acquire, or it will not return until ctx expires.
func (q *ExecQueue) ShutdownAndWait(ctx context.Context) error {
q.mu.Lock()
q.closed = true
if q.cancel != nil {
q.cancel()
}
waitCh := q.doneWaiter
if q.inFlight && waitCh == nil {
waitCh = make(chan struct{})
q.doneWaiter = waitCh
}
q.mu.Unlock()
if waitCh == nil {
return nil
}
select {
case <-waitCh:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (q *ExecQueue) initCtxLocked() {
if q.ctx == nil {
q.ctx, q.cancel = context.WithCancel(context.Background())
}
}
var errExecQueueShutdown = errors.New("execqueue shut down")
// Wait waits for the queue to be empty or shut down.
func (q *ExecQueue) Wait(ctx context.Context) error {
q.mu.Lock()
q.initCtxLocked()
waitCh := q.doneWaiter
if q.inFlight && waitCh == nil {
waitCh = make(chan struct{})
q.doneWaiter = waitCh
}
closed := q.closed
shutdownCtx := q.ctx
q.mu.Unlock()
if closed {
return errExecQueueShutdown
}
if waitCh == nil {
return nil
}
select {
case <-waitCh:
return nil
case <-shutdownCtx.Done():
return errExecQueueShutdown
case <-ctx.Done():
return ctx.Err()
}
}