线上实测 delivered 约 9 万行/天且该表无下游读方(事实在 lucky_draw_records), 30 天窗口会让表膨胀到近 300 万行;7 天足够排障。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
package luckygift
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"time"
|
||
|
||
"hyapp/pkg/logx"
|
||
)
|
||
|
||
// outboxRetentionBatchPause 是同一轮内两批 DELETE 之间的让路间隔,避免清理积压时长时间占用行锁和 IO。
|
||
const outboxRetentionBatchPause = 100 * time.Millisecond
|
||
|
||
// OutboxRetentionOptions 控制 lucky_gift_outbox 中 delivered 历史事件的定期清理循环。
|
||
type OutboxRetentionOptions struct {
|
||
// ScanInterval 是清空一轮可删事件后到下一轮扫描的间隔;非正数会被归一到安全默认值。
|
||
ScanInterval time.Duration
|
||
// MaxAge 是 delivered 事件按 updated_at_ms 计算的保留时长。
|
||
MaxAge time.Duration
|
||
// BatchSize 是单条 DELETE 的行数上限。
|
||
BatchSize int
|
||
}
|
||
|
||
func defaultOutboxRetentionOptions() OutboxRetentionOptions {
|
||
// 默认值和 config.Default 的 retention 保持一致,防止手动构造绕过配置归一化后误删新事件。
|
||
// 线上 delivered 约 9 万行/天且无下游读方,7 天窗口在可排障和表体积之间取平衡。
|
||
return OutboxRetentionOptions{
|
||
ScanInterval: 5 * time.Minute,
|
||
MaxAge: 7 * 24 * time.Hour,
|
||
BatchSize: 500,
|
||
}
|
||
}
|
||
|
||
func normalizeOutboxRetentionOptions(options OutboxRetentionOptions) OutboxRetentionOptions {
|
||
defaults := defaultOutboxRetentionOptions()
|
||
if options.ScanInterval <= 0 {
|
||
options.ScanInterval = defaults.ScanInterval
|
||
}
|
||
if options.MaxAge <= 0 {
|
||
options.MaxAge = defaults.MaxAge
|
||
}
|
||
if options.BatchSize <= 0 {
|
||
options.BatchSize = defaults.BatchSize
|
||
}
|
||
return options
|
||
}
|
||
|
||
// RunOutboxRetentionWorker 周期性删除 delivered 且超过保留窗口的 lucky_gift_outbox 历史事件。
|
||
// 它只做数据保留治理,不参与投递;多节点同时运行时 DELETE 靠 MySQL 行锁串行,语义幂等。
|
||
func (s *Service) RunOutboxRetentionWorker(ctx context.Context, options OutboxRetentionOptions) {
|
||
if s == nil || s.repository == nil {
|
||
return
|
||
}
|
||
options = normalizeOutboxRetentionOptions(options)
|
||
|
||
// 启动先执行一轮,让积压清理不用等第一个 scan interval。
|
||
s.runOutboxRetentionRound(ctx, options)
|
||
|
||
ticker := time.NewTicker(options.ScanInterval)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
s.runOutboxRetentionRound(ctx, options)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *Service) runOutboxRetentionRound(ctx context.Context, options OutboxRetentionOptions) {
|
||
if _, err := s.ProcessOutboxRetention(ctx, options); err != nil && ctx.Err() == nil {
|
||
// 单轮失败只影响清理进度,下一轮扫描会继续;不升级为 Error 避免和投递故障混在告警里。
|
||
logx.Warn(ctx, "lucky_gift_outbox_retention_failed", slog.String("error", err.Error()))
|
||
}
|
||
}
|
||
|
||
// ProcessOutboxRetention 执行一轮清理:按批删除到没有可删事件为止,返回本轮总删除行数。
|
||
func (s *Service) ProcessOutboxRetention(ctx context.Context, options OutboxRetentionOptions) (int64, error) {
|
||
if err := s.requireRepository(); err != nil {
|
||
return 0, err
|
||
}
|
||
options = normalizeOutboxRetentionOptions(options)
|
||
cutoffMS := s.now().UTC().Add(-options.MaxAge).UnixMilli()
|
||
|
||
var total int64
|
||
for {
|
||
deleted, err := s.repository.DeleteDeliveredLuckyGiftOutboxBefore(ctx, cutoffMS, options.BatchSize)
|
||
total += deleted
|
||
if err != nil {
|
||
return total, err
|
||
}
|
||
if deleted < int64(options.BatchSize) {
|
||
break
|
||
}
|
||
// 整批删满说明还有积压,稍作停顿继续,直到删空;退出信号优先于继续清理。
|
||
select {
|
||
case <-ctx.Done():
|
||
return total, ctx.Err()
|
||
case <-time.After(outboxRetentionBatchPause):
|
||
}
|
||
}
|
||
|
||
if total > 0 {
|
||
logx.Info(ctx, "lucky_gift_outbox_retention_purged",
|
||
slog.Int64("rows_deleted", total),
|
||
slog.Int64("cutoff_ms", cutoffMS),
|
||
)
|
||
}
|
||
return total, nil
|
||
}
|