fix(v2): Runner interval<=0 守卫(skip+WARN)+ 启动预热只跑本地 job(网络型交 ticker 首跳)
- Task 加 Local 字段;interval<=0 时 Add/AddLocal 跳过注册 + WARN 日志(NewTicker(<=0) 会 panic,Start 的 per-tick recover 覆盖不到该行)。 - 新增 AddLocal/RunOnceLocal:main.go 启动预热改跑 RunOnceLocal,只同步执行纯 DB 任务 (order-expire/usage-refresh/refund-apply-sweep/refund-stuck-alert);出网 HTTP 的 sync-pending/paid-spotcheck/crypto-orphan-scan 交各自 ticker 首跳,不再拖住服务启动。
This commit is contained in:
@@ -10,10 +10,13 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Task 一个周期任务:名字 + 间隔 + 幂等可重跑的 Run。
|
||||
// Task 一个周期任务:名字 + 间隔 + 幂等可重跑的 Run。Local 标记该任务是否纯本地
|
||||
// (只碰 DB,无出网 HTTP)——启动预热(RunOnceLocal)只跑 Local 任务,网络型任务交给
|
||||
// 各自 ticker 首跳,避免上游慢拖住服务启动(main.go r.Run(addr) 之前的同步阶段)。
|
||||
type Task struct {
|
||||
Name string
|
||||
Interval time.Duration
|
||||
Local bool
|
||||
Run func(ctx context.Context) error
|
||||
}
|
||||
|
||||
@@ -25,9 +28,29 @@ type Runner struct {
|
||||
|
||||
func NewRunner() *Runner { return &Runner{logf: log.Printf} }
|
||||
|
||||
// Add 注册一个周期任务。
|
||||
// Add 注册一个周期任务(默认视为网络型/非 Local,启动预热 RunOnceLocal 不跑它,
|
||||
// 交给 Start 里各自 ticker 的首跳执行)。
|
||||
func (r *Runner) Add(name string, interval time.Duration, run func(ctx context.Context) error) {
|
||||
r.tasks = append(r.tasks, Task{Name: name, Interval: interval, Run: run})
|
||||
r.add(Task{Name: name, Interval: interval, Local: false, Run: run})
|
||||
}
|
||||
|
||||
// AddLocal 注册一个纯本地周期任务(只碰 DB,无出网 HTTP)——会被启动预热
|
||||
// RunOnceLocal 同步执行一次,让 usage 快照/过期清理/退款自愈等立即生效。
|
||||
func (r *Runner) AddLocal(name string, interval time.Duration, run func(ctx context.Context) error) {
|
||||
r.add(Task{Name: name, Interval: interval, Local: true, Run: run})
|
||||
}
|
||||
|
||||
// add 是 Add/AddLocal 的共同落地:Interval<=0 是 operator 配置错误(如
|
||||
// expire_every_sec: 0)——time.NewTicker 对 <=0 的间隔会 panic,且 Start 里每 tick
|
||||
// 的 recover 覆盖不到 NewTicker 本身(它在 goroutine 里、ticker 创建那一行就炸,
|
||||
// 无 defer 保护)。显式优于静默改值:跳过注册 + WARN 日志,而不是偷偷 clamp 成默认值
|
||||
// 掩盖配置错误。
|
||||
func (r *Runner) add(t Task) {
|
||||
if t.Interval <= 0 {
|
||||
r.logf("[reconcile] WARN 任务 %s interval<=0(%v),跳过注册(检查配置)", t.Name, t.Interval)
|
||||
return
|
||||
}
|
||||
r.tasks = append(r.tasks, t)
|
||||
}
|
||||
|
||||
// exec 跑单个任务一次:panic recover + error 记录,绝不外抛(单任务失败不拖垮其它)。
|
||||
@@ -42,13 +65,27 @@ func (r *Runner) exec(ctx context.Context, t Task) {
|
||||
}
|
||||
}
|
||||
|
||||
// RunOnce 顺序跑一遍全部任务(启动预热 + 单测入口)。
|
||||
// RunOnce 顺序跑一遍全部任务(单测入口;main.go 启动预热改用 RunOnceLocal,
|
||||
// 网络型任务不再阻塞启动,见下)。
|
||||
func (r *Runner) RunOnce(ctx context.Context) {
|
||||
for _, t := range r.tasks {
|
||||
r.exec(ctx, t)
|
||||
}
|
||||
}
|
||||
|
||||
// RunOnceLocal 只顺序跑一遍 Local 任务(启动预热用):order-expire/usage-refresh/
|
||||
// refund-apply-sweep/refund-stuck-alert 等纯 DB 任务立即生效;sync-pending/
|
||||
// paid-spotcheck/crypto-orphan-scan 等有出网 HTTP(10-15s 超时)的任务跳过,交给
|
||||
// Start 里各自 ticker 的首跳执行,避免上游慢拖住 r.Run(addr) 前的服务启动。
|
||||
func (r *Runner) RunOnceLocal(ctx context.Context) {
|
||||
for _, t := range r.tasks {
|
||||
if !t.Local {
|
||||
continue
|
||||
}
|
||||
r.exec(ctx, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 每任务一 goroutine + 独立 ticker 常驻;ctx 取消即退出。每 tick 崩溃安全。
|
||||
func (r *Runner) Start(ctx context.Context) {
|
||||
for _, t := range r.tasks {
|
||||
|
||||
Reference in New Issue
Block a user