zhx 79e60f3815 fix: lucky_gift_outbox 抢占改单 status + SKIP LOCKED,补 delivered 保留期清理
- ClaimPendingLuckyGiftOutbox 把 status IN ('pending','retryable') 拆成单
  status 查询并加 FOR UPDATE SKIP LOCKED:IN 跨 status 会 filesort 全部匹配行
  并逐行加锁,积压时两实例互相阻塞(参照 chatapp3-golang 523cfe9 的修法);
  ORDER BY 对齐 idx_lucky_gift_outbox_claim_retry/claim_lock 列序,索引序扫描
  LIMIT 提前终止,只锁认领行。pending 留 10% 批次额度给 retryable 防饿死。
- 新增 delivered 行保留期清理 worker(默认开启,保留 30 天、5 分钟一轮、
  单批 500):线上已积累 15.4 万只增不减的 delivered 行。清理按 app_code
  迭代走 idx_lucky_gift_outbox_retention,只删 delivered 终态。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:32:04 +08:00

46 lines
1.1 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestLoadDefaultsEnableOutboxRetention(t *testing.T) {
cfg, err := Load("")
if err != nil {
t.Fatalf("Load(empty) failed: %v", err)
}
retention := cfg.LuckyGiftWorker.Retention
if !retention.Enabled {
t.Fatal("retention should be enabled by default")
}
if retention.MaxAge != 30*24*time.Hour {
t.Fatalf("retention.MaxAge = %s, want 720h", retention.MaxAge)
}
if retention.ScanInterval != 5*time.Minute {
t.Fatalf("retention.ScanInterval = %s, want 5m", retention.ScanInterval)
}
if retention.BatchSize != 500 {
t.Fatalf("retention.BatchSize = %d, want 500", retention.BatchSize)
}
}
func TestLoadRejectsRetentionMaxAgeBelowSafetyFloor(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
content := `
lucky_gift_worker:
retention:
enabled: true
max_age: 30s
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "safety floor") {
t.Fatalf("Load should reject max_age below 24h, got err=%v", err)
}
}