feat(orchestration/comm): agent-to-agent 通信(bus ask/reply/event,私密 reply 主题 + 超时)
This commit is contained in:
1 parent
8b723009f9
commit
20f4a91d02
4 files changed
+814
No files matched your search
@@ -0,0 +1,359 @@
|
||||
package comm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
)
|
||||
|
||||
// recvEvent blocks until an event arrives on ch or d elapses, failing the test
|
||||
// on timeout. It makes delivery assertions deterministic instead of relying on a
|
||||
// bare sleep.
|
||||
func recvEvent(t *testing.T, ch <-chan Event, d time.Duration) Event {
|
||||
t.Helper()
|
||||
select {
|
||||
case e := <-ch:
|
||||
return e
|
||||
case <-time.After(d):
|
||||
t.Fatalf("timed out waiting for an event after %v", d)
|
||||
return Event{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubjectConvention(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"ask", askSubject("alice", "bob"), "cyrene.agent.alice.bob.ask"},
|
||||
{"reply carries ask id", replySubject("bob", "alice", "alice-3"), "cyrene.agent.bob.alice.reply.alice-3"},
|
||||
{"event addressed", eventSubject("alice", "bob"), "cyrene.agent.alice.bob.event"},
|
||||
{"event broadcast", broadcastEventSubject("alice"), "cyrene.agent.alice.event"},
|
||||
{"ask receive pattern", askReceivePattern("bob"), "cyrene.agent.*.bob.ask"},
|
||||
{"agent ids sanitized", askSubject("a.b", "c d"), "cyrene.agent.a_b.c_d.ask"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if c.got != c.want {
|
||||
t.Fatalf("subject = %q, want %q", c.got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventReceivePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := eventReceivePatterns("bob")
|
||||
want := []string{"cyrene.agent.*.event", "cyrene.agent.*.bob.event"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("patterns = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("patterns[%d] = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskReply_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
bob := New(b, "bob")
|
||||
|
||||
unsub, err := bob.SubscribeAsk(context.Background(), func(ctx context.Context, a Ask) error {
|
||||
return bob.Answer(ctx, a.From, a.ID, "pong:"+a.Question, a.Data)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeAsk: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
reply, err := alice.Ask(ctx, "bob", "ping", "payload")
|
||||
if err != nil {
|
||||
t.Fatalf("Ask: %v", err)
|
||||
}
|
||||
if reply.Text != "pong:ping" {
|
||||
t.Fatalf("reply.Text = %q, want pong:ping", reply.Text)
|
||||
}
|
||||
if reply.From != "bob" {
|
||||
t.Fatalf("reply.From = %q, want bob", reply.From)
|
||||
}
|
||||
if reply.To != "alice" {
|
||||
t.Fatalf("reply.To = %q, want alice", reply.To)
|
||||
}
|
||||
if reply.Data != "payload" {
|
||||
t.Fatalf("reply.Data = %#v, want payload", reply.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsk_TimeoutNoReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
// bob has no ask handler, so nothing ever replies.
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err := alice.Ask(ctx, "bob", "hello", nil)
|
||||
if !errors.Is(err, ErrTimeout) {
|
||||
t.Fatalf("Ask err = %v, want ErrTimeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsk_ContextCanceled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
got := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := alice.Ask(ctx, "bob", "hello", nil)
|
||||
got <- err
|
||||
}()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-got:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Ask err = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("Ask did not return after ctx cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsk_ContextPreCanceled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err := alice.Ask(ctx, "bob", "q", nil)
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Ask err = %v, want context.Canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventBroadcast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
bob := New(b, "bob")
|
||||
|
||||
events := make(chan Event, 4)
|
||||
unsub, err := bob.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
|
||||
events <- e
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeEvent: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := alice.PublishEvent(ctx, Event{Name: "tick", Data: 7}); err != nil {
|
||||
t.Fatalf("PublishEvent: %v", err)
|
||||
}
|
||||
e := recvEvent(t, events, 2*time.Second)
|
||||
if e.Name != "tick" || e.Data != 7 || e.From != "alice" {
|
||||
t.Fatalf("event = %+v, want name=tick data=7 from=alice", e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventSend_AddressedAndIsolated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
bob := New(b, "bob")
|
||||
carol := New(b, "carol")
|
||||
|
||||
bobEvents := make(chan Event, 4)
|
||||
carolEvents := make(chan Event, 4)
|
||||
|
||||
unsubBob, err := bob.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
|
||||
bobEvents <- e
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeEvent bob: %v", err)
|
||||
}
|
||||
defer unsubBob()
|
||||
unsubCarol, err := carol.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
|
||||
carolEvents <- e
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeEvent carol: %v", err)
|
||||
}
|
||||
defer unsubCarol()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := alice.Send(ctx, "bob", Event{Name: "direct"}); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
e := recvEvent(t, bobEvents, 2*time.Second)
|
||||
if e.Name != "direct" || e.From != "alice" {
|
||||
t.Fatalf("bob event = %+v, want name=direct from=alice", e)
|
||||
}
|
||||
|
||||
// An event addressed to bob must not reach carol.
|
||||
select {
|
||||
case e := <-carolEvents:
|
||||
t.Fatalf("carol unexpectedly received %+v", e)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventHandler_Concurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
emitter := New(b, "emit")
|
||||
recv := New(b, "recv")
|
||||
|
||||
const n = 100
|
||||
var got atomic.Int64
|
||||
done := make(chan struct{})
|
||||
unsub, err := recv.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
|
||||
if got.Add(1) == n {
|
||||
close(done)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeEvent: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Fire a mix of broadcasts and events addressed to recv so both event
|
||||
// subscriptions (and their separate worker goroutines) invoke the handler
|
||||
// concurrently.
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
if i%2 == 0 {
|
||||
_ = emitter.PublishEvent(ctx, Event{Name: "bc", Data: i})
|
||||
} else {
|
||||
_ = emitter.Send(ctx, "recv", Event{Name: "to", Data: i})
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("only %d of %d events delivered", got.Load(), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsk_ConcurrentDistinct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
bob := New(b, "bob")
|
||||
|
||||
unsub, err := bob.SubscribeAsk(context.Background(), func(ctx context.Context, a Ask) error {
|
||||
return bob.Answer(ctx, a.From, a.ID, "ack:"+a.Question, a.Data)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SubscribeAsk: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
const n = 20
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, n)
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
q := fmt.Sprintf("q%d", i)
|
||||
reply, err := alice.Ask(ctx, "bob", q, i)
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("ask %d: %w", i, err)
|
||||
return
|
||||
}
|
||||
if reply.Text != "ack:"+q {
|
||||
errs <- fmt.Errorf("ask %d: reply.Text = %q, want %q", i, reply.Text, "ack:"+q)
|
||||
return
|
||||
}
|
||||
if reply.Data != i {
|
||||
errs <- fmt.Errorf("ask %d: reply.Data = %#v, want %d", i, reply.Data, i)
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSend_NoErrorAndNoReceiver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
alice := New(b, "alice")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Publishing to a subject with no subscriber is a no-op, not an error.
|
||||
if err := alice.Send(ctx, "ghost", Event{Name: "nobody"}); err != nil {
|
||||
t.Fatalf("Send to lost receiver: %v", err)
|
||||
}
|
||||
if err := alice.PublishEvent(ctx, Event{Name: "nobody"}); err != nil {
|
||||
t.Fatalf("PublishEvent with no subscriber: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user