WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
4 changed files with 130 additions and 3 deletions
Showing only changes of commit 2051c5f358 - Show all commits
+30
View File
@@ -88,6 +88,36 @@ func (q *ExecQueue) Shutdown() {
}
}
// 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())
+54
View File
@@ -7,6 +7,8 @@ import (
"context"
"sync/atomic"
"testing"
"testing/synctest"
"time"
)
func TestExecQueue(t *testing.T) {
@@ -29,3 +31,55 @@ func TestExecQueueRunSyncLocking(t *testing.T) {
q.Shutdown()
})
}
func TestShutdownAndWait(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
q := &ExecQueue{}
started := make(chan struct{})
release := make(chan struct{})
var finished, ranPending atomic.Bool
q.Add(func() {
close(started)
<-release
finished.Store(true)
})
q.Add(func() { ranPending.Store(true) })
<-started
// The fake clock only advances once ShutdownAndWait below is
// blocked, so the release cannot fire early.
go func() {
time.Sleep(time.Second)
close(release)
}()
if err := q.ShutdownAndWait(context.Background()); err != nil {
t.Fatalf("ShutdownAndWait: %v", err)
}
if !finished.Load() {
t.Error("ShutdownAndWait returned before the in-flight function completed")
}
if ranPending.Load() {
t.Error("pending function ran after shutdown")
}
})
}
func TestShutdownAndWaitTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
q := &ExecQueue{}
started := make(chan struct{})
release := make(chan struct{})
q.Add(func() {
close(started)
<-release
})
<-started
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := q.ShutdownAndWait(ctx); err == nil {
t.Error("ShutdownAndWait = nil; want deadline exceeded")
}
close(release)
})
}
+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()
+38
View File
@@ -12,6 +12,7 @@ import (
"slices"
"sync"
"testing"
"time"
"github.com/tailscale/wireguard-go/device"
"go4.org/mem"
@@ -643,3 +644,40 @@ func TestLinkChangeReapplyPreservesMagicDNSRoutes(t *testing.T) {
logger.AsJSON(initial), logger.AsJSON(after))
}
}
// TestCloseWaitsForLinkChange tests that Close waits for in-flight
// linkChangeQueue work to finish before tearing down the subsystems
// that linkChange uses.
//
// See https://github.com/tailscale/tailscale/issues/17641.
func TestCloseWaitsForLinkChange(t *testing.T) {
bus := eventbustest.NewBus(t)
ht := health.NewTracker(bus)
reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus)
if err != nil {
t.Fatal(err)
}
started := make(chan struct{})
release := make(chan struct{})
done := make(chan struct{})
e.(*userspaceEngine).linkChangeQueue.Add(func() {
close(started)
<-release
close(done)
})
<-started
go func() {
time.Sleep(50 * time.Millisecond)
close(release)
}()
e.Close()
select {
case <-done:
default:
t.Fatal("Close returned with link change work still in flight")
}
}