mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-05-14 17:43:49 +00:00
extension: split out an async specific broker for "after" events (#346)
Signed-off-by: James Hillyerd <james@hillyerd.com>
This commit is contained in:
89
pkg/extension/async_broker.go
Normal file
89
pkg/extension/async_broker.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AsyncEventBroker maintains a list of listeners interested in a specific type
|
||||
// of event. Events are sent in parallel to all listeners, and no result is
|
||||
// returned.
|
||||
type AsyncEventBroker[E any] struct {
|
||||
sync.RWMutex
|
||||
listenerNames []string // Ordered listener names.
|
||||
listenerFuncs []func(E) // Ordered listener functions.
|
||||
}
|
||||
|
||||
// Emit sends the provided event to each registered listener in parallel.
|
||||
func (eb *AsyncEventBroker[E]) Emit(event *E) {
|
||||
eb.RLock()
|
||||
defer eb.RUnlock()
|
||||
|
||||
for _, l := range eb.listenerFuncs {
|
||||
// Events are copied to minimize the risk of mutation.
|
||||
go l(*event)
|
||||
}
|
||||
}
|
||||
|
||||
// AddListener registers the named listener, replacing one with a duplicate
|
||||
// name if present. Listeners should be added in order of priority, most
|
||||
// significant first.
|
||||
func (eb *AsyncEventBroker[E]) AddListener(name string, listener func(E)) {
|
||||
eb.Lock()
|
||||
defer eb.Unlock()
|
||||
|
||||
eb.lockedRemoveListener(name)
|
||||
eb.listenerNames = append(eb.listenerNames, name)
|
||||
eb.listenerFuncs = append(eb.listenerFuncs, listener)
|
||||
}
|
||||
|
||||
// RemoveListener unregisters the named listener.
|
||||
func (eb *AsyncEventBroker[E]) RemoveListener(name string) {
|
||||
eb.Lock()
|
||||
defer eb.Unlock()
|
||||
|
||||
eb.lockedRemoveListener(name)
|
||||
}
|
||||
|
||||
func (eb *AsyncEventBroker[E]) lockedRemoveListener(name string) {
|
||||
for i, entry := range eb.listenerNames {
|
||||
if entry == name {
|
||||
eb.listenerNames = append(eb.listenerNames[:i], eb.listenerNames[i+1:]...)
|
||||
eb.listenerFuncs = append(eb.listenerFuncs[:i], eb.listenerFuncs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncTestListener returns a func that will wait for an event and return it, or timeout
|
||||
// with an error.
|
||||
func (eb *AsyncEventBroker[E]) AsyncTestListener(name string, capacity int) func() (*E, error) {
|
||||
// Send event down channel.
|
||||
events := make(chan E, capacity)
|
||||
eb.AddListener(name,
|
||||
func(msg E) {
|
||||
events <- msg
|
||||
})
|
||||
|
||||
count := 0
|
||||
|
||||
return func() (*E, error) {
|
||||
count++
|
||||
|
||||
defer func() {
|
||||
if count >= capacity {
|
||||
eb.RemoveListener(name)
|
||||
close(events)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case event := <-events:
|
||||
return &event, nil
|
||||
|
||||
case <-time.After(time.Second * 2):
|
||||
return nil, errors.New("Timeout waiting for event")
|
||||
}
|
||||
}
|
||||
}
|
||||
101
pkg/extension/async_broker_test.go
Normal file
101
pkg/extension/async_broker_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package extension_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/inbucket/inbucket/pkg/extension"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Simple smoke test without using AsyncTestListener.
|
||||
func TestAsyncBrokerEmitCallsOneListener(t *testing.T) {
|
||||
broker := &extension.AsyncEventBroker[string]{}
|
||||
|
||||
// Setup listener.
|
||||
events := make(chan string, 1)
|
||||
listener := func(s string) {
|
||||
events <- s
|
||||
}
|
||||
broker.AddListener("x", listener)
|
||||
|
||||
want := "bacon"
|
||||
broker.Emit(&want)
|
||||
|
||||
var got string
|
||||
select {
|
||||
case event := <-events:
|
||||
got = event
|
||||
|
||||
case <-time.After(time.Second * 2):
|
||||
t.Fatal("Timeout waiting for event")
|
||||
}
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Emit got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncBrokerEmitCallsMultipleListeners(t *testing.T) {
|
||||
broker := &extension.AsyncEventBroker[string]{}
|
||||
|
||||
// Setup listeners.
|
||||
first := broker.AsyncTestListener("first", 1)
|
||||
second := broker.AsyncTestListener("second", 1)
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *first_got)
|
||||
|
||||
second_got, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerAddingDuplicateNameReplacesPrevious(t *testing.T) {
|
||||
broker := &extension.AsyncEventBroker[string]{}
|
||||
|
||||
// Setup listeners.
|
||||
first := broker.AsyncTestListener("dup", 1)
|
||||
second := broker.AsyncTestListener("dup", 1)
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, first_got)
|
||||
|
||||
second_got, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerRemovingListenerSuccessful(t *testing.T) {
|
||||
broker := &extension.AsyncEventBroker[string]{}
|
||||
|
||||
// Setup listeners.
|
||||
first := broker.AsyncTestListener("1", 1)
|
||||
second := broker.AsyncTestListener("2", 1)
|
||||
broker.RemoveListener("1")
|
||||
|
||||
want := "hi"
|
||||
broker.Emit(&want)
|
||||
|
||||
first_got, err := first()
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, first_got)
|
||||
|
||||
second_got, err := second()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, want, *second_got)
|
||||
}
|
||||
|
||||
func TestAsyncBrokerRemovingMissingListener(t *testing.T) {
|
||||
broker := &extension.AsyncEventBroker[string]{}
|
||||
broker.RemoveListener("doesn't crash")
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EventBroker maintains a list of listeners interested in a specific type
|
||||
@@ -59,38 +57,3 @@ func (eb *EventBroker[E, R]) lockedRemoveListener(name string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AsyncTestListener returns a func that will wait for an event and return it, or timeout
|
||||
// with an error.
|
||||
func (eb *EventBroker[E, R]) AsyncTestListener(capacity int) func() (*E, error) {
|
||||
const name = "asyncTestListener"
|
||||
|
||||
// Send event down channel.
|
||||
events := make(chan E, capacity)
|
||||
eb.AddListener(name,
|
||||
func(msg E) *R {
|
||||
events <- msg
|
||||
return nil
|
||||
})
|
||||
|
||||
count := 0
|
||||
|
||||
return func() (*E, error) {
|
||||
count++
|
||||
|
||||
defer func() {
|
||||
if count >= capacity {
|
||||
eb.RemoveListener(name)
|
||||
close(events)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case event := <-events:
|
||||
return &event, nil
|
||||
|
||||
case <-time.After(time.Second * 2):
|
||||
return nil, errors.New("Timeout waiting for event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ type Host struct {
|
||||
// processed asynchronously with respect to the rest of Inbuckets operation. However, an event
|
||||
// listener will not be called until the one before it complets.
|
||||
type Events struct {
|
||||
AfterMessageDeleted EventBroker[event.MessageMetadata, Void]
|
||||
AfterMessageStored EventBroker[event.MessageMetadata, Void]
|
||||
AfterMessageDeleted AsyncEventBroker[event.MessageMetadata]
|
||||
AfterMessageStored AsyncEventBroker[event.MessageMetadata]
|
||||
BeforeMailAccepted EventBroker[event.AddressParts, bool]
|
||||
}
|
||||
|
||||
|
||||
@@ -125,10 +125,10 @@ func (h *Host) wireFunctions(logger zerolog.Logger, ls *lua.LState) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) *extension.Void {
|
||||
func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) {
|
||||
logger, ls, lfunc, ok := h.prepareFuncCall(afterMessageDeletedFnName)
|
||||
if !ok {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
defer h.pool.putState(ls)
|
||||
|
||||
@@ -140,14 +140,12 @@ func (h *Host) handleAfterMessageDeleted(msg event.MessageMetadata) *extension.V
|
||||
); err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) *extension.Void {
|
||||
func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) {
|
||||
logger, ls, lfunc, ok := h.prepareFuncCall(afterMessageStoredFnName)
|
||||
if !ok {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
defer h.pool.putState(ls)
|
||||
|
||||
@@ -159,8 +157,6 @@ func (h *Host) handleAfterMessageStored(msg event.MessageMetadata) *extension.Vo
|
||||
); err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to call Lua function")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Host) handleBeforeMailAccepted(addr event.AddressParts) *bool {
|
||||
|
||||
Reference in New Issue
Block a user