From df747f1c1b24057de03844ba0561e41123de7c27 Mon Sep 17 00:00:00 2001 From: "M. J. Fromberger" Date: Wed, 24 Sep 2025 09:14:41 -0700 Subject: [PATCH] util/eventbus: add a Done method to the Monitor type (#17263) Some systems need to tell whether the monitored goroutine has finished alongside other channel operations (notably in this case the relay server, but there seem likely to be others similarly situated). Updates #15160 Change-Id: I5f0f3fae827b07f9b7102a3b08f60cda9737fe28 Signed-off-by: M. J. Fromberger --- util/eventbus/bus_test.go | 27 +++++++++++++++++++++++++-- util/eventbus/monitor.go | 14 +++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/util/eventbus/bus_test.go b/util/eventbus/bus_test.go index 67f68cd4a..f9e7ee3dd 100644 --- a/util/eventbus/bus_test.go +++ b/util/eventbus/bus_test.go @@ -236,6 +236,17 @@ func TestMonitor(t *testing.T) { } }) + t.Run("ZeroDone", func(t *testing.T) { + var zero eventbus.Monitor + + select { + case <-zero.Done(): + // OK + case <-time.After(time.Second): + t.Fatal("timeout waiting for zero monitor to be done") + } + }) + t.Run("ZeroClose", func(t *testing.T) { var zero eventbus.Monitor @@ -276,7 +287,13 @@ func TestMonitor(t *testing.T) { // While the goroutine is running, Wait does not complete. select { case <-done: - t.Error("monitor is ready before its goroutine is finished") + t.Error("monitor is ready before its goroutine is finished (Wait)") + default: + // OK + } + select { + case <-m.Done(): + t.Error("monitor is ready before its goroutine is finished (Done)") default: // OK } @@ -286,7 +303,13 @@ func TestMonitor(t *testing.T) { case <-done: // OK case <-time.After(time.Second): - t.Fatal("timeout waiting for monitor to complete") + t.Fatal("timeout waiting for monitor to complete (Wait)") + } + select { + case <-m.Done(): + // OK + case <-time.After(time.Second): + t.Fatal("timeout waiting for monitor to complete (Done)") } } } diff --git a/util/eventbus/monitor.go b/util/eventbus/monitor.go index 18cc2a413..db6fe1be4 100644 --- a/util/eventbus/monitor.go +++ b/util/eventbus/monitor.go @@ -3,9 +3,12 @@ package eventbus +import "tailscale.com/syncs" + // A Monitor monitors the execution of a goroutine processing events from a // [Client], allowing the caller to block until it is complete. The zero value -// of m is valid and its Close and Wait methods return immediately. +// of m is valid; its Close and Wait methods return immediately, and its Done +// method returns an already-closed channel. type Monitor struct { // These fields are immutable after initialization cli *Client @@ -32,6 +35,15 @@ func (m Monitor) Wait() { <-m.done } +// Done returns a channel that is closed when the monitored goroutine has +// finished executing. +func (m Monitor) Done() <-chan struct{} { + if m.done == nil { + return syncs.ClosedChan() + } + return m.done +} + // Monitor executes f in a new goroutine attended by a [Monitor]. The caller // is responsible for waiting for the goroutine to complete, by calling either // [Monitor.Close] or [Monitor.Wait].