1
0
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:
James Hillyerd
2023-02-16 16:17:06 -08:00
committed by GitHub
parent e1b8996412
commit 36095a2cdf
8 changed files with 205 additions and 71 deletions

View 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")
}
}
}

View 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")
}

View File

@@ -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")
}
}
}

View File

@@ -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]
}

View File

@@ -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 {