项目文件夹

文件
2026-07-14 10:24:10 +08:00

841 行
23 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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-Jhandler 收到的 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 检查并运行到期任务。
//
// C12bper-task running 守卫(CAS)防止长任务跨 tick 重叠 spawn。
// C12c:占用守卫后**先推进 NextRun**(以上次 NextRun 锚定,非 time.Now()),
// 避免每周期累积 handler 时长致调度漂移;推进在 spawn 前,下次 tick 不会重复 spawn。
// C12aNextRun 推进在写锁内;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=67→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" - 每周日凌晨
//
// 非法表达式会 panicfail-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
}