test: EventPublisher publish accepted

This commit is contained in:
Jay
2026-06-03 18:07:28 -04:00
parent cb848e3f32
commit dae4c7773d
4 changed files with 188 additions and 66 deletions
+4 -14
View File
@@ -124,9 +124,8 @@ func NewEmbassy(
e.logger = slog.New(cfg.handler).With(slog.Any("component", comp))
}
e.wg.Add(2)
go e.routeEvents()
go e.routeInbox()
e.wg.Go(e.routeEvents)
e.wg.Go(e.routeInbox)
return e
}
@@ -237,8 +236,6 @@ func (e *Embassy) unsubscribeInboxLock(url string) {
}
func (e *Embassy) routeEvents() {
defer e.wg.Done()
for {
select {
case <-e.ctx.Done():
@@ -273,8 +270,6 @@ func (e *Embassy) routeEvents() {
}
func (e *Embassy) routeInbox() {
defer e.wg.Done()
for {
select {
case <-e.ctx.Done():
@@ -363,9 +358,8 @@ func newEnvoy(
e.logger = slog.New(e.handler).With(slog.Any("component", comp))
}
e.wg.Add(2)
go e.publishEvents()
go e.routeInbox()
e.wg.Go(e.publishEvents)
e.wg.Go(e.routeInbox)
return e
}
@@ -440,8 +434,6 @@ func (e *Envoy) SubscribeInbox(labels []string) <-chan InboxMessage {
}
func (e *Envoy) publishEvents() {
defer e.wg.Done()
for {
select {
case <-e.ctx.Done():
@@ -473,8 +465,6 @@ func (e *Envoy) publishEvents() {
}
func (e *Envoy) routeInbox() {
defer e.wg.Done()
for {
select {
case <-e.ctx.Done():
+138 -39
View File
@@ -2,8 +2,11 @@ package prism
import (
"context"
"errors"
"sync"
"time"
"git.wisehodl.dev/jay/go-roots-ws"
)
// ----------------------------------------------------------------------------
@@ -74,33 +77,82 @@ type EventPublisher struct {
}
func NewEventPublisher(e *Envoy) *EventPublisher {
// SubscribeInbox([]string{"OK"})
// SubscribeEvents()
// context.WithCancel from e.Context()
// make pending map
// wg.Add(2); go p.routeInbox(); go p.handleEvents()
return nil
ctx, cancel := context.WithCancel(e.Context())
p := &EventPublisher{
envoy: e,
pending: make(map[string]*pendingEntry),
inbox: e.SubscribeInbox([]string{"OK"}),
events: e.SubscribeEvents(),
ctx: ctx,
cancel: cancel,
}
p.wg.Go(p.routeInbox)
p.wg.Go(p.handleEvents)
return p
}
func (p *EventPublisher) Publish(eventID string, eventJSON []byte, timeout time.Duration) (bool, string, error) {
// build pendingEntry with result chan (buffered 1) and sync.Once
// compute deadline; create timer that fires deliver(timeout error) via once
// timer should lock -> deregister -> record PublishTimeout -> unlock -> deliver
// mu.Lock: insert into pending map
// if envoy connected: EncloseEvent + Send
// on send error: deregister, deliver send error, return
// on success: mark sent, record PublishDispatched
// mu.Unlock
// block on entry.result
// return received publishResult fields
return false, "", nil
entry := &pendingEntry{
eventID: eventID,
eventJSON: eventJSON,
result: make(chan publishResult, 1),
}
entry.timer = time.AfterFunc(timeout, func() {
p.mu.Lock()
p.deregister(eventID)
p.mu.Unlock()
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishTimeout{EventID: eventID, At: time.Now()})
p.deliver(entry, publishResult{err: errors.New("publish timeout")})
})
p.mu.Lock()
p.pending[eventID] = entry
connected := p.envoy.IsConnected()
p.mu.Unlock()
if connected {
if err := p.trySend(entry); err != nil {
p.mu.Lock()
p.deregister(eventID)
p.mu.Unlock()
p.deliver(entry, publishResult{err: err})
}
}
r := <-entry.result
return r.accepted, r.message, r.err
}
func (p *EventPublisher) Close() {
// p.cancel()
// mu.Lock: iterate pending, call once.Do(deliver cancellation error) for each
// mu.Unlock
// p.wg.Wait()
p.cancel()
p.mu.Lock()
for id, e := range p.pending {
p.deregister(id)
p.deliver(e, publishResult{err: errors.New("publisher closed")})
}
p.mu.Unlock()
p.wg.Wait()
}
func (p *EventPublisher) trySend(entry *pendingEntry) error {
err := p.envoy.Send([]byte(envelope.EncloseEvent(entry.eventJSON)))
if err != nil {
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishSendFailed{EventID: entry.eventID, Err: err, At: time.Now()})
return err
}
p.mu.Lock()
entry.sent = true
p.mu.Unlock()
p.envoy.Observer().Record(p.envoy.PeerID(),
PublishDispatched{EventID: entry.eventID, At: time.Now()})
return nil
}
func (p *EventPublisher) deliver(entry *pendingEntry, result publishResult) {
@@ -111,27 +163,74 @@ func (p *EventPublisher) deliver(entry *pendingEntry, result publishResult) {
}
func (p *EventPublisher) routeInbox() {
defer p.wg.Done()
// select ctx.Done | inbox msg
// msg: envelope.FindOK → on err continue
// mu.Lock; look up pending by eventID
// if not found: mu.Unlock; continue
// deregister; mu.Unlock
// record PublishAccepted or PublishRejected
// deliver(entry, publishResult{accepted, message, nil})
for {
select {
case <-p.ctx.Done():
return
case msg := <-p.inbox:
eventID, accepted, message, err := envelope.FindOK(msg.Data)
if err != nil {
continue
}
p.mu.Lock()
entry, ok := p.pending[eventID]
if ok {
p.deregister(eventID)
}
p.mu.Unlock()
if !ok {
continue
}
if accepted {
p.envoy.Observer().Record(msg.ID,
PublishAccepted{EventID: eventID, At: time.Now()})
} else {
p.envoy.Observer().Record(msg.ID,
PublishRejected{EventID: eventID, Message: message, At: time.Now()})
}
p.deliver(entry, publishResult{accepted: accepted, message: message})
}
}
}
func (p *EventPublisher) handleEvents() {
defer p.wg.Done()
// select ctx.Done | event
// EventConnected:
// mu.Lock; iterate pending where !sent
// Send each; on error: deregister, deliver send error, record PublishSendFailed
// on success: mark sent, record PublishDispatched
// mu.Unlock
// EventDisconnected:
// mu.Lock; mark all sent entries as unsent
// mu.Unlock
for {
select {
case <-p.ctx.Done():
return
case ev := <-p.events:
switch ev.Kind {
case EventConnected:
p.mu.Lock()
var toSend []*pendingEntry
for _, e := range p.pending {
if !e.sent {
toSend = append(toSend, e)
}
}
p.mu.Unlock()
for _, e := range toSend {
if err := p.trySend(e); err != nil {
p.mu.Lock()
p.deregister(e.eventID)
p.mu.Unlock()
p.deliver(e, publishResult{err: err})
}
}
case EventDisconnected:
p.mu.Lock()
for _, e := range p.pending {
e.sent = false
}
p.mu.Unlock()
}
}
}
}
func (p *EventPublisher) deregister(eventID string) {
+44 -6
View File
@@ -2,16 +2,54 @@ package prism
import (
"testing"
"git.wisehodl.dev/jay/go-roots-ws"
"github.com/stretchr/testify/assert"
)
func TestEventPublisher(t *testing.T) {
t.Run("publish accepted", func(t *testing.T) {
// p, envoy := newMockEnvoy(t); p.connect()
// pub := NewEventPublisher(envoy); t.Cleanup(pub.Close)
// go: accepted, msg, err := pub.Publish(id, json, TestTimeout)
// Eventually: EVENT envelope on p.sent
// p.receive(envelope.EncloseOK(id, true, ""))
// assert accepted==true, msg=="", err==nil
p, envoy := newMockEnvoy(t)
p.connect()
pub := NewEventPublisher(envoy)
t.Cleanup(pub.Close)
const id = "aaaa1111"
eventJSON := []byte(`{"id":"aaaa1111","kind":1}`)
type result struct {
accepted bool
message string
err error
}
ch := make(chan result, 1)
go func() {
accepted, msg, err := pub.Publish(id, eventJSON, TestTimeout)
ch <- result{accepted, msg, err}
}()
var sent []byte
Eventually(t, func() bool {
select {
case sent = <-p.sent:
return true
default:
return false
}
}, "EVENT envelope not sent")
assert.Contains(t, string(sent), id)
p.receive([]byte(envelope.EncloseOK(id, true, "")))
Eventually(t, func() bool {
select {
case r := <-ch:
return r.accepted && r.message == "" && r.err == nil
default:
return false
}
}, "Publish did not return accepted result")
})
t.Run("publish rejected", func(t *testing.T) {
+2 -7
View File
@@ -165,9 +165,8 @@ func NewRequestManager(e *Envoy) *RequestManager {
m.logger = slog.New(m.handler).With(slog.Any("component", comp))
}
m.wg.Add(2)
go m.handleEvents()
go m.routeInbox()
m.wg.Go(m.handleEvents)
m.wg.Go(m.routeInbox)
return m
}
@@ -354,8 +353,6 @@ func (m *RequestManager) deregister(req *request) {
}
func (m *RequestManager) handleEvents() {
defer m.wg.Done()
for {
select {
case <-m.ctx.Done():
@@ -381,8 +378,6 @@ func (m *RequestManager) handleEvents() {
}
func (m *RequestManager) routeInbox() {
defer m.wg.Done()
for {
select {
case <-m.ctx.Done():