mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-09 03:11:42 +01:00
A common pattern in event bus usage is to run a goroutine to service a
collection of subscribers on a single bus client. To have an orderly shutdown,
however, we need a way to wait for such a goroutine to be finished.
This commit adds a Monitor type that makes this pattern easier to wire up:
rather than having to track all the subscribers and an extra channel, the
component need only track the client and the monitor. For example:
cli := bus.Client("example")
m := cli.Monitor(func(c *eventbus.Client) {
s1 := eventbus.Subscribe[T](cli)
s2 := eventbus.Subscribe[U](cli)
for {
select {
case <-c.Done():
return
case t := <-s1.Events():
processT(t)
case u := <-s2.Events():
processU(u)
}
}
})
To shut down the client and wait for the goroutine, the caller can write:
m.Close()
which closes cli and waits for the goroutine to finish. Or, separately:
cli.Close()
// do other stuff
m.Wait()
While the goroutine management is not explicitly tied to subscriptions, it is a
common enough pattern that this seems like a useful simplification in use.
Updates #15160
Change-Id: I657afda1cfaf03465a9dce1336e9fd518a968bca
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
43 lines
1.2 KiB
Go
43 lines
1.2 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package eventbus
|
|
|
|
// 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.
|
|
type Monitor struct {
|
|
// These fields are immutable after initialization
|
|
cli *Client
|
|
done <-chan struct{}
|
|
}
|
|
|
|
// Close closes the client associated with m and blocks until the processing
|
|
// goroutine is complete.
|
|
func (m Monitor) Close() {
|
|
if m.cli == nil {
|
|
return
|
|
}
|
|
m.cli.Close()
|
|
<-m.done
|
|
}
|
|
|
|
// Wait blocks until the goroutine monitored by m has finished executing, but
|
|
// does not close the associated client. It is safe to call Wait repeatedly,
|
|
// and from multiple concurrent goroutines.
|
|
func (m Monitor) Wait() {
|
|
if m.done == nil {
|
|
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].
|
|
func (c *Client) Monitor(f func(*Client)) Monitor {
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); f(c) }()
|
|
return Monitor{cli: c, done: done}
|
|
}
|