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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user