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:
committed by
Dave Anderson
parent
f840aad49e
commit
3e18434595
+106
-77
@@ -4,46 +4,59 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type dispatchFn func(vals *queue, stop goroutineShutdownWorker, acceptCh func() chan any) bool
|
||||
// subscriber is a uniformly typed wrapper around Subscriber[T], so
|
||||
// that debugging facilities can look at active subscribers.
|
||||
type subscriber interface {
|
||||
subscribeType() reflect.Type
|
||||
// dispatch is a function that dispatches the head value in vals to
|
||||
// a subscriber, while also handling stop and incoming queue write
|
||||
// events.
|
||||
//
|
||||
// dispatch exists because of the strongly typed Subscriber[T]
|
||||
// wrapper around subscriptions: within the bus events are boxed in an
|
||||
// 'any', and need to be unpacked to their full type before delivery
|
||||
// to the subscriber. This involves writing to a strongly-typed
|
||||
// channel, so subscribeState cannot handle that dispatch by itself -
|
||||
// but if that strongly typed send blocks, we also need to keep
|
||||
// processing other potential sources of wakeups, which is how we end
|
||||
// up at this awkward type signature and sharing of internal state
|
||||
// through dispatch.
|
||||
dispatch(vals *queue, stop goroutineShutdownWorker, acceptCh func() chan any) bool
|
||||
Close()
|
||||
}
|
||||
|
||||
// A Queue receives events from a Bus.
|
||||
//
|
||||
// To receive events through the queue, see [Subscribe]. Subscribers
|
||||
// that share the same Queue receive events one at time, in the order
|
||||
// they were published.
|
||||
type Queue struct {
|
||||
bus *Bus
|
||||
name string
|
||||
// subscribeState handles dispatching of events received from a Bus.
|
||||
type subscribeState struct {
|
||||
client *Client
|
||||
|
||||
write chan any
|
||||
stop goroutineShutdownControl
|
||||
snapshot chan chan []any
|
||||
|
||||
outputsMu sync.Mutex
|
||||
outputs map[reflect.Type]dispatchFn
|
||||
outputs map[reflect.Type]subscriber
|
||||
}
|
||||
|
||||
func newQueue(b *Bus, name string) *Queue {
|
||||
func newSubscribeState(c *Client) *subscribeState {
|
||||
stopCtl, stopWorker := newGoroutineShutdown()
|
||||
ret := &Queue{
|
||||
bus: b,
|
||||
name: name,
|
||||
ret := &subscribeState{
|
||||
client: c,
|
||||
write: make(chan any),
|
||||
stop: stopCtl,
|
||||
snapshot: make(chan chan []any),
|
||||
outputs: map[reflect.Type]dispatchFn{},
|
||||
outputs: map[reflect.Type]subscriber{},
|
||||
}
|
||||
b.addQueue(ret)
|
||||
go ret.pump(stopWorker)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (q *Queue) pump(stop goroutineShutdownWorker) {
|
||||
func (q *subscribeState) pump(stop goroutineShutdownWorker) {
|
||||
defer stop.Done()
|
||||
var vals queue
|
||||
acceptCh := func() chan any {
|
||||
@@ -55,13 +68,13 @@ func (q *Queue) pump(stop goroutineShutdownWorker) {
|
||||
for {
|
||||
if !vals.Empty() {
|
||||
val := vals.Peek()
|
||||
fn := q.dispatchFn(val)
|
||||
if fn == nil {
|
||||
sub := q.subscriberFor(val)
|
||||
if sub == nil {
|
||||
// Raced with unsubscribe.
|
||||
vals.Drop()
|
||||
continue
|
||||
}
|
||||
if !fn(&vals, stop, acceptCh) {
|
||||
if !sub.dispatch(&vals, stop, acceptCh) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -81,16 +94,74 @@ func (q *Queue) pump(stop goroutineShutdownWorker) {
|
||||
}
|
||||
}
|
||||
|
||||
// A Subscriber delivers one type of event from a [Queue].
|
||||
func (s *subscribeState) addSubscriber(t reflect.Type, sub subscriber) {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
if s.outputs[t] != nil {
|
||||
panic(fmt.Errorf("double subscription for event %s", t))
|
||||
}
|
||||
s.outputs[t] = sub
|
||||
s.client.addSubscriber(t, s)
|
||||
}
|
||||
|
||||
func (s *subscribeState) deleteSubscriber(t reflect.Type) {
|
||||
s.outputsMu.Lock()
|
||||
defer s.outputsMu.Unlock()
|
||||
delete(s.outputs, t)
|
||||
s.client.deleteSubscriber(t, s)
|
||||
}
|
||||
|
||||
func (q *subscribeState) subscriberFor(val any) subscriber {
|
||||
q.outputsMu.Lock()
|
||||
defer q.outputsMu.Unlock()
|
||||
return q.outputs[reflect.TypeOf(val)]
|
||||
}
|
||||
|
||||
// Close closes the subscribeState. Implicitly closes all Subscribers
|
||||
// linked to this state, and any pending events are discarded.
|
||||
func (s *subscribeState) close() {
|
||||
s.stop.StopAndWait()
|
||||
|
||||
var subs map[reflect.Type]subscriber
|
||||
s.outputsMu.Lock()
|
||||
subs, s.outputs = s.outputs, nil
|
||||
s.outputsMu.Unlock()
|
||||
for _, sub := range subs {
|
||||
sub.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// A Subscriber delivers one type of event from a [Client].
|
||||
type Subscriber[T any] struct {
|
||||
recv *Queue
|
||||
read chan T
|
||||
doneCtx context.Context
|
||||
done context.CancelFunc
|
||||
recv *subscribeState
|
||||
read chan T
|
||||
}
|
||||
|
||||
func newSubscriber[T any](r *subscribeState) *Subscriber[T] {
|
||||
t := reflect.TypeFor[T]()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ret := &Subscriber[T]{
|
||||
doneCtx: ctx,
|
||||
done: cancel,
|
||||
recv: r,
|
||||
read: make(chan T),
|
||||
}
|
||||
r.addSubscriber(t, ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) subscribeType() reflect.Type {
|
||||
return reflect.TypeFor[T]()
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) dispatch(vals *queue, stop goroutineShutdownWorker, acceptCh func() chan any) bool {
|
||||
t := vals.Peek().(T)
|
||||
for {
|
||||
// Keep the cases in this select in sync with Queue.pump
|
||||
// Keep the cases in this select in sync with subscribeState.pump
|
||||
// above. The only different should be that this select
|
||||
// delivers a value on s.read.
|
||||
select {
|
||||
@@ -113,58 +184,16 @@ func (s *Subscriber[T]) Events() <-chan T {
|
||||
return s.read
|
||||
}
|
||||
|
||||
// Close shuts down the Subscriber, indicating the caller no longer
|
||||
// wishes to receive these events. After Close, receives on
|
||||
// [Subscriber.Chan] block for ever.
|
||||
// Done returns a channel that is closed when the subscriber is
|
||||
// closed.
|
||||
func (s *Subscriber[T]) Done() <-chan struct{} {
|
||||
return s.doneCtx.Done()
|
||||
}
|
||||
|
||||
// Close closes the Subscriber, indicating the caller no longer wishes
|
||||
// to receive this event type. After Close, receives on
|
||||
// [Subscriber.Events] block for ever.
|
||||
func (s *Subscriber[T]) Close() {
|
||||
t := reflect.TypeFor[T]()
|
||||
s.recv.bus.unsubscribe(t, s.recv)
|
||||
s.recv.deleteDispatchFn(t)
|
||||
}
|
||||
|
||||
func (q *Queue) dispatchFn(val any) dispatchFn {
|
||||
q.outputsMu.Lock()
|
||||
defer q.outputsMu.Unlock()
|
||||
return q.outputs[reflect.ValueOf(val).Type()]
|
||||
}
|
||||
|
||||
func (q *Queue) addDispatchFn(t reflect.Type, fn dispatchFn) {
|
||||
q.outputsMu.Lock()
|
||||
defer q.outputsMu.Unlock()
|
||||
if q.outputs[t] != nil {
|
||||
panic(fmt.Errorf("double subscription for event %s", t))
|
||||
}
|
||||
q.outputs[t] = fn
|
||||
}
|
||||
|
||||
func (q *Queue) deleteDispatchFn(t reflect.Type) {
|
||||
q.outputsMu.Lock()
|
||||
defer q.outputsMu.Unlock()
|
||||
delete(q.outputs, t)
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the Queue is closed.
|
||||
func (q *Queue) Done() <-chan struct{} {
|
||||
return q.stop.WaitChan()
|
||||
}
|
||||
|
||||
// Close closes the queue. All Subscribers attached to the queue are
|
||||
// implicitly closed, and any pending events are discarded.
|
||||
func (q *Queue) Close() {
|
||||
q.stop.StopAndWait()
|
||||
q.bus.deleteQueue(q)
|
||||
}
|
||||
|
||||
// Subscribe requests delivery of events of type T through the given
|
||||
// Queue. Panics if the queue already has a subscriber for T.
|
||||
func Subscribe[T any](r *Queue) Subscriber[T] {
|
||||
t := reflect.TypeFor[T]()
|
||||
ret := Subscriber[T]{
|
||||
recv: r,
|
||||
read: make(chan T),
|
||||
}
|
||||
r.addDispatchFn(t, ret.dispatch)
|
||||
r.bus.subscribe(t, r)
|
||||
|
||||
return ret
|
||||
s.done() // unblock receivers
|
||||
s.recv.deleteSubscriber(reflect.TypeFor[T]())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user