125 lines
3.4 KiB
Go
125 lines
3.4 KiB
Go
package scheduler
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"testing"
|
||
"testing/synctest"
|
||
"time"
|
||
|
||
"hyapp/services/cron-service/internal/config"
|
||
mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
|
||
)
|
||
|
||
type synctestStore struct {
|
||
mu sync.Mutex
|
||
leases int
|
||
started []mysqlstorage.TaskRun
|
||
finished []mysqlstorage.TaskRun
|
||
}
|
||
|
||
type staticAppCodeProvider []string
|
||
|
||
func (p staticAppCodeProvider) ListEnabledAppCodes(context.Context) ([]string, error) {
|
||
return append([]string(nil), p...), nil
|
||
}
|
||
|
||
func (s *synctestStore) TryAcquireTaskLease(_ context.Context, _ string, _ string, _ string, _ int64, _ time.Duration) (bool, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.leases++
|
||
return true, nil
|
||
}
|
||
|
||
func (s *synctestStore) StartTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.started = append(s.started, run)
|
||
return nil
|
||
}
|
||
|
||
func (s *synctestStore) FinishTaskRun(_ context.Context, run mysqlstorage.TaskRun) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
s.finished = append(s.finished, run)
|
||
return nil
|
||
}
|
||
|
||
// TestSchedulerRunUsesSynctestClock 锁定 cron loop 的首轮立即执行和 interval 退避。
|
||
// synctest 的虚拟时钟让 worker/timer 测试不依赖真实 sleep,避免调度抖动污染 CI。
|
||
func TestSchedulerRunUsesSynctestClock(t *testing.T) {
|
||
synctest.Test(t, func(t *testing.T) {
|
||
store := &synctestStore{}
|
||
ctx, cancel := context.WithCancel(t.Context())
|
||
defer cancel()
|
||
|
||
var mu sync.Mutex
|
||
runTimes := make([]time.Time, 0, 2)
|
||
s := New("node-a", map[string]config.TaskConfig{
|
||
"room_outbox": {
|
||
Enabled: true,
|
||
Interval: "1s",
|
||
Timeout: "100ms",
|
||
LockTTL: "5s",
|
||
BatchSize: 8,
|
||
},
|
||
}, store, map[string]Handler{
|
||
"room_outbox": func(_ context.Context, request BatchRequest) (BatchResult, error) {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
if request.BatchSize != 8 || request.AppCode != "lalu" || request.WorkerID != "node-a" {
|
||
t.Fatalf("unexpected request: %+v", request)
|
||
}
|
||
runTimes = append(runTimes, time.Now())
|
||
return BatchResult{ProcessedCount: 1}, nil
|
||
},
|
||
}, staticAppCodeProvider{"lalu"})
|
||
|
||
go s.Run(ctx)
|
||
synctest.Wait()
|
||
mu.Lock()
|
||
if len(runTimes) != 1 {
|
||
t.Fatalf("first run count = %d, want 1", len(runTimes))
|
||
}
|
||
firstRunAt := runTimes[0]
|
||
mu.Unlock()
|
||
|
||
time.Sleep(time.Second)
|
||
synctest.Wait()
|
||
mu.Lock()
|
||
if len(runTimes) != 2 {
|
||
t.Fatalf("second run count = %d, want 2", len(runTimes))
|
||
}
|
||
if got := runTimes[1].Sub(firstRunAt); got != time.Second {
|
||
t.Fatalf("interval = %s, want 1s", got)
|
||
}
|
||
mu.Unlock()
|
||
|
||
cancel()
|
||
synctest.Wait()
|
||
store.mu.Lock()
|
||
defer store.mu.Unlock()
|
||
if store.leases != 2 || len(store.started) != 2 || len(store.finished) != 2 {
|
||
t.Fatalf("store calls: leases=%d started=%d finished=%d", store.leases, len(store.started), len(store.finished))
|
||
}
|
||
})
|
||
}
|
||
|
||
func TestSchedulerDiscoversEveryEnabledTenant(t *testing.T) {
|
||
s := New("node-a", nil, nil, nil, staticAppCodeProvider{" huwaa ", "lalu", "huwaa", "fami", ""})
|
||
|
||
appCodes, err := s.enabledAppCodes(context.Background())
|
||
if err != nil {
|
||
t.Fatalf("enabledAppCodes failed: %v", err)
|
||
}
|
||
want := []string{"fami", "huwaa", "lalu"}
|
||
if len(appCodes) != len(want) {
|
||
t.Fatalf("app code count mismatch: got=%v want=%v", appCodes, want)
|
||
}
|
||
for index := range want {
|
||
if appCodes[index] != want[index] {
|
||
t.Fatalf("app code mismatch: got=%v want=%v", appCodes, want)
|
||
}
|
||
}
|
||
}
|