zhx ec35fcad5e room_outbox 增加 delivered 历史事件定期清理并移除重复索引
- outbox_worker.retention 配置块(默认关闭,max_age<24h 拒绝启动)
- 清理走预留的 idx_room_outbox_retention 索引,先选主键再小批量 DELETE
- 移除与 idx_room_outbox_pending 完全重复的 idx_room_outbox_retry,
  auto_migrate=true 时 Migrate 自动 DROP
- 开启清理前需和数据侧确认保留窗口覆盖统计重放/补数工具

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 01:12:25 +08:00

87 lines
3.0 KiB
Go

package service_test
import (
"context"
"fmt"
"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"
)
// 一轮清理必须按批删空所有过期 delivered 事件,同时保留窗口内 delivered 和未完成投递的事件。
func TestProcessOutboxRetentionDrainsExpiredDelivered(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-outbox-retention-test",
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
roomID := "room-outbox-retention"
newRecord := func(eventID string) outbox.Record {
return outbox.Record{
AppCode: appcode.Default,
EventID: eventID,
EventType: "RoomGiftSent",
RoomID: roomID,
Status: outbox.StatusPending,
Envelope: &roomeventsv1.EventEnvelope{
AppCode: appcode.Default,
EventId: eventID,
RoomId: roomID,
EventType: "RoomGiftSent",
},
CreatedAtMS: 1000,
}
}
expiredMS := time.Now().UTC().Add(-48 * time.Hour).UnixMilli()
records := make([]outbox.Record, 0, 7)
expired := make([]string, 0, 5)
for index := 0; index < 5; index++ {
eventID := fmt.Sprintf("evt-retention-expired-%d", index)
expired = append(expired, eventID)
records = append(records, newRecord(eventID))
}
records = append(records, newRecord("evt-retention-fresh"), newRecord("evt-retention-pending"))
if err := repository.SaveOutbox(ctx, records); err != nil {
t.Fatalf("save outbox failed: %v", err)
}
for _, eventID := range expired {
repository.SetOutboxDeliveredAt(eventID, expiredMS)
}
if err := repository.MarkOutboxDelivered(ctx, "evt-retention-fresh"); err != nil {
t.Fatalf("mark fresh delivered failed: %v", err)
}
// BatchSize=2 强制一轮内分 3 批删完 5 条过期事件,覆盖“整批删满继续、删空退出”的循环边界。
total, err := svc.ProcessOutboxRetention(ctx, roomservice.OutboxRetentionOptions{
MaxAge: 24 * time.Hour,
BatchSize: 2,
})
if err != nil {
t.Fatalf("process outbox retention failed: %v", err)
}
if total != int64(len(expired)) {
t.Fatalf("retention deleted %d rows, want %d", total, len(expired))
}
for _, eventID := range expired {
if _, ok := repository.OutboxRecord(eventID); ok {
t.Fatalf("expired delivered event %s must be purged", eventID)
}
}
if record, ok := repository.OutboxRecord("evt-retention-fresh"); !ok || record.Status != outbox.StatusDelivered {
t.Fatalf("fresh delivered event must survive: ok=%v record=%+v", ok, record)
}
if record, ok := repository.OutboxRecord("evt-retention-pending"); !ok || record.Status != outbox.StatusPending {
t.Fatalf("pending event must survive: ok=%v record=%+v", ok, record)
}
}