841 lines
23 KiB
Go
841 lines
23 KiB
Go
package cron
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"runtime/debug"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/logger"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// Task 定时任务
|
||
type Task struct {
|
||
Name string // 任务名称
|
||
Schedule Schedule // 调度规则
|
||
Handler TaskHandler // 任务处理函数
|
||
Enabled bool // 是否启用
|
||
LastRun time.Time // 上次运行时间
|
||
NextRun time.Time // 下次运行时间
|
||
RunCount int // 运行次数
|
||
LastError error // CR3 修复:最近一次执行错误
|
||
|
||
// running 防止长任务跨 tick 重叠执行(C12b)。
|
||
// 用指针以便 GetTask/ListTasks 返回 Task 拷贝时不触发 atomic.Bool 值类型的
|
||
// copylocks 警告;拷贝共享同一守卫状态,但该字段未导出,外部无法操作。
|
||
// nil 表示未初始化(仅非 AddTask 构造的 Task),checkAndRun/RunTask 以 nil 守卫跳过。
|
||
running *atomic.Bool
|
||
}
|
||
|
||
// TaskHandler 任务处理函数
|
||
type TaskHandler func(ctx context.Context) error
|
||
|
||
// Schedule 调度接口
|
||
type Schedule interface {
|
||
Next(now time.Time) time.Time
|
||
}
|
||
|
||
func validateSchedule(schedule Schedule) {
|
||
switch s := schedule.(type) {
|
||
case *IntervalSchedule:
|
||
validateInterval(s.Interval)
|
||
case *DailySchedule:
|
||
validateClock(s.Hour, s.Minute, "Daily")
|
||
case *WeeklySchedule:
|
||
validateWeekday(s.Day)
|
||
validateClock(s.Hour, s.Minute, "Weekly")
|
||
}
|
||
}
|
||
|
||
func validateInterval(interval time.Duration) {
|
||
if interval <= 0 {
|
||
panic("cron: interval must be positive")
|
||
}
|
||
}
|
||
|
||
func validateClock(hour, minute int, name string) {
|
||
if hour < 0 || hour > 23 {
|
||
panic(fmt.Sprintf("cron: %s hour must be in [0,23]", name))
|
||
}
|
||
if minute < 0 || minute > 59 {
|
||
panic(fmt.Sprintf("cron: %s minute must be in [0,59]", name))
|
||
}
|
||
}
|
||
|
||
func validateWeekday(day time.Weekday) {
|
||
if day < time.Sunday || day > time.Saturday {
|
||
panic("cron: Weekly day must be in [Sunday,Saturday]")
|
||
}
|
||
}
|
||
|
||
// Scheduler 调度器
|
||
type Scheduler struct {
|
||
tasks map[string]*Task
|
||
mu sync.RWMutex
|
||
lifecycleMu sync.Mutex
|
||
ctx context.Context
|
||
cancel context.CancelFunc
|
||
wg sync.WaitGroup
|
||
running bool
|
||
}
|
||
|
||
func newSchedulerContext() (context.Context, context.CancelFunc) {
|
||
return context.WithCancel(context.Background())
|
||
}
|
||
|
||
// NewScheduler 创建调度器
|
||
func NewScheduler() *Scheduler {
|
||
ctx, cancel := newSchedulerContext()
|
||
return &Scheduler{
|
||
tasks: make(map[string]*Task),
|
||
ctx: ctx,
|
||
cancel: cancel,
|
||
}
|
||
}
|
||
|
||
// AddTask 添加任务
|
||
func (s *Scheduler) AddTask(name string, schedule Schedule, handler TaskHandler) *Task {
|
||
if schedule == nil {
|
||
panic("cron: AddTask requires a non-nil schedule")
|
||
}
|
||
if handler == nil {
|
||
panic("cron: AddTask requires a non-nil handler")
|
||
}
|
||
validateSchedule(schedule)
|
||
|
||
task := &Task{
|
||
Name: name,
|
||
Schedule: schedule,
|
||
Handler: handler,
|
||
Enabled: true,
|
||
NextRun: schedule.Next(time.Now()),
|
||
running: &atomic.Bool{},
|
||
}
|
||
|
||
s.mu.Lock()
|
||
s.tasks[name] = task
|
||
s.mu.Unlock()
|
||
|
||
return task
|
||
}
|
||
|
||
// RemoveTask 移除任务
|
||
func (s *Scheduler) RemoveTask(name string) {
|
||
s.mu.Lock()
|
||
delete(s.tasks, name)
|
||
s.mu.Unlock()
|
||
}
|
||
|
||
// EnableTask 启用任务
|
||
func (s *Scheduler) EnableTask(name string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
task, ok := s.tasks[name]
|
||
if !ok {
|
||
return fmt.Errorf("任务不存在: %s", name)
|
||
}
|
||
|
||
task.Enabled = true
|
||
task.NextRun = task.Schedule.Next(time.Now())
|
||
return nil
|
||
}
|
||
|
||
// DisableTask 禁用任务
|
||
func (s *Scheduler) DisableTask(name string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
task, ok := s.tasks[name]
|
||
if !ok {
|
||
return fmt.Errorf("任务不存在: %s", name)
|
||
}
|
||
|
||
task.Enabled = false
|
||
return nil
|
||
}
|
||
|
||
// GetTask 获取任务(返回拷贝快照,避免外部并发读 live 指针,C12a)。
|
||
func (s *Scheduler) GetTask(name string) (*Task, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
task, ok := s.tasks[name]
|
||
if !ok {
|
||
return nil, fmt.Errorf("任务不存在: %s", name)
|
||
}
|
||
cp := *task
|
||
return &cp, nil
|
||
}
|
||
|
||
// ListTasks 获取所有任务(返回拷贝快照,C12a)。
|
||
func (s *Scheduler) ListTasks() []*Task {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
|
||
tasks := make([]*Task, 0, len(s.tasks))
|
||
for _, task := range s.tasks {
|
||
cp := *task
|
||
tasks = append(tasks, &cp)
|
||
}
|
||
return tasks
|
||
}
|
||
|
||
// executeTask 在 exec 边界运行 task handler;recover 捕获 panic 转为 error(含调用栈),
|
||
// 防止调度 goroutine 内未 recover 的 panic 终止整个进程(M13)。
|
||
// 命名返回值让 defer 在 panic 时改写 err。两侧调用点(RunTask / checkAndRun goroutine)
|
||
// 共用此边界,panic 一律转为 error 记入 LastError 并向上返回,不破坏 running 守卫与 wg.Done
|
||
// (recover 在本函数内部完成,外侧 defer 仍正常执行)。
|
||
func (s *Scheduler) executeTask(t *Task) (err error) {
|
||
s.mu.RLock()
|
||
ctx := s.ctx
|
||
s.mu.RUnlock()
|
||
return s.executeTaskWithContext(ctx, t)
|
||
}
|
||
|
||
func (s *Scheduler) executeTaskWithContext(ctx context.Context, t *Task) (err error) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
err = fmt.Errorf("cron task %q panic recovered: %v\n%s", t.Name, r, debug.Stack())
|
||
}
|
||
}()
|
||
return t.Handler(ctx)
|
||
}
|
||
|
||
// RunTask 立即运行任务(手动触发,同步返回 handler 错误)。
|
||
//
|
||
// 占用 per-task running 守卫,与调度循环互斥,防止同一任务重叠执行(C12b)。
|
||
// 不推进 NextRun(手动触发不影响调度节奏,C12c)。LastRun/RunCount 在锁内更新(C12a)。
|
||
//
|
||
// L-J:handler 收到的 ctx 为调度器 s.ctx。Stop 后 s.ctx 已取消,handler 会收到 canceled
|
||
// ctx——这是预期行为(手动触发在调度器停止后不应继续执行长任务)。调用方若需在 Stop 后
|
||
// 仍运行一次性任务,应在自己的 ctx 下执行而非依赖调度器。
|
||
func (s *Scheduler) RunTask(name string) error {
|
||
s.mu.Lock()
|
||
task, ok := s.tasks[name]
|
||
s.mu.Unlock()
|
||
|
||
if !ok {
|
||
return fmt.Errorf("任务不存在: %s", name)
|
||
}
|
||
|
||
// 占用 running 守卫(nil 守卫直接放行,仅防御非 AddTask 构造的 Task)。
|
||
if task.running != nil && !task.running.CompareAndSwap(false, true) {
|
||
return fmt.Errorf("任务正在执行中: %s", name)
|
||
}
|
||
defer func() {
|
||
if task.running != nil {
|
||
task.running.Store(false)
|
||
}
|
||
}()
|
||
|
||
err := s.executeTask(task)
|
||
|
||
s.mu.Lock()
|
||
task.LastRun = time.Now()
|
||
task.RunCount++
|
||
task.LastError = err // M13: 手动路径也记 LastError(与调度路径对齐)
|
||
s.mu.Unlock()
|
||
|
||
return err
|
||
}
|
||
|
||
// Start 启动调度器
|
||
func (s *Scheduler) Start() {
|
||
s.lifecycleMu.Lock()
|
||
defer s.lifecycleMu.Unlock()
|
||
|
||
s.mu.Lock()
|
||
if s.running {
|
||
s.mu.Unlock()
|
||
return
|
||
}
|
||
select {
|
||
case <-s.ctx.Done():
|
||
s.ctx, s.cancel = newSchedulerContext()
|
||
default:
|
||
}
|
||
ctx := s.ctx
|
||
s.running = true
|
||
s.wg.Add(1)
|
||
s.mu.Unlock()
|
||
|
||
go s.run(ctx)
|
||
}
|
||
|
||
// Stop 停止调度器并无限等待在跑任务退出(要求 handler 尊重 ctx.Done)。
|
||
// 若担心某 handler 不响应 ctx 而永久阻塞,请用 StopWithTimeout。
|
||
func (s *Scheduler) Stop() {
|
||
s.StopWithTimeout(0)
|
||
}
|
||
|
||
// StopWithTimeout 停止调度器,最多等待 timeout 让在跑任务退出(P1 #10)。
|
||
// 返回 true 表示所有任务已退出;false 表示超时(仍有任务未响应 ctx.Done 而运行)。
|
||
// timeout<=0 等价于无限等待(同 Stop)。幂等:未运行时直接返回 true。
|
||
func (s *Scheduler) StopWithTimeout(timeout time.Duration) bool {
|
||
s.lifecycleMu.Lock()
|
||
defer s.lifecycleMu.Unlock()
|
||
|
||
s.mu.Lock()
|
||
if !s.running {
|
||
s.mu.Unlock()
|
||
return true
|
||
}
|
||
s.running = false
|
||
cancel := s.cancel
|
||
s.mu.Unlock()
|
||
|
||
cancel()
|
||
if timeout <= 0 {
|
||
s.wg.Wait()
|
||
return true
|
||
}
|
||
done := make(chan struct{})
|
||
go func() { s.wg.Wait(); close(done) }()
|
||
select {
|
||
case <-done:
|
||
return true
|
||
case <-time.After(timeout):
|
||
return false
|
||
}
|
||
}
|
||
|
||
// run 运行调度循环
|
||
func (s *Scheduler) run(ctx context.Context) {
|
||
defer s.wg.Done()
|
||
|
||
ticker := time.NewTicker(1 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
s.checkAndRun(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// checkAndRun 检查并运行到期任务。
|
||
//
|
||
// C12b:per-task running 守卫(CAS)防止长任务跨 tick 重叠 spawn。
|
||
// C12c:占用守卫后**先推进 NextRun**(以上次 NextRun 锚定,非 time.Now()),
|
||
// 避免每周期累积 handler 时长致调度漂移;推进在 spawn 前,下次 tick 不会重复 spawn。
|
||
// C12a:NextRun 推进在写锁内;LastRun/RunCount 在 goroutine 内写锁更新。
|
||
//
|
||
// M-H 修复:锁内只收集到期任务(CAS 占用 + 推进 NextRun + wg.Add),锁外再 spawn。
|
||
// 原实现整个遍历+spawn 持写锁,task 多时阻塞 GetTask/AddTask/RunTask 等管理 API。
|
||
//
|
||
// wg.Add 必须在锁内完成:Stop 先持锁置 running=false 再 wg.Wait,若 wg.Add 在锁外,
|
||
// Stop 可能在两个 due 任务之间读到计数器 0 并提前返回,留下后启动的 goroutine 在调度器
|
||
// 停止后运行(其 wg.Done 还会触发计数器下溢 panic)。锁内 Add 保证 Stop 的 wg.Wait
|
||
// 一定能等到本批全部 goroutine。收集阶段已 CAS 占用守卫并推进 NextRun,故 spawn 必须
|
||
// 执行(否则守卫不释放、NextRun 已推进却未跑)。spawn 用 s.ctx——Stop 后 ctx 已取消,
|
||
// handler 收到 canceled ctx 应及时退出。
|
||
func (s *Scheduler) checkAndRun(ctxs ...context.Context) {
|
||
now := time.Now()
|
||
var ctx context.Context
|
||
enforceRunning := len(ctxs) > 0 && ctxs[0] != nil
|
||
if len(ctxs) > 0 && ctxs[0] != nil {
|
||
ctx = ctxs[0]
|
||
} else {
|
||
s.mu.RLock()
|
||
ctx = s.ctx
|
||
s.mu.RUnlock()
|
||
}
|
||
|
||
s.mu.Lock()
|
||
if enforceRunning && (!s.running || ctx.Err() != nil) {
|
||
s.mu.Unlock()
|
||
return
|
||
}
|
||
var due []*Task
|
||
for _, task := range s.tasks {
|
||
if !task.Enabled || task.NextRun.IsZero() || !now.After(task.NextRun) {
|
||
continue
|
||
}
|
||
// 占用 running 守卫;正在执行则跳过本轮(防重叠 C12b)。
|
||
if task.running != nil && !task.running.CompareAndSwap(false, true) {
|
||
continue
|
||
}
|
||
// 先推进 NextRun(以上次 NextRun 锚定防漂移 C12c),再收集待 spawn。
|
||
task.NextRun = task.Schedule.Next(task.NextRun)
|
||
s.wg.Add(1) // 锁内 Add,保证 Stop 的 wg.Wait 等到本批(见上方注释)
|
||
due = append(due, task)
|
||
}
|
||
s.mu.Unlock()
|
||
|
||
for _, t := range due {
|
||
go func(t *Task) {
|
||
defer s.wg.Done()
|
||
defer func() {
|
||
if t.running != nil {
|
||
t.running.Store(false)
|
||
}
|
||
}()
|
||
|
||
err := s.executeTaskWithContext(ctx, t)
|
||
|
||
s.mu.Lock()
|
||
t.LastRun = time.Now()
|
||
t.RunCount++
|
||
t.LastError = err // CR3 修复:记录错误不再静默丢弃
|
||
s.mu.Unlock()
|
||
if err != nil {
|
||
logger.Error("cron task failed",
|
||
zap.String("task", t.Name),
|
||
zap.Error(err))
|
||
}
|
||
}(t)
|
||
}
|
||
}
|
||
|
||
// IntervalSchedule 固定间隔调度
|
||
type IntervalSchedule struct {
|
||
Interval time.Duration
|
||
}
|
||
|
||
// Next 计算下次运行时间
|
||
func (s *IntervalSchedule) Next(now time.Time) time.Time {
|
||
return now.Add(s.Interval)
|
||
}
|
||
|
||
// Every 每隔指定时间运行
|
||
func Every(interval time.Duration) *IntervalSchedule {
|
||
validateInterval(interval)
|
||
return &IntervalSchedule{Interval: interval}
|
||
}
|
||
|
||
// DailySchedule 每日定时调度
|
||
type DailySchedule struct {
|
||
Hour int
|
||
Minute int
|
||
}
|
||
|
||
// Next 计算下次运行时间
|
||
func (s *DailySchedule) Next(now time.Time) time.Time {
|
||
next := time.Date(now.Year(), now.Month(), now.Day(), s.Hour, s.Minute, 0, 0, now.Location())
|
||
if next.Before(now) || next.Equal(now) {
|
||
next = next.Add(24 * time.Hour)
|
||
}
|
||
return next
|
||
}
|
||
|
||
// Daily 每日指定时间运行
|
||
func Daily(hour, minute int) *DailySchedule {
|
||
validateClock(hour, minute, "Daily")
|
||
return &DailySchedule{Hour: hour, Minute: minute}
|
||
}
|
||
|
||
// WeeklySchedule 每周定时调度
|
||
type WeeklySchedule struct {
|
||
Day time.Weekday
|
||
Hour int
|
||
Minute int
|
||
}
|
||
|
||
// Next 计算下次运行时间。
|
||
//
|
||
// C12d:原实现 `daysUntil <= 0 → +7` 仅按 weekday 差值,不比较当天时刻,
|
||
// 当天目标时刻未到(如周一 9:00 目标、当前周一 12:00 之前的 8:00)被错误跳一周。
|
||
// 改为:先算今天的目标时刻,按 `((day-now)+7)%7` 加天数,再与 now 比较——
|
||
// 当天未到点则本周,当天已过则下周。
|
||
func (s *WeeklySchedule) Next(now time.Time) time.Time {
|
||
next := time.Date(now.Year(), now.Month(), now.Day(), s.Hour, s.Minute, 0, 0, now.Location())
|
||
daysUntil := (int(s.Day) - int(now.Weekday()) + 7) % 7
|
||
next = next.AddDate(0, 0, daysUntil)
|
||
if !next.After(now) {
|
||
next = next.AddDate(0, 0, 7)
|
||
}
|
||
return next
|
||
}
|
||
|
||
// Weekly 每周指定时间运行
|
||
func Weekly(day time.Weekday, hour, minute int) *WeeklySchedule {
|
||
validateWeekday(day)
|
||
validateClock(hour, minute, "Weekly")
|
||
return &WeeklySchedule{Day: day, Hour: hour, Minute: minute}
|
||
}
|
||
|
||
// FullCronSchedule 完整 Cron 表达式调度
|
||
// 格式: "分钟 小时 日 月 星期" (5字段)
|
||
// 示例: "0 12 * * *" 每天12点
|
||
//
|
||
// "0 0 1 * *" 每月1号凌晨
|
||
// "0 9-17 * * 1-5" 周一到周五 9点到17点每小时
|
||
type FullCronSchedule struct {
|
||
Minute string // 分钟: 0-59, "*", "*/n", "a-b", "a,b,c"
|
||
Hour string // 小时: 0-23, "*", "*/n", "a-b", "a,b,c"
|
||
Day string // 日: 1-31, "*", "*/n", "a-b", "a,b,c"
|
||
Month string // 月: 1-12, "*", "*/n", "a-b", "a,b,c"
|
||
Weekday string // 星期: 0-6 (周日=0), "*", "*/n", "a-b", "a,b,c"
|
||
}
|
||
|
||
// Next 计算下次运行时间
|
||
func (s *FullCronSchedule) Next(now time.Time) time.Time {
|
||
// 从下一分钟开始查找
|
||
next := now.Add(time.Minute)
|
||
next = time.Date(next.Year(), next.Month(), next.Day(), next.Hour(), next.Minute(), 0, 0, next.Location())
|
||
|
||
// 最多查找一年(防止无效表达式死循环)
|
||
maxAttempts := 366 * 24 * 60
|
||
for i := 0; i < maxAttempts; i++ {
|
||
if s.match(next) {
|
||
return next
|
||
}
|
||
next = next.Add(time.Minute)
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
// match 检查时间是否匹配表达式
|
||
func (s *FullCronSchedule) match(t time.Time) bool {
|
||
return s.matchField(s.Minute, t.Minute(), 0, 59) &&
|
||
s.matchField(s.Hour, t.Hour(), 0, 23) &&
|
||
s.matchField(s.Day, t.Day(), 1, 31) &&
|
||
s.matchField(s.Month, int(t.Month()), 1, 12) &&
|
||
s.matchField(s.Weekday, int(t.Weekday()), 0, 6)
|
||
}
|
||
|
||
// matchField 匹配单个字段(C12e 重写)。
|
||
//
|
||
// 旧实现先判 `-` 再判 `*/` 最后列表,且 parseInt 忽略非数字逐位累积:
|
||
// - `1-5,8` 因整字段含 `-` 被当范围,parseInt("5,8")=58 → 范围被破坏为 1..58,列表项丢失。
|
||
// - `garbage` → parseInt=0,分/时/周字段 value=0 时误触发。
|
||
// - `*/garbage` → step=0 → return true 匹配全部。
|
||
// - 周日 `7` 不匹配(Go Sunday=0)。
|
||
//
|
||
// 新实现:先按逗号拆列表,每项独立判 `*/n` / `a-b/n` / `a-b` / 单值(列表分支独立于范围分支);
|
||
// 全部用 strconv.Atoi 返错;weekday 字段(min=0,max=6)7→0,范围 lo>hi 环绕。
|
||
func (s *FullCronSchedule) matchField(field string, value int, min, max int) bool {
|
||
if field == "*" {
|
||
return true
|
||
}
|
||
for _, raw := range strings.Split(field, ",") {
|
||
item := strings.TrimSpace(raw)
|
||
if item == "" {
|
||
continue
|
||
}
|
||
if matchCronItem(item, value, min, max) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// matchCronItem 处理单个 cron 字段项(已按逗号拆分)。
|
||
func matchCronItem(item string, value, min, max int) bool {
|
||
isWeekday := min == 0 && max == 6
|
||
|
||
// 步长 "*/n" 或 "a-b/n"
|
||
if idx := strings.Index(item, "/"); idx >= 0 {
|
||
base := item[:idx]
|
||
step, err := strconv.Atoi(item[idx+1:])
|
||
if err != nil || step <= 0 {
|
||
return false
|
||
}
|
||
lo, hi := min, max
|
||
if base != "*" {
|
||
rlo, rhi, err := parseCronRange(base, min, max)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
lo, hi = rlo, rhi
|
||
}
|
||
return value >= lo && value <= hi && (value-lo)%step == 0
|
||
}
|
||
|
||
// 范围 "a-b"
|
||
if strings.Contains(item, "-") {
|
||
lo, hi, err := parseCronRange(item, min, max)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
if isWeekday && lo > hi {
|
||
// 环绕:lo..6 ∪ 0..hi(如 "6-1" = 周六、周日、周一)
|
||
return (value >= lo && value <= max) || (value >= min && value <= hi)
|
||
}
|
||
return value >= lo && value <= hi
|
||
}
|
||
|
||
// 单值
|
||
v, err := strconv.Atoi(item)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
if isWeekday && v == 7 {
|
||
v = 0
|
||
}
|
||
if v < min || v > max {
|
||
return false
|
||
}
|
||
return v == value
|
||
}
|
||
|
||
// parseCronRange 解析 "a-b" 范围,含边界校验与 weekday 7→0 归一化。
|
||
func parseCronRange(s string, min, max int) (int, int, error) {
|
||
parts := strings.SplitN(s, "-", 2)
|
||
if len(parts) != 2 {
|
||
return 0, 0, fmt.Errorf("invalid range %q", s)
|
||
}
|
||
lo, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||
hi, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||
if err1 != nil || err2 != nil {
|
||
return 0, 0, fmt.Errorf("invalid range %q", s)
|
||
}
|
||
isWeekday := min == 0 && max == 6
|
||
if isWeekday {
|
||
// 7 → 0(周日)。但若两端归一化后都为 0 而原始值不同(如 "0-7"/"7-0"),
|
||
// 语义为"整周"却坍缩成"仅周日",属歧义范围,拒绝以免静默错误匹配。
|
||
nlo, nhi := lo, hi
|
||
if nlo == 7 {
|
||
nlo = 0
|
||
}
|
||
if nhi == 7 {
|
||
nhi = 0
|
||
}
|
||
if nlo == nhi && lo != hi {
|
||
return 0, 0, fmt.Errorf("ambiguous weekday range %q (0 与 7 均为周日)", s)
|
||
}
|
||
lo, hi = nlo, nhi
|
||
}
|
||
if lo < min || lo > max || hi < min || hi > max {
|
||
return 0, 0, fmt.Errorf("range out of bounds %q", s)
|
||
}
|
||
return lo, hi, nil
|
||
}
|
||
|
||
// ParseCron 解析完整 Cron 表达式。
|
||
// 格式: "分钟 小时 日 月 星期"
|
||
// 示例:
|
||
//
|
||
// "0 12 * * *" - 每天12:00
|
||
// "*/15 * * * *" - 每15分钟
|
||
// "0 9-17 * * 1-5" - 工作日9-17点每小时
|
||
// "0 0 1 * *" - 每月1号凌晨
|
||
// "0 0 * * 0" - 每周日凌晨
|
||
//
|
||
// 非法表达式会 panic(fail-fast),动态输入请用 ParseCronStrict 处理 error。
|
||
// 如需旧版“非法表达式回退为每分钟”的兼容语义,请显式使用 ParseCronOrDefault。
|
||
func ParseCron(expr string) *FullCronSchedule {
|
||
if sched, err := ParseCronStrict(expr); err == nil {
|
||
return sched
|
||
} else {
|
||
panic(err)
|
||
}
|
||
}
|
||
|
||
// ParseCronOrDefault parses a Cron expression and falls back to all "*" (every
|
||
// minute) when expr is invalid. Prefer ParseCronStrict or ParseCron for new
|
||
// code; this helper exists for callers that intentionally want legacy fallback
|
||
// semantics.
|
||
func ParseCronOrDefault(expr string) *FullCronSchedule {
|
||
if sched, err := ParseCronStrict(expr); err == nil {
|
||
return sched
|
||
}
|
||
return &FullCronSchedule{"*", "*", "*", "*", "*"}
|
||
}
|
||
|
||
// ParseCronStrict 严格解析 Cron 表达式,校验字段数与各字段范围,非法返 error。
|
||
// 字段范围:分钟 0-59,小时 0-23,日 1-31,月 1-12,星期 0-6(周日=0,7 归一为 0)。
|
||
func ParseCronStrict(expr string) (*FullCronSchedule, error) {
|
||
fields := strings.Fields(expr)
|
||
if len(fields) != 5 {
|
||
return nil, fmt.Errorf("cron: 需要 5 个字段,实际 %d", len(fields))
|
||
}
|
||
specs := []struct {
|
||
val string
|
||
min, max int
|
||
name string
|
||
}{
|
||
{fields[0], 0, 59, "minute"},
|
||
{fields[1], 0, 23, "hour"},
|
||
{fields[2], 1, 31, "day"},
|
||
{fields[3], 1, 12, "month"},
|
||
{fields[4], 0, 6, "weekday"},
|
||
}
|
||
for _, sp := range specs {
|
||
if err := validateCronField(sp.val, sp.min, sp.max, sp.name); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return &FullCronSchedule{
|
||
Minute: fields[0],
|
||
Hour: fields[1],
|
||
Day: fields[2],
|
||
Month: fields[3],
|
||
Weekday: fields[4],
|
||
}, nil
|
||
}
|
||
|
||
// validateCronField 校验单个 cron 字段语法与范围。
|
||
func validateCronField(field string, min, max int, name string) error {
|
||
if field == "*" {
|
||
return nil
|
||
}
|
||
for _, raw := range strings.Split(field, ",") {
|
||
item := strings.TrimSpace(raw)
|
||
if item == "" {
|
||
return fmt.Errorf("cron %s: 空列表项", name)
|
||
}
|
||
if err := validateCronItem(item, min, max, name); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateCronItem(item string, min, max int, name string) error {
|
||
isWeekday := min == 0 && max == 6
|
||
norm := func(v int) int {
|
||
if isWeekday && v == 7 {
|
||
return 0
|
||
}
|
||
return v
|
||
}
|
||
if idx := strings.Index(item, "/"); idx >= 0 {
|
||
base := item[:idx]
|
||
step, err := strconv.Atoi(item[idx+1:])
|
||
if err != nil || step <= 0 {
|
||
return fmt.Errorf("cron %s: 非法步长 %q", name, item)
|
||
}
|
||
if base == "*" {
|
||
return nil
|
||
}
|
||
lo, hi, err := parseCronRange(base, min, max)
|
||
if err != nil {
|
||
return fmt.Errorf("cron %s: %v", name, err)
|
||
}
|
||
_ = lo
|
||
_ = hi
|
||
return nil
|
||
}
|
||
if strings.Contains(item, "-") {
|
||
if _, _, err := parseCronRange(item, min, max); err != nil {
|
||
return fmt.Errorf("cron %s: %v", name, err)
|
||
}
|
||
return nil
|
||
}
|
||
v, err := strconv.Atoi(item)
|
||
if err != nil {
|
||
return fmt.Errorf("cron %s: 非数字 %q", name, item)
|
||
}
|
||
if v = norm(v); v < min || v > max {
|
||
return fmt.Errorf("cron %s: %d 超出范围 [%d,%d]", name, v, min, max)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CronSchedule 简化 Cron 调度(仅分钟和小时)
|
||
type CronSchedule struct {
|
||
Minute string // 分钟: "*" 或具体值如 "0,15,30"
|
||
Hour string // 小时: "*" 或具体值如 "8,12"
|
||
}
|
||
|
||
// Next 计算下次运行时间
|
||
func (s *CronSchedule) Next(now time.Time) time.Time {
|
||
for i := 1; i <= 60*24; i++ { // 最多查找24小时
|
||
next := now.Add(time.Duration(i) * time.Minute)
|
||
if s.matchMinute(next.Minute()) && s.matchHour(next.Hour()) {
|
||
return next
|
||
}
|
||
}
|
||
return time.Time{}
|
||
}
|
||
|
||
func (s *CronSchedule) matchMinute(minute int) bool {
|
||
return s.Minute == "*" || s.matchValue(s.Minute, minute)
|
||
}
|
||
|
||
func (s *CronSchedule) matchHour(hour int) bool {
|
||
return s.Hour == "*" || s.matchValue(s.Hour, hour)
|
||
}
|
||
|
||
func (s *CronSchedule) matchValue(pattern string, value int) bool {
|
||
for _, p := range splitPattern(pattern) {
|
||
if p == value {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// splitPattern 将逗号分隔的模式解析为值列表(C12e:用 strconv.Atoi,非法项跳过)。
|
||
func splitPattern(pattern string) []int {
|
||
var values []int
|
||
for _, p := range strings.Split(pattern, ",") {
|
||
v, err := strconv.Atoi(strings.TrimSpace(p))
|
||
if err != nil {
|
||
continue
|
||
}
|
||
values = append(values, v)
|
||
}
|
||
return values
|
||
}
|
||
|
||
// Cron 创建类 Cron 调度
|
||
func Cron(minute, hour string) *CronSchedule {
|
||
return &CronSchedule{Minute: minute, Hour: hour}
|
||
}
|
||
|
||
// 全局调度器。init 时预创建默认实例(与 storage.DefaultStorage / cache.defaultCachePtr
|
||
// 对齐),使 SwapDefaultScheduler 返回非 nil、App 回滚总能恢复一个有效默认。用 atomic.Pointer
|
||
// 保护并发读写,消除原 once+裸指针读写竞态。GetScheduler 仍保留懒初始化分支作防御。
|
||
var globalScheduler atomic.Pointer[Scheduler]
|
||
|
||
func init() {
|
||
globalScheduler.Store(NewScheduler())
|
||
}
|
||
|
||
// GetScheduler 获取全局调度器(并发安全;init 后通常非 nil,保留懒初始化作防御)。
|
||
func GetScheduler() *Scheduler {
|
||
if s := globalScheduler.Load(); s != nil {
|
||
return s
|
||
}
|
||
ns := NewScheduler()
|
||
if globalScheduler.CompareAndSwap(nil, ns) {
|
||
return ns
|
||
}
|
||
return globalScheduler.Load()
|
||
}
|
||
|
||
// SwapDefaultScheduler 将指定 Scheduler 置为全局默认,并返回被替换的旧调度器。
|
||
// 旧调度器不会被停止,供 App 初始化这类需要失败回滚的生命周期流程暂存(照
|
||
// database.SwapDefaultManager / SwapDefaultRedisManager 模式)。nil 被忽略,返回当前默认。
|
||
func SwapDefaultScheduler(s *Scheduler) *Scheduler {
|
||
if s == nil {
|
||
return globalScheduler.Load()
|
||
}
|
||
return globalScheduler.Swap(s)
|
||
}
|
||
|
||
// AddTask 添加任务到全局调度器
|
||
func AddTask(name string, schedule Schedule, handler TaskHandler) *Task {
|
||
return GetScheduler().AddTask(name, schedule, handler)
|
||
}
|
||
|
||
// Start 启动全局调度器
|
||
func Start() {
|
||
GetScheduler().Start()
|
||
}
|
||
|
||
// Stop 停止全局调度器(无限等待,见 Scheduler.Stop)。
|
||
func Stop() {
|
||
GetScheduler().Stop()
|
||
}
|
||
|
||
// StopGlobalWithTimeout 停止全局调度器并最多等待 timeout(P1 #10)。
|
||
// 全局调度器尚未创建时无操作返回 true,不会因此惰性创建它——供 App.Shutdown 安全调用。
|
||
func StopGlobalWithTimeout(timeout time.Duration) bool {
|
||
if s := globalScheduler.Load(); s != nil {
|
||
return s.StopWithTimeout(timeout)
|
||
}
|
||
return true
|
||
}
|