pomerium/internal/events/target.go
Caleb Doxsey e0693e54f0
core/config: refactor change dispatcher (#4657)
* core/config: refactor change dispatcher

* update test

* close listener go routine when context is canceled

* use cancel cause

* use context

* add more time

* more time
2023-11-01 13:52:23 -06:00

166 lines
4.3 KiB
Go

package events
import (
"context"
"errors"
"sync"
"github.com/google/uuid"
)
type (
// A Listener is a function that listens for events of type T.
Listener[T any] func(ctx context.Context, event T)
// A Handle represents a listener.
Handle string
addListenerEvent[T any] struct {
listener Listener[T]
handle Handle
}
removeListenerEvent[T any] struct {
handle Handle
}
dispatchEvent[T any] struct {
ctx context.Context
event T
}
)
// A Target is a target for events.
//
// Listeners are added with AddListener with a function to be called when the event occurs.
// AddListener returns a Handle which can be used to remove a listener with RemoveListener.
//
// Dispatch dispatches events to all the registered listeners.
//
// Target is safe to use in its zero state.
//
// The first time any method of Target is called a background goroutine is started that handles
// any requests and maintains the state of the listeners. Each listener also starts a
// separate goroutine so that all listeners can be invoked concurrently.
//
// The channels to the main goroutine and to the listener goroutines have a size of 1 so typically
// methods and dispatches will return immediately. However a slow listener will cause the next event
// dispatch to block. This is the opposite behavior from Manager.
//
// Close will cancel all the goroutines. Subsequent calls to AddListener, RemoveListener, Close and
// Dispatch are no-ops.
type Target[T any] struct {
initOnce sync.Once
ctx context.Context
cancel context.CancelCauseFunc
addListenerCh chan addListenerEvent[T]
removeListenerCh chan removeListenerEvent[T]
dispatchCh chan dispatchEvent[T]
listeners map[Handle]chan dispatchEvent[T]
}
// AddListener adds a listener to the target.
func (t *Target[T]) AddListener(listener Listener[T]) Handle {
t.init()
// using a handle is necessary because you can't use a function as a map key.
handle := Handle(uuid.NewString())
select {
case <-t.ctx.Done():
case t.addListenerCh <- addListenerEvent[T]{listener, handle}:
}
return handle
}
// Close closes the event target. This can be called multiple times safely.
// Once closed the target cannot be used.
func (t *Target[T]) Close() {
t.init()
t.cancel(errors.New("target closed"))
}
// Dispatch dispatches an event to all listeners.
func (t *Target[T]) Dispatch(ctx context.Context, evt T) {
t.init()
select {
case <-t.ctx.Done():
case t.dispatchCh <- dispatchEvent[T]{ctx: ctx, event: evt}:
}
}
// RemoveListener removes a listener from the target.
func (t *Target[T]) RemoveListener(handle Handle) {
t.init()
select {
case <-t.ctx.Done():
case t.removeListenerCh <- removeListenerEvent[T]{handle}:
}
}
func (t *Target[T]) init() {
t.initOnce.Do(func() {
t.ctx, t.cancel = context.WithCancelCause(context.Background())
t.addListenerCh = make(chan addListenerEvent[T], 1)
t.removeListenerCh = make(chan removeListenerEvent[T], 1)
t.dispatchCh = make(chan dispatchEvent[T], 1)
t.listeners = map[Handle]chan dispatchEvent[T]{}
go t.run()
})
}
func (t *Target[T]) run() {
// listen for add/remove/dispatch events and call functions
for {
select {
case <-t.ctx.Done():
return
case evt := <-t.addListenerCh:
t.addListener(evt.listener, evt.handle)
case evt := <-t.removeListenerCh:
t.removeListener(evt.handle)
case evt := <-t.dispatchCh:
t.dispatch(evt.ctx, evt.event)
}
}
}
// these functions are not thread-safe. They are intended to be called only by "run".
func (t *Target[T]) addListener(listener Listener[T], handle Handle) {
ch := make(chan dispatchEvent[T], 1)
t.listeners[handle] = ch
// start a goroutine to send events to the listener
go func() {
for {
select {
case <-t.ctx.Done():
case evt := <-ch:
listener(evt.ctx, evt.event)
}
}
}()
}
func (t *Target[T]) removeListener(handle Handle) {
ch, ok := t.listeners[handle]
if !ok {
// nothing to do since the listener doesn't exist
return
}
// close the channel to kill the goroutine
close(ch)
delete(t.listeners, handle)
}
func (t *Target[T]) dispatch(ctx context.Context, evt T) {
// loop over all the listeners and send the event to them
for _, ch := range t.listeners {
select {
case <-t.ctx.Done():
return
case ch <- dispatchEvent[T]{ctx: ctx, event: evt}:
}
}
}