1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +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 {

View File

@@ -2,14 +2,13 @@ package message_test
import (
"testing"
"time"
"github.com/inbucket/inbucket/pkg/extension"
"github.com/inbucket/inbucket/pkg/extension/event"
"github.com/inbucket/inbucket/pkg/message"
"github.com/inbucket/inbucket/pkg/policy"
"github.com/inbucket/inbucket/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestManagerEmitsMessageStoredEvent(t *testing.T) {
@@ -20,16 +19,7 @@ func TestManagerEmitsMessageStoredEvent(t *testing.T) {
ExtHost: extHost,
}
// Capture message event.
gotc := make(chan *event.MessageMetadata)
defer close(gotc)
extHost.Events.AfterMessageStored.AddListener(
"test",
func(msg event.MessageMetadata) *extension.Void {
gotc <- &msg
return nil
})
listener := extHost.Events.AfterMessageStored.AsyncTestListener("manager", 1)
// Attempt to deliver a message to generate event.
if _, err := sm.Deliver(
@@ -42,10 +32,7 @@ func TestManagerEmitsMessageStoredEvent(t *testing.T) {
t.Fatal(err)
}
select {
case got := <-gotc:
assert.NotNil(t, got, "No event received, or it was nil")
case <-time.After(time.Second * 2):
t.Fatal("Timeout waiting for message event")
}
got, err := listener()
require.NoError(t, err)
assert.NotNil(t, got, "No event received, or it was nil")
}

View File

@@ -38,15 +38,13 @@ func New(historyLen int, extHost *extension.Host) *Hub {
// Register an extension event listener for MessageStored.
extHost.Events.AfterMessageStored.AddListener("msghub",
func(msg event.MessageMetadata) *extension.Void {
func(msg event.MessageMetadata) {
hub.Dispatch(msg)
return nil
})
extHost.Events.AfterMessageDeleted.AddListener("msghub",
func(msg event.MessageMetadata) *extension.Void {
func(msg event.MessageMetadata) {
hub.Delete(msg.Mailbox, msg.ID)
return nil
})
return hub

View File

@@ -298,7 +298,7 @@ func testDelete(t *testing.T, store storage.Store, extHost *extension.Host) {
msgs := GetAndCountMessages(t, store, mailbox, len(subjects))
// Subscribe to events.
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener(2)
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
// Delete a couple messages.
deleteIDs := []string{msgs[1].ID(), msgs[3].ID()}
@@ -345,7 +345,7 @@ func testPurge(t *testing.T, store storage.Store, extHost *extension.Host) {
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
// Subscribe to events.
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener(len(subjects))
eventListener := extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
// Populate mailbox.
for _, subj := range subjects {