Simplify the event bus system; address pterodactyl/panel#3903
If my debugging is correct, this should address pterodactyl/panel#3903 in its entirety by addressing a few areas where it was possible for a channel to lock up and cause everything to block
This commit is contained in:
135
events/events.go
135
events/events.go
@@ -2,10 +2,11 @@ package events
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Listener chan Event
|
||||
"emperror.dev/errors"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
// Event represents an Event sent over a Bus.
|
||||
type Event struct {
|
||||
@@ -15,137 +16,51 @@ type Event struct {
|
||||
|
||||
// Bus represents an Event Bus.
|
||||
type Bus struct {
|
||||
listenersMx sync.Mutex
|
||||
listeners map[string][]Listener
|
||||
*system.SinkPool
|
||||
}
|
||||
|
||||
// NewBus returns a new empty Event Bus.
|
||||
// NewBus returns a new empty Bus. This is simply a nicer wrapper around the
|
||||
// system.SinkPool implementation that allows for more simplistic usage within
|
||||
// the codebase.
|
||||
//
|
||||
// All of the events emitted out of this bus are byte slices that can be decoded
|
||||
// back into an events.Event interface.
|
||||
func NewBus() *Bus {
|
||||
return &Bus{
|
||||
listeners: make(map[string][]Listener),
|
||||
}
|
||||
}
|
||||
|
||||
// Off unregisters a listener from the specified topics on the Bus.
|
||||
func (b *Bus) Off(listener Listener, topics ...string) {
|
||||
b.listenersMx.Lock()
|
||||
defer b.listenersMx.Unlock()
|
||||
|
||||
var closed bool
|
||||
|
||||
for _, topic := range topics {
|
||||
ok := b.off(topic, listener)
|
||||
if !closed && ok {
|
||||
close(listener)
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) off(topic string, listener Listener) bool {
|
||||
listeners, ok := b.listeners[topic]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for i, l := range listeners {
|
||||
if l != listener {
|
||||
continue
|
||||
}
|
||||
|
||||
listeners = append(listeners[:i], listeners[i+1:]...)
|
||||
b.listeners[topic] = listeners
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// On registers a listener to the specified topics on the Bus.
|
||||
func (b *Bus) On(listener Listener, topics ...string) {
|
||||
b.listenersMx.Lock()
|
||||
defer b.listenersMx.Unlock()
|
||||
|
||||
for _, topic := range topics {
|
||||
b.on(topic, listener)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bus) on(topic string, listener Listener) {
|
||||
listeners, ok := b.listeners[topic]
|
||||
if !ok {
|
||||
b.listeners[topic] = []Listener{listener}
|
||||
} else {
|
||||
b.listeners[topic] = append(listeners, listener)
|
||||
system.NewSinkPool(),
|
||||
}
|
||||
}
|
||||
|
||||
// Publish publishes a message to the Bus.
|
||||
func (b *Bus) Publish(topic string, data interface{}) {
|
||||
// Some of our topics for the socket support passing a more specific namespace,
|
||||
// Some of our actions for the socket support passing a more specific namespace,
|
||||
// such as "backup completed:1234" to indicate which specific backup was completed.
|
||||
//
|
||||
// In these cases, we still need to send the event using the standard listener
|
||||
// name of "backup completed".
|
||||
if strings.Contains(topic, ":") {
|
||||
parts := strings.SplitN(topic, ":", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
topic = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
b.listenersMx.Lock()
|
||||
defer b.listenersMx.Unlock()
|
||||
|
||||
listeners, ok := b.listeners[topic]
|
||||
if !ok {
|
||||
return
|
||||
enc, err := json.Marshal(Event{Topic: topic, Data: data})
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
if len(listeners) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
event := Event{Topic: topic, Data: data}
|
||||
for _, listener := range listeners {
|
||||
l := listener
|
||||
wg.Add(1)
|
||||
go func(l Listener, event Event) {
|
||||
defer wg.Done()
|
||||
l <- event
|
||||
}(l, event)
|
||||
}
|
||||
wg.Wait()
|
||||
b.Push(enc)
|
||||
}
|
||||
|
||||
// Destroy destroys the Event Bus by unregistering and closing all listeners.
|
||||
func (b *Bus) Destroy() {
|
||||
b.listenersMx.Lock()
|
||||
defer b.listenersMx.Unlock()
|
||||
|
||||
// Track what listeners have already been closed. Because the same listener
|
||||
// can be listening on multiple topics, we need a way to essentially
|
||||
// "de-duplicate" all the listeners across all the topics.
|
||||
var closed []Listener
|
||||
|
||||
for _, listeners := range b.listeners {
|
||||
for _, listener := range listeners {
|
||||
if contains(closed, listener) {
|
||||
continue
|
||||
}
|
||||
|
||||
close(listener)
|
||||
closed = append(closed, listener)
|
||||
}
|
||||
}
|
||||
|
||||
b.listeners = make(map[string][]Listener)
|
||||
// MustDecode decodes the event byte slice back into an events.Event struct or
|
||||
// panics if an error is encountered during this process.
|
||||
func MustDecode(data []byte) (e Event) {
|
||||
MustDecodeTo(data, &e)
|
||||
return
|
||||
}
|
||||
|
||||
func contains(closed []Listener, listener Listener) bool {
|
||||
for _, c := range closed {
|
||||
if c == listener {
|
||||
return true
|
||||
}
|
||||
func MustDecodeTo(data []byte, v interface{}) {
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
panic(errors.Wrap(err, "events: failed to decode event data into interface"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,162 +9,90 @@ import (
|
||||
|
||||
func TestNewBus(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
bus := NewBus()
|
||||
|
||||
g.Describe("NewBus", func() {
|
||||
g.It("is not nil", func() {
|
||||
g.Assert(bus).IsNotNil("Bus expected to not be nil")
|
||||
g.Assert(bus.listeners).IsNotNil("Bus#listeners expected to not be nil")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBus_Off(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
|
||||
const topic = "test"
|
||||
|
||||
g.Describe("Off", func() {
|
||||
g.It("unregisters listener", func() {
|
||||
bus := NewBus()
|
||||
|
||||
g.Assert(bus.listeners[topic]).IsNotNil()
|
||||
g.Assert(len(bus.listeners[topic])).IsZero()
|
||||
listener := make(chan Event)
|
||||
bus.On(listener, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(1, "Listener was not registered")
|
||||
|
||||
bus.Off(listener, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(0, "Topic still has one or more listeners")
|
||||
g.Describe("Events", func() {
|
||||
var bus *Bus
|
||||
g.BeforeEach(func() {
|
||||
bus = NewBus()
|
||||
})
|
||||
|
||||
g.It("unregisters correct listener", func() {
|
||||
bus := NewBus()
|
||||
|
||||
listener := make(chan Event)
|
||||
listener2 := make(chan Event)
|
||||
listener3 := make(chan Event)
|
||||
bus.On(listener, topic)
|
||||
bus.On(listener2, topic)
|
||||
bus.On(listener3, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(3, "Listeners were not registered")
|
||||
|
||||
bus.Off(listener, topic)
|
||||
bus.Off(listener3, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(1, "Expected 1 listener to remain")
|
||||
|
||||
if bus.listeners[topic][0] != listener2 {
|
||||
// A normal Assert does not properly compare channels.
|
||||
g.Fail("wrong listener unregistered")
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
bus.Off(listener2, topic)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBus_On(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
|
||||
const topic = "test"
|
||||
|
||||
g.Describe("On", func() {
|
||||
g.It("registers listener", func() {
|
||||
bus := NewBus()
|
||||
|
||||
g.Assert(bus.listeners[topic]).IsNotNil()
|
||||
g.Assert(len(bus.listeners[topic])).IsZero()
|
||||
listener := make(chan Event)
|
||||
bus.On(listener, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(1, "Listener was not registered")
|
||||
|
||||
if bus.listeners[topic][0] != listener {
|
||||
// A normal Assert does not properly compare channels.
|
||||
g.Fail("wrong listener registered")
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
bus.Off(listener, topic)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestBus_Publish(t *testing.T) {
|
||||
g := Goblin(t)
|
||||
|
||||
const topic = "test"
|
||||
const message = "this is a test message!"
|
||||
|
||||
g.Describe("Publish", func() {
|
||||
g.It("publishes message", func() {
|
||||
bus := NewBus()
|
||||
|
||||
g.Assert(bus.listeners[topic]).IsNotNil()
|
||||
g.Assert(len(bus.listeners[topic])).IsZero()
|
||||
listener := make(chan Event)
|
||||
bus.On(listener, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(1, "Listener was not registered")
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
select {
|
||||
case m := <-listener:
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case <-time.After(1 * time.Second):
|
||||
g.Fail("listener did not receive message in time")
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
bus.Publish(topic, message)
|
||||
<-done
|
||||
|
||||
// Cleanup
|
||||
bus.Off(listener, topic)
|
||||
g.Describe("NewBus", func() {
|
||||
g.It("is not nil", func() {
|
||||
g.Assert(bus).IsNotNil("Bus expected to not be nil")
|
||||
})
|
||||
})
|
||||
|
||||
g.It("publishes message to all listeners", func() {
|
||||
bus := NewBus()
|
||||
g.Describe("Publish", func() {
|
||||
const topic = "test"
|
||||
const message = "this is a test message!"
|
||||
|
||||
g.Assert(bus.listeners[topic]).IsNotNil()
|
||||
g.Assert(len(bus.listeners[topic])).IsZero()
|
||||
listener := make(chan Event)
|
||||
listener2 := make(chan Event)
|
||||
listener3 := make(chan Event)
|
||||
bus.On(listener, topic)
|
||||
bus.On(listener2, topic)
|
||||
bus.On(listener3, topic)
|
||||
g.Assert(len(bus.listeners[topic])).Equal(3, "Listener was not registered")
|
||||
g.It("publishes message", func() {
|
||||
bus := NewBus()
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for i := 0; i < 3; i++ {
|
||||
listener := make(chan []byte)
|
||||
bus.On(listener)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
select {
|
||||
case m := <-listener:
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case m := <-listener2:
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case m := <-listener3:
|
||||
case v := <-listener:
|
||||
m := MustDecode(v)
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case <-time.After(1 * time.Second):
|
||||
g.Fail("all listeners did not receive the message in time")
|
||||
i = 3
|
||||
g.Fail("listener did not receive message in time")
|
||||
}
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
bus.Publish(topic, message)
|
||||
<-done
|
||||
|
||||
done <- struct{}{}
|
||||
}()
|
||||
bus.Publish(topic, message)
|
||||
<-done
|
||||
// Cleanup
|
||||
bus.Off(listener)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
bus.Off(listener, topic)
|
||||
bus.Off(listener2, topic)
|
||||
bus.Off(listener3, topic)
|
||||
g.It("publishes message to all listeners", func() {
|
||||
bus := NewBus()
|
||||
|
||||
listener := make(chan []byte)
|
||||
listener2 := make(chan []byte)
|
||||
listener3 := make(chan []byte)
|
||||
bus.On(listener)
|
||||
bus.On(listener2)
|
||||
bus.On(listener3)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case v := <-listener:
|
||||
m := MustDecode(v)
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case v := <-listener2:
|
||||
m := MustDecode(v)
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case v := <-listener3:
|
||||
m := MustDecode(v)
|
||||
g.Assert(m.Topic).Equal(topic)
|
||||
g.Assert(m.Data).Equal(message)
|
||||
case <-time.After(1 * time.Second):
|
||||
g.Fail("all listeners did not receive the message in time")
|
||||
i = 3
|
||||
}
|
||||
}
|
||||
|
||||
done <- struct{}{}
|
||||
}()
|
||||
bus.Publish(topic, message)
|
||||
<-done
|
||||
|
||||
// Cleanup
|
||||
bus.Off(listener)
|
||||
bus.Off(listener2)
|
||||
bus.Off(listener3)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user