2026-06-26 23:21:22 +08:00

90 lines
2.1 KiB
Go

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