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

106 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mysql
import (
"context"
"testing"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
"hyapp/internal/testutil/mysqlschema"
"hyapp/pkg/appcode"
"hyapp/services/room-service/internal/room/outbox"
)
func retentionTestRecord(eventID string, createdAtMS int64) outbox.Record {
return outbox.Record{
AppCode: appcode.Default,
EventID: eventID,
EventType: "RoomGiftSent",
RoomID: "room-retention",
Status: outbox.StatusPending,
Envelope: &roomeventsv1.EventEnvelope{
AppCode: appcode.Default,
EventId: eventID,
RoomId: "room-retention",
EventType: "RoomGiftSent",
},
CreatedAtMS: createdAtMS,
}
}
// 清理只允许命中 delivered 且早于 cutoff 的行;其他状态和窗口内的 delivered 必须原样保留,
// 否则统计重放和人工补偿会丢事实。
func TestDeleteDeliveredOutboxBeforeOnlyPurgesOldDelivered(t *testing.T) {
schema := mysqlschema.New(t, mysqlschema.Config{
EnvVar: "ROOM_SERVICE_MYSQL_TEST_DSN",
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_room_service.sql"),
DatabasePrefix: "hyapp_room_retention_test",
})
ctx := context.Background()
repo, err := Open(ctx, schema.DSN, 4, 4)
if err != nil {
t.Fatalf("open repository: %v", err)
}
defer repo.Close()
records := []outbox.Record{
retentionTestRecord("evt-old-delivered-1", 1000),
retentionTestRecord("evt-old-delivered-2", 2000),
retentionTestRecord("evt-old-delivered-3", 3000),
retentionTestRecord("evt-fresh-delivered", 4000),
retentionTestRecord("evt-old-pending", 1000),
retentionTestRecord("evt-old-failed", 1000),
}
if err := repo.SaveOutbox(ctx, records); err != nil {
t.Fatalf("save outbox: %v", err)
}
const cutoffMS = int64(1_000_000)
setStatusAt := func(eventID string, status string, updatedAtMS int64) {
t.Helper()
if _, err := schema.DB.ExecContext(ctx, `UPDATE room_outbox SET status = ?, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, status, updatedAtMS, appcode.Default, eventID); err != nil {
t.Fatalf("rewrite outbox row %s: %v", eventID, err)
}
}
setStatusAt("evt-old-delivered-1", outbox.StatusDelivered, 1000)
setStatusAt("evt-old-delivered-2", outbox.StatusDelivered, 2000)
setStatusAt("evt-old-delivered-3", outbox.StatusDelivered, 3000)
setStatusAt("evt-fresh-delivered", outbox.StatusDelivered, cutoffMS+1)
setStatusAt("evt-old-failed", outbox.StatusFailed, 1000)
// 批量上限必须生效3 条可删行按 limit=2 分两批删完,第三批确认删空。
for step, want := range []int64{2, 1, 0} {
deleted, err := repo.DeleteDeliveredOutboxBefore(ctx, cutoffMS, 2)
if err != nil {
t.Fatalf("delete batch %d: %v", step, err)
}
if deleted != want {
t.Fatalf("delete batch %d deleted %d rows, want %d", step, deleted, want)
}
}
var survivors []string
rows, err := schema.DB.QueryContext(ctx, `SELECT event_id FROM room_outbox WHERE app_code = ? ORDER BY event_id`, appcode.Default)
if err != nil {
t.Fatalf("list survivors: %v", err)
}
defer rows.Close()
for rows.Next() {
var eventID string
if err := rows.Scan(&eventID); err != nil {
t.Fatalf("scan survivor: %v", err)
}
survivors = append(survivors, eventID)
}
if err := rows.Err(); err != nil {
t.Fatalf("iterate survivors: %v", err)
}
want := []string{"evt-fresh-delivered", "evt-old-failed", "evt-old-pending"}
if len(survivors) != len(want) {
t.Fatalf("survivors mismatch got=%v want=%v", survivors, want)
}
for index := range want {
if survivors[index] != want[index] {
t.Fatalf("survivors mismatch got=%v want=%v", survivors, want)
}
}
}