zhx 551ad9b03a fix: lucky_gift_outbox delivered 保留期收紧为 7 天
线上实测 delivered 约 9 万行/天且该表无下游读方(事实在 lucky_draw_records),
30 天窗口会让表膨胀到近 300 万行;7 天足够排障。

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

164 lines
6.2 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 config
import (
"fmt"
"strings"
"time"
"hyapp/pkg/configx"
"hyapp/pkg/logx"
)
// Config 描述 lucky-gift-service 的进程启动边界。
// 规则、RTP、奖池、应用级外部接入和 outbox 都属于本服务room/activity/admin 只能通过 gRPC 访问。
type Config struct {
ServiceName string `yaml:"service_name"`
NodeID string `yaml:"node_id"`
Environment string `yaml:"environment"`
GRPCAddr string `yaml:"grpc_addr"`
HealthHTTPAddr string `yaml:"health_http_addr"`
MySQLDSN string `yaml:"mysql_dsn"`
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
WalletServiceAddr string `yaml:"wallet_service_addr"`
LuckyGiftWorker LuckyGiftWorkerConfig `yaml:"lucky_gift_worker"`
Log logx.Config `yaml:"log"`
}
// LuckyGiftWorkerConfig 控制 draw outbox 补偿和统计快照刷新。
// 外部 App 抽奖不走 HyApp wallet但仍写 outbox 给审计/统计;内部房间抽奖中奖后由 worker 兜底补发钱包和 IM。
type LuckyGiftWorkerConfig struct {
Enabled bool `yaml:"enabled"`
WorkerPollInterval time.Duration `yaml:"worker_poll_interval"`
WorkerBatchSize int `yaml:"worker_batch_size"`
WorkerConcurrency int `yaml:"worker_concurrency"`
WorkerLockTTL time.Duration `yaml:"worker_lock_ttl"`
WorkerMaxRetry int `yaml:"worker_max_retry"`
PublishTimeout time.Duration `yaml:"publish_timeout"`
StatsRefreshInterval time.Duration `yaml:"stats_refresh_interval"`
StatsBatchSize int `yaml:"stats_batch_size"`
// Retention 控制 delivered 历史事件的定期清理;它独立于 Enabled临时关闭补偿投递不会停掉清理。
Retention OutboxRetentionConfig `yaml:"retention"`
}
// OutboxRetentionConfig 控制 lucky_gift_outbox 中 delivered 历史事件的小批量定期删除。
// 只清理 deliveredpending/retryable/delivering 仍在投递生命周期内failed 留给人工补偿。
// 抽奖事实和审计都在 lucky_draw_recordsoutbox 没有下游读方,默认开启。
type OutboxRetentionConfig struct {
Enabled bool `yaml:"enabled"`
// MaxAge 是 delivered 事件按 updated_at_ms 计算的保留时长,早于 now-MaxAge 的才会删除。
MaxAge time.Duration `yaml:"max_age"`
// ScanInterval 是清空一轮可删事件后到下一轮扫描的间隔。
ScanInterval time.Duration `yaml:"scan_interval"`
// BatchSize 是单条 DELETE 的行数上限,小批量避免大事务长时间持有行锁。
BatchSize int `yaml:"batch_size"`
}
func Default() Config {
return Config{
ServiceName: "lucky-gift-service",
NodeID: "lucky-gift-local",
Environment: "local",
GRPCAddr: ":13013",
HealthHTTPAddr: ":13113",
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_lucky_gift?parseTime=true&charset=utf8mb4&loc=UTC",
MySQLAutoMigrate: true,
WalletServiceAddr: "127.0.0.1:13004",
LuckyGiftWorker: LuckyGiftWorkerConfig{
Enabled: true,
WorkerPollInterval: time.Second,
WorkerBatchSize: 100,
WorkerConcurrency: 8,
WorkerLockTTL: 30 * time.Second,
WorkerMaxRetry: 8,
PublishTimeout: 5 * time.Second,
StatsRefreshInterval: 10 * time.Minute,
StatsBatchSize: 5000,
Retention: OutboxRetentionConfig{
Enabled: true,
// 线上 delivered 约 9 万行/天且无下游读方7 天足够排障30 天会让表膨胀到近 300 万行。
MaxAge: 7 * 24 * time.Hour,
ScanInterval: 5 * time.Minute,
BatchSize: 500,
},
},
Log: logx.Config{
Level: "info",
Format: "json",
MaxPayloadBytes: 2048,
},
}
}
func Load(path string) (Config, error) {
cfg := Default()
if strings.TrimSpace(path) == "" {
return cfg, nil
}
if err := configx.LoadYAML(path, &cfg); err != nil {
return Config{}, err
}
cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
if cfg.ServiceName == "" {
cfg.ServiceName = "lucky-gift-service"
}
cfg.NodeID = strings.TrimSpace(cfg.NodeID)
if cfg.NodeID == "" {
cfg.NodeID = cfg.ServiceName
}
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
if cfg.Environment == "" {
cfg.Environment = "local"
}
cfg.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr)
if cfg.GRPCAddr == "" {
cfg.GRPCAddr = ":13013"
}
cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr)
if cfg.HealthHTTPAddr == "" {
cfg.HealthHTTPAddr = ":13113"
}
cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr)
if cfg.WalletServiceAddr == "" {
cfg.WalletServiceAddr = "127.0.0.1:13004"
}
if cfg.LuckyGiftWorker.WorkerPollInterval <= 0 {
cfg.LuckyGiftWorker.WorkerPollInterval = time.Second
}
if cfg.LuckyGiftWorker.WorkerBatchSize <= 0 {
cfg.LuckyGiftWorker.WorkerBatchSize = 100
}
if cfg.LuckyGiftWorker.WorkerConcurrency <= 0 {
cfg.LuckyGiftWorker.WorkerConcurrency = 8
}
if cfg.LuckyGiftWorker.WorkerLockTTL <= 0 {
cfg.LuckyGiftWorker.WorkerLockTTL = 30 * time.Second
}
if cfg.LuckyGiftWorker.WorkerMaxRetry <= 0 {
cfg.LuckyGiftWorker.WorkerMaxRetry = 8
}
if cfg.LuckyGiftWorker.PublishTimeout <= 0 {
cfg.LuckyGiftWorker.PublishTimeout = 5 * time.Second
}
if cfg.LuckyGiftWorker.StatsRefreshInterval <= 0 {
cfg.LuckyGiftWorker.StatsRefreshInterval = 10 * time.Minute
}
if cfg.LuckyGiftWorker.StatsBatchSize <= 0 {
cfg.LuckyGiftWorker.StatsBatchSize = 5000
}
defaults := Default().LuckyGiftWorker.Retention
if cfg.LuckyGiftWorker.Retention.MaxAge <= 0 {
cfg.LuckyGiftWorker.Retention.MaxAge = defaults.MaxAge
}
if cfg.LuckyGiftWorker.Retention.ScanInterval <= 0 {
cfg.LuckyGiftWorker.Retention.ScanInterval = defaults.ScanInterval
}
if cfg.LuckyGiftWorker.Retention.BatchSize <= 0 {
cfg.LuckyGiftWorker.Retention.BatchSize = defaults.BatchSize
}
if cfg.LuckyGiftWorker.Retention.Enabled && cfg.LuckyGiftWorker.Retention.MaxAge < 24*time.Hour {
// 保留窗口低于一天基本可以断定是把单位写错(比如 30 写成 30ns/30s宁可拒绝启动。
return Config{}, fmt.Errorf("lucky_gift_worker retention max_age %s is below the 24h safety floor", cfg.LuckyGiftWorker.Retention.MaxAge)
}
return cfg, nil
}