util/eventbus: rework to have a Client abstraction

The Client carries both publishers and subscribers for a single
actor. This makes the APIs for publish and subscribe look more
similar, and this structure is a better fit for upcoming debug
facilities.

Updates #15160

Signed-off-by: David Anderson <dave@tailscale.com>
This commit is contained in:
David Anderson
2025-03-04 11:22:30 -08:00
committed by Dave Anderson
parent f840aad49e
commit 3e18434595
6 changed files with 346 additions and 209 deletions

View File

@@ -11,35 +11,41 @@ import (
// publisher is a uniformly typed wrapper around Publisher[T], so that
// debugging facilities can look at active publishers.
type publisher interface {
publisherName() string
publishType() reflect.Type
Close()
}
// A Publisher publishes events on the bus.
// A Publisher publishes typed events on a bus.
type Publisher[T any] struct {
bus *Bus
name string
client *Client
stopCtx context.Context
stop context.CancelFunc
}
// PublisherOf returns a publisher for event type T on the given bus.
//
// The publisher's name should be a short, human-readable string that
// identifies this event publisher. The name is only visible through
// debugging APIs.
func PublisherOf[T any](b *Bus, name string) *Publisher[T] {
func newPublisher[T any](c *Client) *Publisher[T] {
ctx, cancel := context.WithCancel(context.Background())
ret := &Publisher[T]{
bus: b,
name: name,
client: c,
stopCtx: ctx,
stop: cancel,
}
b.addPublisher(ret)
c.addPublisher(ret)
return ret
}
func (p *Publisher[T]) publisherName() string { return p.name }
// Close closes the publisher.
//
// Calls to Publish after Close silently do nothing.
func (p *Publisher[T]) Close() {
// Just unblocks any active calls to Publish, no other
// synchronization needed.
p.stop()
p.client.deletePublisher(p)
}
func (p *Publisher[T]) publishType() reflect.Type {
return reflect.TypeFor[T]()
}
// Publish publishes event v on the bus.
func (p *Publisher[T]) Publish(v T) {
@@ -48,32 +54,21 @@ func (p *Publisher[T]) Publish(v T) {
select {
case <-p.stopCtx.Done():
return
case <-p.bus.stop.WaitChan():
return
default:
}
select {
case p.bus.write <- v:
case p.client.publish() <- v:
case <-p.stopCtx.Done():
case <-p.bus.stop.WaitChan():
}
}
// ShouldPublish reports whether anyone is subscribed to events of
// type T.
// ShouldPublish reports whether anyone is subscribed to the events
// that this publisher emits.
//
// ShouldPublish can be used to skip expensive event construction if
// nobody seems to care. Publishers must not assume that someone will
// definitely receive an event if ShouldPublish returns true.
func (p *Publisher[T]) ShouldPublish() bool {
dests := p.bus.dest(reflect.TypeFor[T]())
return len(dests) > 0
}
// Close closes the publisher, indicating that no further events will
// be published with it.
func (p *Publisher[T]) Close() {
p.stop()
p.bus.deletePublisher(p)
return p.client.shouldPublish(reflect.TypeFor[T]())
}