155 lines
6.5 KiB
Go
155 lines
6.5 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/services/room-service/internal/integration"
|
|
"hyapp/services/room-service/internal/room/outbox"
|
|
roomservice "hyapp/services/room-service/internal/room/service"
|
|
"hyapp/services/room-service/internal/router"
|
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
|
)
|
|
|
|
type recordingOutboxPublisher struct {
|
|
mu sync.Mutex
|
|
envelopes []*roomeventsv1.EventEnvelope
|
|
}
|
|
|
|
func (p *recordingOutboxPublisher) PublishOutboxEvent(_ context.Context, envelope *roomeventsv1.EventEnvelope) error {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.envelopes = append(p.envelopes, envelope)
|
|
return nil
|
|
}
|
|
|
|
func (p *recordingOutboxPublisher) count() int {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return len(p.envelopes)
|
|
}
|
|
|
|
func TestRobotOutboxWorkerDropsStaleRecordsWithoutPublishing(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repository := mysqltest.NewRepository(t)
|
|
publisher := &recordingOutboxPublisher{}
|
|
svc := roomservice.New(roomservice.Config{
|
|
NodeID: "node-robot-stale-worker",
|
|
LeaseTTL: 10 * time.Second,
|
|
RobotOutboxPublisher: publisher,
|
|
}, router.NewMemoryDirectory(), repository, nil, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
|
|
|
stale := robotOutboxRecordForTest(t, "room-robot-stale", time.Now().UTC().Add(-2*time.Minute))
|
|
if err := repository.SaveRobotOutbox(ctx, []outbox.Record{stale}); err != nil {
|
|
t.Fatalf("save stale robot outbox failed: %v", err)
|
|
}
|
|
|
|
if err := svc.ProcessPendingRobotOutbox(ctx, roomservice.OutboxWorkerOptions{
|
|
BatchSize: 10,
|
|
PublishTimeout: time.Second,
|
|
StaleDiscardAfter: 60 * time.Second,
|
|
}); err != nil {
|
|
t.Fatalf("process stale robot outbox failed: %v", err)
|
|
}
|
|
if publisher.count() != 0 {
|
|
t.Fatalf("stale robot outbox must not publish, got %d publishes", publisher.count())
|
|
}
|
|
if _, exists := repository.RobotOutboxRecord(stale.EventID); exists {
|
|
t.Fatalf("stale robot outbox must be deleted")
|
|
}
|
|
}
|
|
|
|
func TestRobotOutboxWorkerPublishesFreshRecords(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repository := mysqltest.NewRepository(t)
|
|
publisher := &recordingOutboxPublisher{}
|
|
svc := roomservice.New(roomservice.Config{
|
|
NodeID: "node-robot-fresh-worker",
|
|
LeaseTTL: 10 * time.Second,
|
|
RobotOutboxPublisher: publisher,
|
|
}, router.NewMemoryDirectory(), repository, nil, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
|
|
|
fresh := robotOutboxRecordForTest(t, "room-robot-fresh", time.Now().UTC())
|
|
if err := repository.SaveRobotOutbox(ctx, []outbox.Record{fresh}); err != nil {
|
|
t.Fatalf("save fresh robot outbox failed: %v", err)
|
|
}
|
|
|
|
if err := svc.ProcessPendingRobotOutbox(ctx, roomservice.OutboxWorkerOptions{
|
|
BatchSize: 10,
|
|
PublishTimeout: time.Second,
|
|
StaleDiscardAfter: 60 * time.Second,
|
|
}); err != nil {
|
|
t.Fatalf("process fresh robot outbox failed: %v", err)
|
|
}
|
|
if publisher.count() != 1 {
|
|
t.Fatalf("fresh robot outbox must publish once, got %d publishes", publisher.count())
|
|
}
|
|
record, exists := repository.RobotOutboxRecord(fresh.EventID)
|
|
if !exists || record.Status != outbox.StatusDelivered {
|
|
t.Fatalf("fresh robot outbox must be delivered, exists=%v record=%+v", exists, record)
|
|
}
|
|
}
|
|
|
|
func TestCleanupRobotOutboxDeletesExpiredActiveAndTerminalRecords(t *testing.T) {
|
|
ctx := appcode.WithContext(context.Background(), appcode.Default)
|
|
repository := mysqltest.NewRepository(t)
|
|
now := time.Now().UTC()
|
|
oldCreated := now.Add(-10 * time.Minute)
|
|
freshCreated := now.Add(-30 * time.Second)
|
|
oldUpdatedMS := now.Add(-2 * time.Hour).UnixMilli()
|
|
freshUpdatedMS := now.UnixMilli()
|
|
expiredLockMS := now.Add(-time.Minute).UnixMilli()
|
|
futureLockMS := now.Add(time.Hour).UnixMilli()
|
|
|
|
oldPending := robotOutboxRecordForTest(t, "room-robot-cleanup", oldCreated)
|
|
oldRetryable := robotOutboxRecordForTest(t, "room-robot-cleanup", oldCreated.Add(time.Millisecond))
|
|
oldDeliveringExpired := robotOutboxRecordForTest(t, "room-robot-cleanup", oldCreated.Add(2*time.Millisecond))
|
|
oldDeliveringLocked := robotOutboxRecordForTest(t, "room-robot-cleanup", oldCreated.Add(3*time.Millisecond))
|
|
oldDelivered := robotOutboxRecordForTest(t, "room-robot-cleanup", oldCreated.Add(4*time.Millisecond))
|
|
freshPending := robotOutboxRecordForTest(t, "room-robot-cleanup", freshCreated)
|
|
freshDelivered := robotOutboxRecordForTest(t, "room-robot-cleanup", freshCreated.Add(time.Millisecond))
|
|
records := []outbox.Record{oldPending, oldRetryable, oldDeliveringExpired, oldDeliveringLocked, oldDelivered, freshPending, freshDelivered}
|
|
if err := repository.SaveRobotOutbox(ctx, records); err != nil {
|
|
t.Fatalf("save cleanup robot outbox records failed: %v", err)
|
|
}
|
|
repository.SetRobotOutboxStatus(oldRetryable.EventID, outbox.StatusRetryable, oldUpdatedMS, nil)
|
|
repository.SetRobotOutboxStatus(oldDeliveringExpired.EventID, outbox.StatusDelivering, oldUpdatedMS, &expiredLockMS)
|
|
repository.SetRobotOutboxStatus(oldDeliveringLocked.EventID, outbox.StatusDelivering, oldUpdatedMS, &futureLockMS)
|
|
repository.SetRobotOutboxStatus(oldDelivered.EventID, outbox.StatusDelivered, oldUpdatedMS, nil)
|
|
repository.SetRobotOutboxStatus(freshDelivered.EventID, outbox.StatusDelivered, freshUpdatedMS, nil)
|
|
|
|
activeDeleted, terminalDeleted, err := repository.CleanupRobotOutbox(ctx, now.Add(-5*time.Minute).UnixMilli(), now.Add(-time.Hour).UnixMilli(), 50000)
|
|
if err != nil {
|
|
t.Fatalf("cleanup robot outbox failed: %v", err)
|
|
}
|
|
if activeDeleted != 3 || terminalDeleted != 1 {
|
|
t.Fatalf("cleanup counts mismatch active=%d terminal=%d", activeDeleted, terminalDeleted)
|
|
}
|
|
for _, record := range []outbox.Record{oldPending, oldRetryable, oldDeliveringExpired, oldDelivered} {
|
|
if _, exists := repository.RobotOutboxRecord(record.EventID); exists {
|
|
t.Fatalf("expired robot outbox %s should be deleted", record.EventID)
|
|
}
|
|
}
|
|
for _, record := range []outbox.Record{oldDeliveringLocked, freshPending, freshDelivered} {
|
|
if _, exists := repository.RobotOutboxRecord(record.EventID); !exists {
|
|
t.Fatalf("fresh or locked robot outbox %s should remain", record.EventID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func robotOutboxRecordForTest(t *testing.T, roomID string, occurredAt time.Time) outbox.Record {
|
|
t.Helper()
|
|
record, err := outbox.Build(roomID, "RoomHeatChanged", 1, occurredAt, &roomeventsv1.RoomHeatChanged{
|
|
Delta: 1,
|
|
CurrentHeat: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build robot outbox record failed: %v", err)
|
|
}
|
|
return record
|
|
}
|