- 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>
106 lines
3.6 KiB
Go
106 lines
3.6 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|