# TaskScheduler **Repository Path**: vipkwds/taskscheduler ## Basic Information - **Project Name**: TaskScheduler - **Description**: `taskscheduler` 是一个轻量级 Go 任务调度器,支持: - 周期任务(基于Task 接口) - 计时器任务(立即、延迟、定时、Cron) - 动态周期配置(通过 Redis 等自定义配置源) - 生命周期管理(暂停、恢复、停止) - 回调钩子(前置、后置、暂停、恢复、停止) - **Primary Language**: Go - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-20 - **Last Updated**: 2026-05-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # taskscheduler 任务调度器 ## 概述 `taskscheduler` 是一个轻量级 Go 任务调度器,支持: - 周期任务(基于 Task 接口) - 计时器任务(立即、延迟、定时、Cron) - 动态周期配置(通过 Redis 等配置源) - 生命周期管理(暂停、恢复、停止) - 回调钩子(前置、后置、暂停、恢复、停止) ### V2 ID-based 设计 **重要变更**:V2 版本采用 **ID-based** 任务管理: - 所有内部 map 使用 `taskId` 作为 key - `name` 仅用于显示目的,不作为逻辑判断因子 - 所有 API 均通过 `taskId` 操作任务 ### 统一抽象设计 所有任务(周期任务和计时器任务)都支持两种实现方式: | 实现方式 | 说明 | 适用场景 | |---------|------|---------| | `TaskFunc` 函数 | 轻量级,快速上手 | 简单的计时器任务 | | `Task` 接口 | 完整能力,支持感知接口 | 需要前置/后置回调、动态周期配置等 | --- ## 设计原则与职责边界 ### Scheduler 只负责调度 Scheduler 是一个**纯调度组件**,不负责数据持久化: | 职责范围 | 说明 | |---------|------| | **Scheduler 负责** | 任务调度、周期管理、生命周期控制、回调触发 | | **Scheduler 不负责** | 执行历史记录、统计数据持久化、任务结果存储 | ### 不负责的数据 Scheduler **内部不持久化**以下信息: - 任务执行历史 - 执行次数统计 - 最后执行时间 - 历史执行结果 - 任务状态变更记录 ### 业务侧职责 如果需要上述持久化数据,业务侧应在感知回调中自行实现: ```go func (t *DataSyncTask) OnStartAfter(taskID, taskName string, result any, err error) error { // 自己的数据库记录执行历史 saveToMyDB(taskID, taskName, result, err) return nil } ``` ### 任务标识的外部价值 由于 Scheduler 不持久化数据,**Name 变更导致历史丢失**不是 TaskScheduler 的问题: ``` 业务侧(使用方) TaskScheduler ┌─────────────────────┐ ┌──────────────────┐ │ 自己的数据库/存储 │◄─────────────│ 只负责调度 │ │ - 业务ID(主键) │ 通过 taskId │ - 按ID管理 │ │ - 执行历史 │ 关联/查询 │ - 执行回调 │ │ - 统计信息 │ │ │ └─────────────────────┘ └──────────────────┘ ``` 如果业务侧需要跨 Name 变更追踪历史,应自行维护业务表,以业务ID为主键,Name 仅作展示用。 ### 结论 - **Scheduler 设计是合理的**:专注调度,不越界 - **ID 唯一性由系统保证**:同一 ID 不会重复创建 - **持久化需求自行在回调中实现**:Scheduler 通过感知接口提供足够的扩展点 --- ## 核心接口 ### Task 接口 所有任务(周期任务和计时器任务)可选择实现此接口以获得完整能力: ```go type Task interface { Run() (any, error) // 执行任务,返回结果和错误 Name() string // 任务显示名称 ConfigKey() string // 动态周期配置的 Redis key DefaultInterval() int // 默认周期(秒),无动态配置时使用 } ``` **说明**: - 周期任务(通过 `AddTask` 添加)必须实现此接口 - 计时器任务可选择实现此接口以获得感知接口支持 ### TaskFunc 类型 计时器任务和周期调度任务的函数类型约束: ```go type TaskFunc func(taskId string) (any, error) ``` --- ## 可选感知接口 | 接口 | 方法 | 触发时机 | |------|------|----------| | `TaskPauseAwareInterface` | `OnPauseAfter(taskID, taskName string) error` | 任务被暂停时 | | `TaskPauseAwareInterface` | `OnResumeAfter(taskID, taskName string) error` | 任务被恢复时 | | `TaskStopAwareInterface` | `OnStopAfter(taskID, taskName string) error` | 任务被停止时 | | `TaskStartBeforeAwareInterface` | `OnStartBefore(taskID, taskName string) (taskSkip bool, err error)` | 任务执行前(返回 `taskSkip=true` 可跳过本次) | | `TaskStartAfterAwareInterface` | `OnStartAfter(taskID, taskName string, result any, taskRunErr error) error` | 任务执行后 | ### 感知接口使用示例 ```go type DataSyncTask struct{} func (t *DataSyncTask) Name() string { return "data_sync" } func (t *DataSyncTask) ConfigKey() string { return "task:interval:data_sync" } func (t *DataSyncTask) DefaultInterval() int { return 300 } func (t *DataSyncTask) Run() (any, error) { return "同步完成", nil } // 实现暂停感知接口 func (t *DataSyncTask) OnPauseAfter(taskID, taskName string) error { fmt.Printf("任务 %s (ID: %s) 已暂停\n", taskName, taskID) return nil } func (t *DataSyncTask) OnResumeAfter(taskID, taskName string) error { fmt.Printf("任务 %s (ID: %s) 已恢复\n", taskName, taskID) return nil } // 实现停止感知接口 func (t *DataSyncTask) OnStopAfter(taskID, taskName string) error { fmt.Printf("任务 %s (ID: %s) 已停止\n", taskName, taskID) return nil } // 实现前置感知接口(可跳过执行) func (t *DataSyncTask) OnStartBefore(taskID, taskName string) (bool, error) { if someCondition { return true, nil // 跳过本次执行 } return false, nil } // 实现后置感知接口(处理结果) func (t *DataSyncTask) OnStartAfter(taskID, taskName string, result any, taskRunErr error) error { fmt.Printf("任务 %s (ID: %s) 执行完成: result=%v, err=%v\n", taskName, taskID, result, taskRunErr) return nil } ``` --- ## 构造与初始化 ```go // 创建任务调度器 ts := taskscheduler.NewScheduler() // 设置自定义日志(可选,默认使用 fmt 输出) ts.SetLogger(&MyLogger{}) // 设置 Redis 周期配置获取器(可选) ts.SetRedisGetter(redisClient, taskscheduler.WithRedisTimeout(3*time.Second)) // 清除周期配置获取器,后续任务将使用默认周期 ts.ClearIntervalGetter() ``` ### 日志接口 ```go type Logger interface { Infof(format string, args ...interface{}) Warnf(format string, args ...interface{}) Errorf(format string, args ...interface{}) } // 使用默认日志(fmt.Println) ts := taskscheduler.NewScheduler() // 使用自定义日志 type MyLogger struct{} func (l *MyLogger) Infof(f string, args ...interface{}) { fmt.Printf("[INFO] "+f+"\n", args...) } func (l *MyLogger) Warnf(f string, args ...interface{}) { fmt.Printf("[WARN] "+f+"\n", args...) } func (l *MyLogger) Errorf(f string, args ...interface{}) { fmt.Printf("[ERROR] "+f+"\n", args...) } ts.SetLogger(&MyLogger{}) ``` --- ## 周期任务(Task 接口方式) ### 添加任务 ```go // 添加任务,返回 taskId taskId := ts.AddTask(&DataSyncTask{}) fmt.Printf("任务已添加,ID: %s\n", taskId) ``` ### 启动流程说明 当调用 `AddTask` 时: 1. 系统生成唯一 taskId 2. 根据当前全局暂停状态或任务自身暂停状态决定是否立即启动 3. 任务会立即执行一次,后续按周期循环执行 ### 移除任务 ```go // 移除任务(会执行停止回调并移入回收站) ts.Remove(taskId) ``` ### 停止任务 ```go // 停止任务(保留在管理器中,仅停止调度) ts.Stop(taskId) ``` --- ## 计时器任务调度 ### 两种实现路径 | 路径 | 方法 | 参数类型 | 适用场景 | |------|------|-----------|----------| | 轻量函数 | `Now/Delay/At/Every/Daily/Cron` | `TaskFunc` | 简单任务,快速上手 | | 完整能力 | `AddNowTask/AddDelayTask/...` | `Task` | 需要感知接口、动态周期配置 | ### 轻量函数路径 ```go // 立即执行 taskId := ts.Now(func(taskId string) (any, error) { return "完成", nil }) // 延迟执行 taskId := ts.Delay(5*time.Minute, func(taskId string) (any, error) { return "延迟完成", nil }) ``` ### 完整能力路径 ```go type MyTask struct{} func (t *MyTask) Name() string { return "完整任务" } func (t *MyTask) ConfigKey() string { return "task:interval:my_task" } func (t *MyTask) DefaultInterval() int { return 60 } func (t *MyTask) Run() (any, error) { return "完成", nil } // 实现感知接口 func (t *MyTask) OnStartBefore(taskID, taskName string) (bool, error) { fmt.Printf("任务 %s 即将执行\n", taskName) return false, nil } func (t *MyTask) OnStartAfter(taskID, taskName string, result any, err error) error { fmt.Printf("任务 %s 执行完成: %v\n", taskName, result) return nil } ts.AddNowTask("完整任务", &MyTask{}) ts.AddDailyTask("每日备份", "02:00:00", &MyTask{}) ``` ### 方法列表 **轻量函数路径:** | 方法 | 参数 | 说明 | |------|------|------| | `Now(taskFunc)` | taskFunc: `func(taskId string) (any, error)` | 立即执行 | | `Delay(delay, taskFunc)` | delay: `time.Duration` | 延迟执行 | | `At(timeSpec, taskFunc)` | timeSpec: 时间字符串/Time/Cron | 指定时间执行 | | `Every(interval, taskFunc)` | interval: `time.Duration` | 周期执行 | | `Daily(timeStr, taskFunc)` | timeStr: `"HH:MM:SS"` | 每日固定时间 | | `Cron(cronExpr, taskFunc)` | cronExpr: Cron表达式 | Cron调度 | **完整能力路径:** | 方法 | 参数 | 说明 | |------|------|------| | `AddNowTask(name, task)` | task: `Task` | 立即执行 | | `AddDelayTask(name, delay, task)` | task: `Task` | 延迟执行 | | `AddAtTask(name, timeSpec, task)` | task: `Task` | 指定时间执行 | | `AddEveryTask(name, interval, task)` | task: `Task` | 周期执行 | | `AddDailyTask(name, timeStr, task)` | task: `Task` | 每日固定时间 | | `AddCronTask(name, cronExpr, task)` | task: `Task` | Cron调度 | --- ## 详细使用案例 ### 案例1:系统初始化 ```go // 应用启动时立即执行初始化任务 ts.Now(func(taskId string) (any, error) { fmt.Println("加载配置、连接数据库、初始化资源...") return "系统就绪", nil }) ``` ### 案例2:延迟检查任务 ```go // 5分钟后检查系统健康状态 ts.Delay(5*time.Minute, func(taskId string) (any, error) { fmt.Println("执行健康检查...") return map[string]any{ "status": "healthy", "database": "connected", "memory": "normal", }, nil }) ``` ### 案例3:指定时间执行 ```go // 2026-05-20 10:00:00 执行备份 ts.At("定时备份", "2026-05-20 10:00:00", func(taskId string) (any, error) { fmt.Println("执行数据库备份...") return "备份完成", nil }) // 只给时间,自动取今天或明天(取决于当前时间, 如果今日已过 13:00 则明日13:00执行) ts.At("午间清理", "13:00", func(taskId string) (any, error) { fmt.Println("执行午间清理任务...") return "清理完成", nil }) ``` ### 案例4:每日定时任务 ```go // 每天凌晨2:30执行备份 ts.Daily("凌晨备份", "02:30:00", func(taskId string) (any, error) { fmt.Println("执行每日备份...") return "备份完成", nil }) // 每天下午6点执行统计 ts.Daily("下班统计", "18:00:00", func(taskId string) (any, error) { stats := collectDailyStats() return stats, nil }) ``` ### 案例5:周期执行任务 ```go // 每30分钟同步一次数据 ts.Every(30*time.Minute, func(taskId string) (any, error) { fmt.Println("同步数据中...") return "同步完成", nil }) // 每2小时清理一次缓存 ts.Every(2*time.Hour, func(taskId string) (any, error) { fmt.Println("清理缓存...") return "清理完成", nil }) ``` ### 案例6:Cron表达式调度 ```go // 每10分钟执行一次 ts.Cron("周期报告", "0 */10 * * * *", func(taskId string) (any, error) { fmt.Println("生成周期报告...") return "报告生成完成", nil }) // 每天凌晨执行 ts.Cron("日常任务", "0 0 2 * * *", func(taskId string) (any, error) { fmt.Println("执行日常任务...") return "任务完成", nil }) // 工作日每小时的第30分执行 ts.Cron("工作日提醒", "0 30 * * * *", func(taskId string) (any, error) { fmt.Println("工作日提醒...") return "提醒已发送", nil }) ``` ### 案例7:返回结构化数据 ```go // 返回任意类型的结果 ts.Every(1*time.Hour, func(taskId string) (any, error) { return map[string]any{ "total_users": 1000, "active_users": 150, "revenue": 50000.00, "status": "ok", }, nil }) // 返回切片结果 ts.Every(5*time.Minute, func(taskId string) (any, error) { files, err := listFiles() if err != nil { return nil, err } return files, nil }) ``` ### 案例8:过期时间自动忽略 ```go // 指定一个过去的时间,任务会被自动忽略 ts.At("过期任务", "2025-01-01 00:00:00", func(taskId string) (any, error) { fmt.Println("这个不会执行") return nil, nil }) ``` --- ## 支持的时间格式 ```go // 完整日期时间 "2026-01-07 15:00:00" "2026/01/07 15:04:05" "2026-01-02T15:04:05" // ISO 8601 // 仅日期 "2026-01-02" // 仅时间(自动取最近日期,今天或明天) "15:04:05" "15:04" "3:04 PM" // Cron 表达式 "0 */10 * * * *" // 每10分钟 "0 2 * * *" // 每天凌晨2点 "0 30 9 * * *" // 每天9:30 ``` --- ## 生命周期管理 ### 暂停与恢复 ```go // 暂停单个任务(使用 taskId) ts.Pause(taskId) // 暂停多个任务 ts.Pause(taskId1, taskId2, taskId3) // 暂停所有任务 ts.PauseAll() // 恢复单个任务 ts.Resume(taskId) // 恢复多个任务 ts.Resume(taskId1, taskId2, taskId3) // 恢复所有任务 ts.ResumeAll() ``` ### 停止与移除 ```go // 停止单个任务(保留在管理器中) ts.Stop(taskId) // 移除任务(完全移除,包括回调执行,进入回收站) ts.Remove(taskId) // 停止并清空所有任务(包括计时器任务) ts.StopAndClearAll() // 取消计时器任务 ts.CancelTimer(taskId) ``` ### 回收站操作 ```go // 列出回收站中的任务 recycleList := ts.ListRecycleBin() // 恢复任务(从回收站恢复后任务处于暂停状态) ts.Restore(taskId) // 永久删除 ts.ForceDelete(taskId) // 清空回收站 ts.ClearRecycleBin() ``` ### 完整退出示例 ```go // 当应用需要优雅退出时 func shutdown(ts *utils.TaskScheduler) { logger.Println("开始优雅退出...") ts.StopAndClearAll() logger.Println("任务管理器已停止") } ``` --- ## 状态查询 ### 获取任务列表 ```go // 列出所有周期任务(返回 taskId 列表) tasks := ts.GetTaskList() fmt.Println(tasks) // ["id1", "id2", ...] ``` ### 获取暂停的任务列表 ```go // 列出已暂停的任务(返回 taskId 列表) paused := ts.GetTasksPausedList() fmt.Println(paused) // ["taskId1", "taskId2"] ``` ### 获取任务状态详情 ```go // 获取所有任务状态(map key 为 taskId) status := ts.GetTaskStatus() for taskId, info := range status { fmt.Printf("任务ID: %s, 状态: %s, 周期: %d秒\n", taskId, info["status"], info["interval"]) } // 获取单个任务状态 status := ts.GetTaskStatus(taskId) if info, ok := status[taskId]; ok { fmt.Printf("任务状态: %s\n", info["status"]) } ``` 返回格式: ```go map[string]map[string]interface{}{ "taskId": { "status": "running", // running | paused | all_paused "interval": 300, "config": "task:interval:data_sync", }, } ``` ### 列出计时器任务 ```go // 列出所有计时器任务(含下次执行时间) timerTasks := ts.GetTimerTasks() for _, info := range timerTasks { fmt.Printf("任务ID: %s, 下次执行: %s\n", taskId["id"], info["nextRun"]) } ``` 返回格式: ```go []map[string]interface{}{ { "id": "xxxxxx", "name": "任务名", "nextRun": "2026-05-20 10:00:00", "remaining": "2h30m15s", "logs": []string{}, "runOnce": false, "executed": false, } } ``` ### 根据 ID 获取任务信息 ```go // 获取单个任务的完整信息 registry := ts.GetTaskById(taskId) if registry != nil { fmt.Printf("任务名: %s, 类型: %s, 状态: %s\n", registry.Name, registry.Type, registry.Status) } ``` --- ## 动态周期配置 通过 Redis 动态调整任务执行周期,无需重启服务。 ### 配置格式 Redis 中存储的 value 格式为 JSON 整数(秒): ```bash # 设置任务周期为 600 秒 redis> SET task:interval:data_sync 600 # 清除配置(使用默认值) redis> DEL task:interval:data_sync ``` ### 配置优先级 1. Redis 配置(有效且 > 0) 2. 任务默认周期 `DefaultInterval()` ### 完整示例 ```go package main import ( "fmt" "time" "github.com/redis/go-redis/v9" "gitee.com/vipkwds/taskscheduler" ) type MyLogger struct{} func (l *MyLogger) Infof(f string, args ...interface{}) { fmt.Printf("[INFO] "+f+"\n", args...) } func (l *MyLogger) Warnf(f string, args ...interface{}) { fmt.Printf("[WARN] "+f+"\n", args...) } func (l *MyLogger) Errorf(f string, args ...interface{}) { fmt.Printf("[ERROR] "+f+"\n", args...) } type DataSyncTask struct{} func (t *DataSyncTask) Name() string { return "data_sync" } func (t *DataSyncTask) ConfigKey() string { return "task:interval:data_sync" } func (t *DataSyncTask) DefaultInterval() int { return 300 } // 默认5分钟 func (t *DataSyncTask) Run() (any, error) { fmt.Println("执行数据同步...") return "同步完成", nil } func main() { // 1. 初始化 Redis 客户端 redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379", }) // 2. 创建任务管理器 ts := taskscheduler.NewScheduler() ts.SetLogger(&MyLogger{}) // 3. 设置 Redis 周期配置源 if ts.SetRedisGetter(redisClient, taskscheduler.WithRedisTimeout(3*time.Second)); err != nil { fmt.Printf("Redis 配置失败: %v\n", err) } // 4. 添加周期任务 taskId := ts.AddTask(&DataSyncTask{}) fmt.Printf("任务已添加,ID: %s\n", taskId) // 5. 添加计时器任务 ts.Now(func(taskId string) (any, error) { fmt.Println("立即执行的任务") return "完成", nil }) ts.Delay(2*time.Second, func(taskId string) (any, error) { fmt.Println("2秒后执行") return "延迟任务完成", nil }) ts.Every(10*time.Second, func(taskId string) (any, error) { fmt.Println("每10秒执行一次") return "ok", nil }) // 6. 暂停所有任务 time.Sleep(30 * time.Second) ts.PauseAll() fmt.Println("所有任务已暂停") // 7. 恢复任务 time.Sleep(10 * time.Second) ts.ResumeAll() fmt.Println("所有任务已恢复") // 8. 查询状态 fmt.Printf("当前任务: %v\n", ts.GetTaskList()) fmt.Printf("任务状态: %v\n", ts.GetTaskStatus()) // 9. 停止所有任务(退出时) // ts.StopAndClearAll() } ``` --- ## 注意事项 1. **V2 ID-based**:所有内部 map 使用 taskId 作为 key,name 仅用于显示 2. **并发安全**:使用读写锁保护内部状态 3. **Panic 恢复**:任务执行 panic 不会导致整个调度器崩溃,任务会自动重新调度 4. **定时器安全停止**:使用 `stopTimerSafely` 防止定时器泄漏 5. **计时器任务防重**:通过 `executed` 标记和双重检查锁防止重复执行 6. **Redis 超时**:默认 2 秒超时,可通过 `WithRedisTimeout` 自定义 7. **过期任务处理**:指定过去时间的任务会被自动忽略 8. **上下文取消**:`StopAndClearAll` 会取消全局上下文,正在执行的任务会立即停止 9. **回收站**:Remove 操作将任务移入回收站而非直接删除,可通过 Restore 恢复 --- ## 高级用法 ### 自定义配置获取器 除了 Redis,还可以通过实现 `IntervalConfigGetter` 接口支持其他配置源: ```go type MyConfigGetter struct{} func (g *MyConfigGetter) GetInterval(ctx context.Context, configKey string) (int, bool, error) { // 从数据库、远程API等获取配置 interval := getIntervalFromDB(configKey) if interval <= 0 { return 0, true, nil // 需要使用默认值 } return interval, false, nil } ts.SetIntervalGetter(&MyConfigGetter{}) ``` ### 任务跳过示例 通过 `OnStartBefore` 实现条件跳过: ```go type ConditionalTask struct{} func (t *ConditionalTask) Name() string { return "conditional_task" } func (t *ConditionalTask) ConfigKey() string { return "" } func (t *ConditionalTask) DefaultInterval() int { return 60 } func (t *ConditionalTask) Run() (any, error) { fmt.Println("任务执行") return nil, nil } func (t *ConditionalTask) OnStartBefore(taskID, taskName string) (skipTask bool, err error) { // 仅在业务高峰时段跳过 if isPeakHours() { fmt.Println("高峰期跳过本次执行") return true, nil } return false, nil } ``` --- ## API 变更记录 ### V2 重要变更 | 旧版 (name-based) | 新版 (id-based) | |-------------------|------------------| | `ts.Pause("taskName")` | `ts.Pause(taskId)` | | `ts.Resume("taskName")` | `ts.Resume(taskId)` | | `ts.StopTask("taskName")` | `ts.Stop(taskId)` | | `ts.RemoveTask("taskName")` | `ts.Remove(taskId)` | | `ts.CancelTimer("taskName")` | `ts.CancelTimer(taskId)` | | `ts.Pause()` | `ts.PauseAll()` | | `ts.Resume()` | `ts.ResumeAll()` | | `GetTaskList()` 返回 `[]string` (names) | `GetTaskList()` 返回 `[]string` (ids) | | `GetTasksPausedList()` 返回 names | `GetTasksPausedList()` 返回 ids | | `GetTaskStatus("taskName")` | `GetTaskStatus(taskId)` | ### 感知接口签名变更 | 接口方法 | 旧签名 | 新签名 | |---------|--------|--------| | `OnPauseAfter` | `(taskName string)` | `(taskID, taskName string)` | | `OnResumeAfter` | `(taskName string)` | `(taskID, taskName string)` | | `OnStopAfter` | `(taskName string)` | `(taskID, taskName string)` | | `OnStartBefore` | `(taskName string)` | `(taskID, taskName string)` | | `OnStartAfter` | `(taskName string, result any, err error)` | `(taskID, taskName string, result any, taskRunErr error)` |