feat(master): in-process broker for live UI change signals
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Package events fans out "something changed" signals from the agent hub to
|
||||
// open browser streams.
|
||||
package events
|
||||
|
||||
import "sync"
|
||||
|
||||
// Topic names what changed. Subscribers refetch that part of the UI.
|
||||
type Topic string
|
||||
|
||||
// Topics.
|
||||
const (
|
||||
Services Topic = "services"
|
||||
Agents Topic = "agents"
|
||||
)
|
||||
|
||||
// Broker is an in-process publish/subscribe hub.
|
||||
type Broker struct {
|
||||
mu sync.Mutex
|
||||
subs map[chan Topic]struct{}
|
||||
}
|
||||
|
||||
// NewBroker returns an empty Broker.
|
||||
func NewBroker() *Broker {
|
||||
return &Broker{subs: make(map[chan Topic]struct{})}
|
||||
}
|
||||
|
||||
// Subscribe returns a channel of topics and a cancel function that must be
|
||||
// called when the subscriber goes away. Cancel is safe to call twice.
|
||||
func (b *Broker) Subscribe() (<-chan Topic, func()) {
|
||||
ch := make(chan Topic, 16)
|
||||
b.mu.Lock()
|
||||
b.subs[ch] = struct{}{}
|
||||
b.mu.Unlock()
|
||||
var once sync.Once
|
||||
return ch, func() {
|
||||
once.Do(func() {
|
||||
b.mu.Lock()
|
||||
delete(b.subs, ch)
|
||||
b.mu.Unlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Publish signals t to every subscriber. A subscriber whose buffer is full is
|
||||
// skipped rather than blocking the agent hub; it already has signals queued
|
||||
// and will refetch current state when it processes them.
|
||||
func (b *Broker) Publish(t Topic) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for ch := range b.subs {
|
||||
select {
|
||||
case ch <- t:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBroker_DeliversToAllSubscribers(t *testing.T) {
|
||||
b := NewBroker()
|
||||
a, cancelA := b.Subscribe()
|
||||
defer cancelA()
|
||||
c, cancelC := b.Subscribe()
|
||||
defer cancelC()
|
||||
|
||||
b.Publish(Services)
|
||||
assert.Equal(t, Services, <-a)
|
||||
assert.Equal(t, Services, <-c)
|
||||
}
|
||||
|
||||
func TestBroker_CancelStopsDelivery(t *testing.T) {
|
||||
b := NewBroker()
|
||||
ch, cancel := b.Subscribe()
|
||||
cancel()
|
||||
cancel() // safe to call twice
|
||||
b.Publish(Agents)
|
||||
assert.Empty(t, ch)
|
||||
}
|
||||
|
||||
func TestBroker_SlowSubscriberDoesNotBlockPublish(t *testing.T) {
|
||||
b := NewBroker()
|
||||
_, cancel := b.Subscribe()
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for range 1000 {
|
||||
b.Publish(Services) // must return even though nobody reads
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Publish blocked on a slow subscriber")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user