f310c58767
- 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 首跳,不再拖住服务启动。
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package reconcile_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/wangjia/pay/internal/reconcile"
|
|
)
|
|
|
|
func TestRunnerRunOnceExecutesAllAndRecoversPanic(t *testing.T) {
|
|
r := reconcile.NewRunner()
|
|
var a, b int
|
|
r.Add("inc-a", time.Minute, func(context.Context) error { a++; return nil })
|
|
r.Add("boom", time.Minute, func(context.Context) error { panic("kaboom") }) // 不得拖垮后续
|
|
r.Add("inc-b", time.Minute, func(context.Context) error { b++; return errors.New("soft") })
|
|
|
|
r.RunOnce(context.Background()) // panic 被 recover,error 被记录,均不中断
|
|
if a != 1 || b != 1 {
|
|
t.Fatalf("a=%d b=%d, want 1/1(panic 任务不应阻断其它)", a, b)
|
|
}
|
|
}
|
|
|
|
// TestRunnerZeroIntervalSkipsRegistrationAndStartDoesNotPanic 覆盖 Important #1:
|
|
// operator 配 interval<=0(如 expire_every_sec: 0)不得让 Start 里 time.NewTicker
|
|
// panic 崩进程 —— Add/AddLocal 应在注册期就 skip 该任务(不进 r.tasks),Start 对它
|
|
// 不会创建 ticker,自然不 panic;RunOnce/RunOnceLocal 也因任务未注册而不执行它。
|
|
func TestRunnerZeroIntervalSkipsRegistrationAndStartDoesNotPanic(t *testing.T) {
|
|
r := reconcile.NewRunner()
|
|
var zeroRan, negRan, okRan int32
|
|
r.Add("zero-interval", 0, func(context.Context) error {
|
|
atomic.AddInt32(&zeroRan, 1)
|
|
return nil
|
|
})
|
|
r.AddLocal("neg-interval", -time.Second, func(context.Context) error {
|
|
atomic.AddInt32(&negRan, 1)
|
|
return nil
|
|
})
|
|
r.Add("ok-interval", 5*time.Millisecond, func(context.Context) error {
|
|
atomic.AddInt32(&okRan, 1)
|
|
return nil
|
|
})
|
|
|
|
r.RunOnce(context.Background())
|
|
if zeroRan != 0 || negRan != 0 {
|
|
t.Fatalf("interval<=0 的任务不应被注册/执行: zeroRan=%d negRan=%d", zeroRan, negRan)
|
|
}
|
|
if okRan != 1 {
|
|
t.Fatalf("正常任务应正常执行一次, got okRan=%d", okRan)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
r.Start(ctx) // 若 zero-interval/neg-interval 仍被注册,NewTicker(<=0) 会 panic 崩掉这个 goroutine
|
|
}()
|
|
time.Sleep(30 * time.Millisecond) // 留时间给 ok-interval 的 ticker 至少 tick 一次
|
|
cancel()
|
|
<-done
|
|
|
|
if atomic.LoadInt32(&zeroRan) != 0 || atomic.LoadInt32(&negRan) != 0 {
|
|
t.Fatalf("Start 之后 interval<=0 的任务仍不应执行: zeroRan=%d negRan=%d", zeroRan, negRan)
|
|
}
|
|
}
|
|
|
|
// TestRunnerRunOnceLocalOnlyRunsLocalTasks 覆盖 Important #2:启动预热
|
|
// RunOnceLocal 只应执行 AddLocal 注册的任务,Add(默认网络型)注册的任务不跑,
|
|
// 交给 Start 里各自 ticker 的首跳。
|
|
func TestRunnerRunOnceLocalOnlyRunsLocalTasks(t *testing.T) {
|
|
r := reconcile.NewRunner()
|
|
var ran []string
|
|
r.AddLocal("local-a", time.Minute, func(context.Context) error {
|
|
ran = append(ran, "local-a")
|
|
return nil
|
|
})
|
|
r.Add("network-b", time.Minute, func(context.Context) error {
|
|
ran = append(ran, "network-b")
|
|
return nil
|
|
})
|
|
r.AddLocal("local-c", time.Minute, func(context.Context) error {
|
|
ran = append(ran, "local-c")
|
|
return nil
|
|
})
|
|
|
|
r.RunOnceLocal(context.Background())
|
|
|
|
if len(ran) != 2 {
|
|
t.Fatalf("RunOnceLocal 应只跑 2 个 Local 任务, got %v", ran)
|
|
}
|
|
for _, name := range ran {
|
|
if name == "network-b" {
|
|
t.Fatalf("RunOnceLocal 不应执行网络型任务, got %v", ran)
|
|
}
|
|
}
|
|
}
|