package integration import ( "context" "sync" "testing" "time" roomeventsv1 "hyapp.local/api/proto/events/room/v1" ) type blockingOutboxPublisher struct { entered chan struct{} release chan struct{} mu sync.Mutex inFlight int maxInFlight int count int } func newBlockingOutboxPublisher() *blockingOutboxPublisher { return &blockingOutboxPublisher{ entered: make(chan struct{}, 2), release: make(chan struct{}), } } func (p *blockingOutboxPublisher) PublishOutboxEvent(ctx context.Context, _ *roomeventsv1.EventEnvelope) error { p.mu.Lock() p.inFlight++ if p.inFlight > p.maxInFlight { p.maxInFlight = p.inFlight } p.count++ p.mu.Unlock() p.entered <- struct{}{} select { case <-ctx.Done(): return ctx.Err() case <-p.release: } p.mu.Lock() p.inFlight-- p.mu.Unlock() return nil } func (p *blockingOutboxPublisher) snapshot() (int, int) { p.mu.Lock() defer p.mu.Unlock() return p.count, p.maxInFlight } func TestShardedOutboxPublisherSerializesSameRoom(t *testing.T) { inner := newBlockingOutboxPublisher() publisher := NewShardedOutboxPublisher(inner, 8) envelope := &roomeventsv1.EventEnvelope{RoomId: "room-same"} ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 两个同房间事件同时进入时,分片锁必须让第二条等第一条完成后再调用真实 IM 发布器。 errs := make(chan error, 2) go func() { errs <- publisher.PublishOutboxEvent(ctx, envelope) }() <-inner.entered go func() { errs <- publisher.PublishOutboxEvent(ctx, envelope) }() select { case <-inner.entered: t.Fatal("same room event entered inner publisher before the first event finished") case <-time.After(50 * time.Millisecond): } inner.release <- struct{}{} if err := <-errs; err != nil { t.Fatalf("first publish failed: %v", err) } <-inner.entered inner.release <- struct{}{} if err := <-errs; err != nil { t.Fatalf("second publish failed: %v", err) } if count, maxInFlight := inner.snapshot(); count != 2 || maxInFlight != 1 { t.Fatalf("same room publishes must be serialized, count=%d max_in_flight=%d", count, maxInFlight) } }