@ -15,8 +15,8 @@ import (
// Bus is an event bus that distributes published events to interested
// Bus is an event bus that distributes published events to interested
// subscribers.
// subscribers.
type Bus struct {
type Bus struct {
router * worker
write chan any
write chan any
stop goroutineShutdownControl
snapshot chan chan [ ] any
snapshot chan chan [ ] any
topicsMu sync . Mutex // guards everything below.
topicsMu sync . Mutex // guards everything below.
@ -30,15 +30,13 @@ type Bus struct {
// New returns a new bus. Use [PublisherOf] to make event publishers,
// New returns a new bus. Use [PublisherOf] to make event publishers,
// and [Bus.Queue] and [Subscribe] to make event subscribers.
// and [Bus.Queue] and [Subscribe] to make event subscribers.
func New ( ) * Bus {
func New ( ) * Bus {
stopCtl , stopWorker := newGoroutineShutdown ( )
ret := & Bus {
ret := & Bus {
write : make ( chan any ) ,
write : make ( chan any ) ,
stop : stopCtl ,
snapshot : make ( chan chan [ ] any ) ,
snapshot : make ( chan chan [ ] any ) ,
topics : map [ reflect . Type ] [ ] * subscribeState { } ,
topics : map [ reflect . Type ] [ ] * subscribeState { } ,
clients : set . Set [ * Client ] { } ,
clients : set . Set [ * Client ] { } ,
}
}
go ret . pump ( stopWorker )
ret . router = runWorker ( ret . pump )
return ret
return ret
}
}
@ -67,7 +65,7 @@ func (b *Bus) Client(name string) *Client {
// Close blocks until the bus is fully shut down. The bus is
// Close blocks until the bus is fully shut down. The bus is
// permanently unusable after closing.
// permanently unusable after closing.
func ( b * Bus ) Close ( ) {
func ( b * Bus ) Close ( ) {
b . stop . StopAndWait ( )
b . router . StopAndWait ( )
var clients set . Set [ * Client ]
var clients set . Set [ * Client ]
b . topicsMu . Lock ( )
b . topicsMu . Lock ( )
@ -79,8 +77,7 @@ func (b *Bus) Close() {
}
}
}
}
func ( b * Bus ) pump ( stop goroutineShutdownWorker ) {
func ( b * Bus ) pump ( ctx context . Context ) {
defer stop . Done ( )
var vals queue
var vals queue
acceptCh := func ( ) chan any {
acceptCh := func ( ) chan any {
if vals . Full ( ) {
if vals . Full ( ) {
@ -102,13 +99,13 @@ func (b *Bus) pump(stop goroutineShutdownWorker) {
select {
select {
case d . write <- val :
case d . write <- val :
break deliverOne
break deliverOne
case <- d . stop . WaitChan ( ) :
case <- d . closed ( ) :
// Queue closed, don't block but continue
// Queue closed, don't block but continue
// delivering to others.
// delivering to others.
break deliverOne
break deliverOne
case in := <- acceptCh ( ) :
case in := <- acceptCh ( ) :
vals . Add ( in )
vals . Add ( in )
case <- stop. Stop ( ) :
case <- ctx. Done ( ) :
return
return
case ch := <- b . snapshot :
case ch := <- b . snapshot :
ch <- vals . Snapshot ( )
ch <- vals . Snapshot ( )
@ -122,7 +119,7 @@ func (b *Bus) pump(stop goroutineShutdownWorker) {
// resuming.
// resuming.
for vals . Empty ( ) {
for vals . Empty ( ) {
select {
select {
case <- stop. Stop ( ) :
case <- ctx. Done ( ) :
return
return
case val := <- b . write :
case val := <- b . write :
vals . Add ( val )
vals . Add ( val )
@ -168,59 +165,89 @@ func (b *Bus) unsubscribe(t reflect.Type, q *subscribeState) {
b . topics [ t ] = slices . Delete ( slices . Clone ( b . topics [ t ] ) , i , i + 1 )
b . topics [ t ] = slices . Delete ( slices . Clone ( b . topics [ t ] ) , i , i + 1 )
}
}
func newGoroutineShutdown ( ) ( goroutineShutdownControl , goroutineShutdownWorker ) {
// A worker runs a worker goroutine and helps coordinate its shutdown.
ctx , cancel := context . WithCancel ( context . Background ( ) )
type worker struct {
ctx context . Context
stop context . CancelFunc
stopped chan struct { }
}
ctl := goroutineShutdownControl {
// runWorker creates a worker goroutine running fn. The context passed
startShutdown : cancel ,
// to fn is canceled by [worker.Stop].
shutdownFinished : make ( chan struct { } ) ,
func runWorker ( fn func ( context . Context ) ) * worker {
ctx , stop := context . WithCancel ( context . Background ( ) )
ret := & worker {
ctx : ctx ,
stop : stop ,
stopped : make ( chan struct { } ) ,
}
}
work := goroutineShutdownWorker {
go ret . run ( fn )
startShutdown : ctx . Done ( ) ,
return ret
shutdownFinished : ctl . shutdownFinished ,
}
}
return ctl , work
func ( w * worker ) run ( fn func ( context . Context ) ) {
defer close ( w . stopped )
fn ( w . ctx )
}
}
// goroutineShutdownControl is a helper type to manage the shutdown of
// Stop signals the worker goroutine to shut down.
// a worker goroutine. The worker goroutine should use the
func ( w * worker ) Stop ( ) { w . stop ( ) }
// goroutineShutdownWorker related to this controller.
type goroutineShutdownControl struct {
startShutdown context . CancelFunc
shutdownFinished chan struct { }
}
func ( ctl * goroutineShutdownControl ) Stop ( ) {
// Done returns a channel that is closed when the worker goroutine
ctl . startShutdown ( )
// exits.
}
func ( w * worker ) Done ( ) <- chan struct { } { return w . stopped }
func ( ctl * goroutineShutdownControl ) Wait ( ) {
// Wait waits until the worker goroutine has exited.
<- ctl . shutdownFinished
func ( w * worker ) Wait ( ) { <- w . stopped }
}
func ( ctl * goroutineShutdownControl ) WaitChan ( ) <- chan struct { } {
// StopAndWait signals the worker goroutine to shut down, then waits
return ctl . shutdownFinished
// for it to exit.
func ( w * worker ) StopAndWait ( ) {
w . stop ( )
<- w . stopped
}
}
func ( ctl * goroutineShutdownControl ) StopAndWait ( ) {
// stopFlag is a value that can be watched for a notification. The
ctl . Stop ( )
// zero value is ready for use.
ctl . Wait ( )
//
// The flag is notified by running [stopFlag.Stop]. Stop can be called
// multiple times. Upon the first call to Stop, [stopFlag.Done] is
// closed, all pending [stopFlag.Wait] calls return, and future Wait
// calls return immediately.
//
// A stopFlag can only notify once, and is intended for use as a
// one-way shutdown signal that's lighter than a cancellable
// context.Context.
type stopFlag struct {
// guards the lazy construction of stopped, and the value of
// alreadyStopped.
mu sync . Mutex
stopped chan struct { }
alreadyStopped bool
}
func ( s * stopFlag ) Stop ( ) {
s . mu . Lock ( )
defer s . mu . Unlock ( )
if s . alreadyStopped {
return
}
}
s . alreadyStopped = true
// goroutineShutdownWorker is a helper type for a worker goroutine to
if s . stopped == nil {
// be notified that it should shut down, and to report that shutdown
s . stopped = make ( chan struct { } )
// has completed. The notification is triggered by the related
}
// goroutineShutdownControl.
close ( s . stopped )
type goroutineShutdownWorker struct {
startShutdown <- chan struct { }
shutdownFinished chan struct { }
}
}
func ( work * goroutineShutdownWorker ) Stop ( ) <- chan struct { } {
func ( s * stopFlag ) Done ( ) <- chan struct { } {
return work . startShutdown
s . mu . Lock ( )
defer s . mu . Unlock ( )
if s . stopped == nil {
s . stopped = make ( chan struct { } )
}
return s . stopped
}
}
func ( work * goroutineShutdownWorker ) Done ( ) {
func ( s * stopFlag ) Wait ( ) {
close ( work . shutdownFinished )
<- s . Done ( )
}
}