scheduler

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 18 Imported by: 0

README

scheduler

中文 | English

基于层级时间轮的 Go 定时任务调度器:支持任务持久化、可插拔自定义触发器、全局并发控制与运行统计。

A cron-style Go job scheduler built on a hierarchical timing wheel, with optional database persistence, pluggable custom triggers, global concurrency control and runtime statistics.


中文说明

特性
  • 三种内置触发器:date(单次)、interval(固定间隔)、cron(cron 表达式,支持 5~7 字段与 L/W/# 扩展)
  • 自定义触发器:实现 Trigger 接口即可注册任意多种类型,或直接注入实例免注册
  • 存储模式可选:内存模式(重启丢失)或落库模式(gorm,重启自动恢复)
  • 并发控制:单任务级 LimitedAliveCount + 全局 MaxConcurrency,满载可排队或跳过
  • 运行统计:待执行 / 正在执行 / 已暂停任务数,并发上限调整即时生效
  • 添加期校验:函数名、参数个数与类型、触发器配置在 AddTask 阶段即校验,非法任务不落库
项目架构
scheduler/
├── schedule.go        # 调度器门面:任务增删改查、存储模式、事件循环、持久化
├── trigger.go         # Trigger 接口、内置三种触发器、自定义类型注册表
├── limiter.go         # 全局并发闸门(可动态调整,广播唤醒等待者)
├── timingwheel.go     # 层级时间轮(移植自 nsq 的实现思路)
├── bucket.go          # 时间轮槽位与 Timer
├── delayqueue.go      # 最小堆 + 延迟队列(移植自 nsq)
├── args_type.go       # Args(JSON 序列化)与 SafeMapOfInt 计数表
├── utils.go           # 时间截断、WaitGroup 封装等小工具
└── cronexpr*.go       # cron 表达式解析与计算(移植自 gorhill/cronexpr,MIT)
flowchart LR
    U["使用方<br/>AddTask / RemoveTask / StopTask / Stats"]

    subgraph S["Scheduler"]
        direction TB
        V["addOrReset<br/>函数名 / 参数 / 触发器校验"]
        TW["层级时间轮<br/>TimingWheel + DelayQueue"]
        G["并发闸门<br/>Limiter"]
        F["callFunction<br/>反射调用任务函数"]
        E["事件循环<br/>handleAction"]
    end

    DB[("periodic_task 表 · gorm")]

    U --> V
    V -- "新增 / 更新 / 恢复" --> DB
    V -- "挂载定时器" --> TW
    TW -- "到期" --> G -- "有名额" --> F
    F -- "run / next / finish 事件" --> E
    E -- "落库" --> DB

执行流程

  1. AddTask:函数符号名校验 → 参数个数与类型校验 → 触发器 Build 校验 → 落库 → 挂载到时间轮
  2. 时间轮到期:先按触发器计算并挂载下一次执行(调度不因执行阻塞而漂移),再申请并发名额
  3. callFunction:申请全局名额 → 同步维护运行计数 → 反射调用 → defer 统一释放名额、回收计数、按需上报结束事件
  4. 生命周期事件(run / next_run / finish)由独立事件循环统一落库,失败仅记日志不影响执行
  5. Stop:关闭退出通道 → 时间轮停止 → 等待内部 goroutine 退出(不等待正在执行的任务)

关键设计

设计点 说明
触发器 Type / Next / Build 三方法接口;落库存类型名 + 配置串,重启按注册的原型重建
存储模式 启动时经 Options.Storage 显式选定;接口字段不能作为数据库列,故持久化字符串
函数注册 任务落库的是 包路径.类型名.方法名,重启后据此在反射注册表中找回函数
并发统计 运行计数在执行路径上同步维护(事件循环是异步的),保证统计与限流判断准确
死代码防护 单次任务执行完毕与循环任务越过结束时间时都会打停止标记,不污染待执行统计
快速开始
go get github.com/angYuHao/scheduler
go build ./...

依赖:gorm.io/gorm、github.com/sirupsen/logrus、github.com/spf13/cast、github.com/glebarez/sqlite(仅测试)。

选择存储模式

启动时通过 Options.Storage 显式选择:内存模式(不落库,重启丢失)或落库模式(写入 periodic_task 表,重启可恢复)。

// 内存模式:任务不落库,即使传入 DB 也会被忽略
s, err := scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:   scheduler.StorageMemory,
	DB:        nil,         // 内存模式下无需 DB
	Tick:      time.Second, // 为 0 时默认 1s
	WheelSize: 24,          // 为 0 时默认 24
})

// 落库模式:必须提供 DB
s, err = scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:   scheduler.StorageDB,
	DB:        db, // gorm 连接,为 nil 时返回错误
	Tick:      time.Second,
	WheelSize: 24,
})
s.Start()
fmt.Println(s.Mode()) // memory / db

也可继续用旧签名(按 db 是否为 nil 隐式选择,行为不变):

// db:gorm 的数据库连接,为 nil 时为内存模式,重启后任务数据将清空
// tick:时间轮切片精度,一般设置为 time.Second
// wheelSize:时间轮槽位数
scheduler.NewScheduler(db *gorm.DB, tick time.Duration, wheelSize int64)
// 例:scheduler.NewScheduler(nil, time.Second, 24)
启动与停止
s.Start() // 幂等;必须先 Start 再 AddTask
defer s.Stop()

注意:Start 之前调用 AddTask 会直接返回错误(内部事件通道此时没有消费者)。

注册函数方法

将结构体方法注册到调度器,任务落库的是函数名字符串,重启后据此找回函数。

type T struct{}

func (t T) A()                              {}
func (t T) B(b string, i int)               {}
func (t T) C(b string, i int, c interface{}) {}

// 注册 T 下所有方法
scheduler.AddTaskFunction(&T{})

Function 必须用「值接收者 + 方法表达式」,这是硬性约束。注册 key 由 包路径 + 类型名 + 方法名 拼接而成,而任务的 Function 存的是 runtime.FuncForPC 拿到的符号名,两套命名体系在下列情况会分叉(分叉的写法在 AddTask 阶段即报错,不会静默失败):

写法 符号名 结果
T.A(值接收者方法表达式) pkg.T.A 正常
Outer.A(嵌入提升后) pkg.Outer.A 正常
tk.A(绑定方法值) pkg.T.A-fm AddTask 报错
(*T).A(指针接收者) pkg.(*T).A AddTask 报错
(&Outer{}).A(嵌入 + 绑定方法值) pkg.Inner.A-fm AddTask 报错
匿名函数 pkg.main.func1 AddTask 报错
type Inner struct{}

func (Inner) Run() {} // 值接收者

type Outer struct{ Inner }

func (Outer) Own() {} // 自己的方法

s.AddTaskFunction(&Outer{})
s.AddTask("a", scheduler.PeriodicTask{Function: Outer.Run})    // OK:嵌入提升,方法表达式
s.AddTask("b", scheduler.PeriodicTask{Function: Outer.Own})    // OK
s.AddTask("c", scheduler.PeriodicTask{Function: (&Outer{}).Run}) // 报错:绑定方法值

由于不能绑定实例,同一类型的所有任务共享同一份配置,差异化参数请通过 Args 传递;函数名变更会导致已落库任务在重启后找不到函数,务必保持稳定。

参数校验:函数已注册时,Args 的个数与类型在 AddTask 阶段即校验(个数不符、类型不可赋值、nil 赋给不可空形参均报错且不落库);执行期还会兜底复检一次(数据库恢复的 Args 经 JSON 反序列化后类型可能变化)。基础类型形参按 cast 语义宽松转换(数字/字符串互转、nil 转零值),其余类型要求 AssignableTo 形参。可变参数函数不支持,超出固定形参个数即报错。

并发控制与运行统计
s, err := scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:           scheduler.StorageMemory,
	Tick:              time.Second,
	MaxConcurrency:    10,                          // 全局同时执行上限,<=0 不限
	ConcurrencyPolicy: scheduler.ConcurrencyBlock, // 满载时排队等待(默认)
})
if err != nil {
	panic(err)
}
s.Start()
defer s.Stop()

// 运行统计
st := s.Stats()
// st.Total          内存中任务总数(含已暂停)
// st.Pending        待执行任务数(已启用、未暂停)
// st.Stopped        已暂停任务数
// st.Running        正在执行的任务数(按任务去重)
// st.Instances      正在执行的实例总数(含同一任务多次并发)
// st.MaxConcurrency 当前并发上限
// st.Skipped        因并发满被跳过的累计次数

fmt.Println(s.PendingCount()) // 待执行任务数
fmt.Println(s.ActiveCount())  // 正在执行的任务数

// 运行时动态调整上限,立即生效(缩限不会中断正在执行的任务)
s.SetMaxConcurrency(20)
fmt.Println(s.MaxConcurrency())

两种满载策略:

策略 行为 适用场景
ConcurrencyBlock(默认) 排队等待空闲名额,不丢任务 任务不可丢,如订单处理、报表生成
ConcurrencySkip 直接跳过本次执行,计入 Stats().Skipped 允许丢弃的轮询类任务

PeriodicTask.LimitedAliveCount 仍是单任务级的并发上限,MaxConcurrency 是全局上限,两者同时生效,取更严格者。

自定义触发器

内置三种触发器:date(单次)、interval(固定间隔)、cron(cron 表达式)。可注册任意多个自定义类型:实现 Trigger 接口(Type/Next/Build)+ RegisterTrigger 注册零值原型,之后 PeriodicTask.TriggerType 填注册名、TriggerConfig 填配置串。

也可跳过注册,直接注入触发器实例(PeriodicTask.Trigger),类型名自动取实例的 Type()。

// 自定义触发器:Type 返回类型名,Build 由任务配置产出真实例
type stepTrigger struct{ Step time.Duration }

func (t *stepTrigger) Type() string                  { return "step" }
func (t *stepTrigger) Next(prev time.Time) time.Time { return prev.Add(t.Step) }
func (t *stepTrigger) Build(p scheduler.PeriodicTask) (scheduler.Trigger, error) {
	d, err := time.ParseDuration(p.TriggerConfig)
	if err != nil {
		return nil, err
	}
	return &stepTrigger{Step: d}, nil
}

// 注册(可注册多个不同类型)
scheduler.RegisterTrigger("step", &stepTrigger{})

// 方式一:按类型名 + 配置串
s.AddTask("by-name", scheduler.PeriodicTask{
	Function:      T.A,
	TriggerType:   "step",
	TriggerConfig: "30s",
})

// 方式二:直接注入实例(免注册)
s.AddTask("by-instance", scheduler.PeriodicTask{
	Function: T.A,
	Trigger:  &stepTrigger{Step: 30 * time.Second},
})

持久化(启用数据库时):接口实例不能作为数据库列,故落库的是 trigger 列(类型名)与 trigger_config 列(配置串),重启后按类型名查找原型自动重建;未注册的类型名在 AddTask 阶段即报错,不会落库。

任务支持的功能
type T struct{}

func (t T) A()                              {}
func (t T) B(b string, i int)               {}
func (t T) C(b string, i int, c interface{}) {}

func main() {
	s := scheduler.NewScheduler(nil, time.Second, 24)
	s.AddTaskFunction(&T{})
	s.Start()
	defer s.Stop()
	// 创建 date 任务,该任务只执行一次
	err := s.AddTask("test1", scheduler.PeriodicTask{
		Name:              "测试任务1",
		Function:          T.A,
		Args:              scheduler.Args{},
		StartTime:         time.Now(),
		TriggerType:       scheduler.Date,
		LimitedAliveCount: 1,
	})
	if err != nil {
		fmt.Println(err)
	}
	// 创建 interval 任务,该任务循环执行
	err = s.AddTask("test2", scheduler.PeriodicTask{
		Name:              "测试任务2",
		Function:          T.B,
		Args:              scheduler.Args{"test2", 1},
		StartTime:         time.Now(),
		TriggerType:       scheduler.Interval,
		Interval:          "5s",
		LimitedAliveCount: 1,
	})
	if err != nil {
		fmt.Println(err)
	}
	// 创建 cron 任务,该任务为计划任务
	err = s.AddTask("test3", scheduler.PeriodicTask{
		Name:              "测试任务3",
		Function:          T.C,
		Args:              scheduler.Args{"test2", 1, "测试任务3"},
		StartTime:         time.Now(),
		TriggerType:       scheduler.Cron,
		Cron:              "*/5 * * * * * *",
		LimitedAliveCount: 1,
	})
	if err != nil {
		fmt.Println(err)
	}
	// 更新任务 test1
	err = s.ResetTask("test1", scheduler.PeriodicTask{
		Name:              "测试任务2",
		Function:          T.B,
		Args:              scheduler.Args{"test2", 1},
		StartTime:         time.Now(),
		TriggerType:       scheduler.Interval,
		Interval:          "5s",
		LimitedAliveCount: 1,
	})
	if err != nil {
		fmt.Println(err)
	}
	// 暂停任务 test1
	err = s.StopTask("test1")
	if err != nil {
		fmt.Println(err)
	}
	// 继续任务 test1
	err = s.ContinueTask("test1")
	if err != nil {
		fmt.Println(err)
	}
	// 立刻执行任务 test1
	err = s.StartTaskNow("test1")
	if err != nil {
		fmt.Println(err)
	}
	for {
		time.Sleep(time.Hour)
	}
}
测试
gofmt -l .                            # 格式检查
go vet ./...                          # 静态检查
go test -short -count=1 -race ./...   # 回归测试(-short 跳过长耗时演示用例)

注释约定:所有导出 API 与关键逻辑均为中英双语;cronexpr*.go 为第三方实现,仅保留文件级来源说明。


English

A Go job scheduler built on a hierarchical timing wheel, with optional database persistence, pluggable custom triggers, global concurrency control and runtime statistics.

Features
  • Three built-in triggers: date (one-shot), interval (fixed interval), cron (5~7 fields, with L/W/# extensions)
  • Custom triggers: implement the Trigger interface and register as many types as you like, or inject an instance directly to skip registration
  • Selectable storage: in-memory (lost on restart) or gorm-backed (auto-restored)
  • Concurrency control: per-job LimitedAliveCount plus a global MaxConcurrency; overflow can queue or skip
  • Runtime stats: pending / running / paused job counts; the concurrency limit can be resized at runtime
  • Fail-fast validation: function names, argument arity/types and trigger configuration are validated in AddTask; invalid jobs never reach the database
Architecture
File Responsibility
schedule.go Scheduler facade: job CRUD, storage modes, event loop, persistence
trigger.go Trigger interface, three built-ins, custom type registry
limiter.go Global concurrency gate (resizable, broadcasts to wake waiters)
timingwheel.go / bucket.go / delayqueue.go Hierarchical timing wheel, buckets, delay queue (ported from nsq)
args_type.go Args (JSON serialization) and SafeMapOfInt counters
utils.go Small helpers: time truncation, WaitGroup wrapper
cronexpr*.go Cron expression parsing/evaluation (ported from gorhill/cronexpr, MIT)
flowchart LR
    U["Caller<br/>AddTask / RemoveTask / StopTask / Stats"]

    subgraph S["Scheduler"]
        direction TB
        V["addOrReset<br/>validate function / args / trigger"]
        TW["Hierarchical timing wheel<br/>TimingWheel + DelayQueue"]
        G["Concurrency gate<br/>Limiter"]
        F["callFunction<br/>reflective invocation"]
        E["Event loop<br/>handleAction"]
    end

    DB[("periodic_task table · gorm")]

    U --> V
    V -- "insert / update / restore" --> DB
    V -- "mount timer" --> TW
    TW -- "due" --> G -- "slot granted" --> F
    F -- "run / next / finish events" --> E
    E -- "persist" --> DB

Execution flow

  1. AddTask: validate the function symbol name → validate argument arity/types → build the trigger → persist → mount on the timing wheel
  2. When the wheel fires, the next occurrence is scheduled first (so a blocked execution never drifts the schedule), then a concurrency slot is requested
  3. callFunction: acquire a global slot → maintain the running counter synchronously → invoke reflectively → the deferred cleanup releases the slot, recovers the counter and reports the finish event when needed
  4. Lifecycle events (run / next_run / finish) are persisted by a dedicated event loop; persistence failures are logged only and never affect execution
  5. Stop: close the exit channel → stop the wheel → wait for internal goroutines (running jobs are not awaited)

Key design decisions

Decision Rationale
Trigger interface Type / Next / Build; only the type name + config string are persisted, and the registered prototype rebuilds the instance on restart
Storage mode Chosen explicitly via Options.Storage at startup; interface fields cannot be database columns, so strings are persisted instead
Function registry Jobs persist pkgpath.Type.Method and the reflective registry resolves it back after restart
Concurrency stats Running counters are updated synchronously on the execution path (the event loop is async) so stats and limiting stay accurate
Stale-state hygiene One-shot jobs and repeating jobs that pass their end time are flagged as stopped so they never pollute the pending count
Quick start
go get github.com/angYuHao/scheduler
go build ./...

Dependencies: gorm.io/gorm, github.com/sirupsen/logrus, github.com/spf13/cast, github.com/glebarez/sqlite (tests only).

Choosing a storage mode
// Memory mode: nothing is persisted; a provided DB is ignored
s, err := scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:   scheduler.StorageMemory,
	DB:        nil,         // not needed in memory mode
	Tick:      time.Second, // 0 means the default 1s
	WheelSize: 24,          // 0 means the default 24
})

// DB mode: a gorm connection is required
s, err = scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:   scheduler.StorageDB,
	DB:        db, // nil returns an error
	Tick:      time.Second,
	WheelSize: 24,
})
s.Start()
fmt.Println(s.Mode()) // memory / db

The legacy constructor still works (storage mode inferred from db):

// db: gorm connection; nil means memory mode where jobs are lost on restart
// tick: wheel precision, usually time.Second
// wheelSize: number of wheel buckets
scheduler.NewScheduler(db *gorm.DB, tick time.Duration, wheelSize int64)
// e.g. scheduler.NewScheduler(nil, time.Second, 24)
Start and stop
s.Start() // idempotent; call Start before AddTask
defer s.Stop()

Calling AddTask before Start returns an error immediately (the internal event channel has no consumer yet).

Registering functions

Register struct methods so that jobs can invoke them by name; only the name is persisted and resolved again after a restart.

type T struct{}

func (t T) A()                              {}
func (t T) B(b string, i int)               {}
func (t T) C(b string, i int, c interface{}) {}

// Register all methods of T
scheduler.AddTaskFunction(&T{})

Function must be a value-receiver method expression — this is a hard requirement. Registry keys are built from pkgpath + type + method, while jobs store the symbol name reported by runtime.FuncForPC; the two naming schemes diverge in the cases below (divergent forms are rejected in AddTask instead of failing silently):

Form Symbol name Result
T.A (value receiver, method expression) pkg.T.A OK
Outer.A (promoted through embedding) pkg.Outer.A OK
tk.A (bound method value) pkg.T.A-fm rejected
(*T).A (pointer receiver) pkg.(*T).A rejected
(&Outer{}).A (embedded + bound value) pkg.Inner.A-fm rejected
anonymous function pkg.main.func1 rejected
type Inner struct{}

func (Inner) Run() {} // value receiver

type Outer struct{ Inner }

func (Outer) Own() {} // Outer's own method

s.AddTaskFunction(&Outer{})
s.AddTask("a", scheduler.PeriodicTask{Function: Outer.Run})      // OK
s.AddTask("b", scheduler.PeriodicTask{Function: Outer.Own})      // OK
s.AddTask("c", scheduler.PeriodicTask{Function: (&Outer{}).Run}) // rejected

Because instances cannot be bound, all jobs of one type share the same configuration; pass per-job differences through Args. Renaming functions breaks persisted jobs after a restart, so keep names stable.

Argument validation: when the function is registered, Args arity and types are validated in AddTask (wrong count, non-assignable types and nil passed to non-nilable parameters are rejected without persisting); the runtime re-checks as a safety net because Args restored from JSON may change type. Basic-type parameters convert leniently via cast (numbers/strings interchange, nil becomes the zero value); everything else must be AssignableTo the parameter. Variadic functions are not supported.

Concurrency control and stats
s, err := scheduler.NewSchedulerWithOptions(scheduler.Options{
	Storage:           scheduler.StorageMemory,
	Tick:              time.Second,
	MaxConcurrency:    10,                          // global limit, <=0 unlimited
	ConcurrencyPolicy: scheduler.ConcurrencyBlock, // queue when full (default)
})
if err != nil {
	panic(err)
}
s.Start()
defer s.Stop()

st := s.Stats()
// st.Total          jobs in memory (including paused)
// st.Pending        enabled, non-paused jobs
// st.Stopped        paused jobs
// st.Running        running jobs, deduplicated by key
// st.Instances      running executions, including concurrent runs of one job
// st.MaxConcurrency current global limit
// st.Skipped        runs dropped because the limit was reached

fmt.Println(s.PendingCount())
fmt.Println(s.ActiveCount())

// Resize at runtime; shrinking never interrupts running jobs
s.SetMaxConcurrency(20)
fmt.Println(s.MaxConcurrency())

Overflow policies:

Policy Behaviour Use case
ConcurrencyBlock (default) Queue until a slot is free; nothing is lost Jobs that must not be dropped
ConcurrencySkip Drop the run and count it in Stats().Skipped Polling jobs where skips are acceptable

PeriodicTask.LimitedAliveCount remains the per-job cap; MaxConcurrency is the global cap. Both apply, and the stricter one wins.

Custom triggers

Built-ins: date (one-shot), interval (fixed interval), cron. Register any number of custom types: implement Trigger (Type/Next/Build) and call RegisterTrigger with a zero-value prototype, then set PeriodicTask.TriggerType and TriggerConfig. Alternatively inject an instance directly via PeriodicTask.Trigger to skip registration.

type stepTrigger struct{ Step time.Duration }

func (t *stepTrigger) Type() string                  { return "step" }
func (t *stepTrigger) Next(prev time.Time) time.Time { return prev.Add(t.Step) }
func (t *stepTrigger) Build(p scheduler.PeriodicTask) (scheduler.Trigger, error) {
	d, err := time.ParseDuration(p.TriggerConfig)
	if err != nil {
		return nil, err
	}
	return &stepTrigger{Step: d}, nil
}

scheduler.RegisterTrigger("step", &stepTrigger{})

// By name + config string
s.AddTask("by-name", scheduler.PeriodicTask{
	Function:      T.A,
	TriggerType:   "step",
	TriggerConfig: "30s",
})

// Or inject an instance directly
s.AddTask("by-instance", scheduler.PeriodicTask{
	Function: T.A,
	Trigger:  &stepTrigger{Step: 30 * time.Second},
})

Persistence (DB mode): trigger instances cannot be database columns, so the trigger column (type name) and trigger_config column (config string) are stored; on restart the prototype is looked up by name and rebuilt. Unknown type names are rejected in AddTask and never persisted.

Job lifecycle example
type T struct{}

func (t T) A()                              {}
func (t T) B(b string, i int)               {}
func (t T) C(b string, i int, c interface{}) {}

func main() {
	s := scheduler.NewScheduler(nil, time.Second, 24)
	s.AddTaskFunction(&T{})
	s.Start()
	defer s.Stop()

	// One-shot date job
	_ = s.AddTask("test1", scheduler.PeriodicTask{
		Name: "job1", Function: T.A, Args: scheduler.Args{},
		StartTime: time.Now(), TriggerType: scheduler.Date, LimitedAliveCount: 1,
	})
	// Repeating interval job
	_ = s.AddTask("test2", scheduler.PeriodicTask{
		Name: "job2", Function: T.B, Args: scheduler.Args{"test2", 1},
		StartTime: time.Now(), TriggerType: scheduler.Interval,
		Interval: "5s", LimitedAliveCount: 1,
	})
	// Cron job
	_ = s.AddTask("test3", scheduler.PeriodicTask{
		Name: "job3", Function: T.C, Args: scheduler.Args{"test2", 1, "job3"},
		StartTime: time.Now(), TriggerType: scheduler.Cron,
		Cron: "*/5 * * * * * *", LimitedAliveCount: 1,
	})
	// Replace test1
	_ = s.ResetTask("test1", scheduler.PeriodicTask{
		Name: "job2", Function: T.B, Args: scheduler.Args{"test2", 1},
		StartTime: time.Now(), TriggerType: scheduler.Interval,
		Interval: "5s", LimitedAliveCount: 1,
	})
	// Pause / resume / run now
	_ = s.StopTask("test1")
	_ = s.ContinueTask("test1")
	_ = s.StartTaskNow("test1")

	for {
		time.Sleep(time.Hour)
	}
}
Testing
gofmt -l .                            # formatting
go vet ./...                          # static analysis
go test -short -count=1 -race ./...   # regression tests (-short skips long demos)

Comment convention: every exported API and key code path is documented in both Chinese and English; cronexpr*.go is third-party code and carries file-level provenance comments only.

Documentation

Overview

文件:cronexpr.go Purpose: Cron expression public API: Parse / MustParse and generic helpers. 来源:移植自第三方开源项目 gorhill/cronexpr(MIT License),仅做最小化适配:

  • 去除对 robinlindsley/go-pkgs 的依赖,改为标准库实现;
  • 保持表达式语义不变,支持 5~7 个字段(秒 分 时 日 月 周 [年])。

说明:cron 相关文件为第三方实现,不做逐行注释,仅补充文件级说明。 Note: cron files are vendored from a third-party project; only file-level documentation is added instead of line-by-line comments.

文件:cronexpr_next.go Purpose: Compute the next activation time of a parsed Expression (year -> month -> day -> hour -> minute -> second cascade). 来源:移植自 gorhill/cronexpr(MIT License),仅做最小化适配,未匹配语义。 Source: adapted from gorhill/cronexpr (MIT License) with minimal changes.

文件:cronexpr_parse.go Purpose: Cron expression parsing: field splitting, range/step/last/weekday expansion into the internal bit lists used by cronexpr_next.go. 来源:移植自 gorhill/cronexpr(MIT License),仅做最小化适配,未修改解析语义。 Source: adapted from gorhill/cronexpr (MIT License) with minimal changes.

Package scheduler 是一个基于层级时间轮的定时任务调度器,支持任务持久化与自定义触发器。 Package scheduler is a cron-style job scheduler built on a hierarchical timing wheel, with optional database persistence and pluggable custom triggers.

Index

Constants

View Source
const (
	Date     = "date"     // 单次任务 / one-shot job
	Interval = "interval" // 固定间隔 / fixed interval
	Cron     = "cron"     // cron 表达式 / cron expression
)

内置触发器类型名 / Built-in trigger type names.

Variables

This section is empty.

Functions

func RegisterTrigger

func RegisterTrigger(name string, proto Trigger) error

RegisterTrigger 注册自定义触发器类型,PeriodicTask.TriggerType 填同名即可使用; 服务重启后从数据库恢复任务时按此名查找原型并重建。 proto 只需零值原型(其 Build 方法负责产出真实例),重复注册返回错误。

RegisterTrigger registers a custom trigger type. proto only needs to be a zero-value prototype; its Build method produces the real instance.

Types

type Args

type Args []interface{}

Args 为任务调用参数,以 JSON 形式存入数据库。 Args holds the job arguments and is persisted as JSON.

func (*Args) Scan

func (a *Args) Scan(val interface{}) error

Scan 实现 sql.Scanner,从数据库读取 JSON 参数。 Scan implements sql.Scanner to read JSON arguments.

func (Args) Value

func (a Args) Value() (driver.Value, error)

Value 实现 driver.Valuer,将参数序列化为 JSON。 Value implements driver.Valuer to serialize arguments as JSON.

type ConcurrencyPolicy

type ConcurrencyPolicy int

ConcurrencyPolicy 决定达到全局并发上限时的行为。 ConcurrencyPolicy defines what happens when the concurrency limit is reached.

const (
	// ConcurrencyBlock 等待空闲名额(默认):任务排队,不会丢失。
	// ConcurrencyBlock waits for a free slot (default); jobs queue instead of being lost.
	ConcurrencyBlock ConcurrencyPolicy = iota
	// ConcurrencySkip 直接跳过本次执行:适合允许丢弃的轮询类任务。
	// ConcurrencySkip drops the run; suitable for polling jobs that may be skipped.
	ConcurrencySkip
)

type CronTrigger

type CronTrigger struct {
	Cron      string
	StartTime time.Time
	EndTime   time.Time // 零值表示不限 / zero means no end
}

CronTrigger 定义计划任务:按 cron 表达式循环执行。 CronTrigger fires according to a cron expression.

func (*CronTrigger) Build

func (t *CronTrigger) Build(p PeriodicTask) (Trigger, error)

Build 校验 cron 表达式非空并构造实例。 Validates that the cron expression is present and builds an instance.

func (*CronTrigger) Next

func (t *CronTrigger) Next(prev time.Time) time.Time

Next 求出下一次匹配 cron 表达式的时间;表达式为空、越过 EndTime 或解析失败时返回零值。 Returns the next matching time, or the zero time on empty expression, passed EndTime or parse failure.

func (*CronTrigger) Type

func (t *CronTrigger) Type() string

Type 返回内置类型名。Returns the built-in type name.

type DateTrigger

type DateTrigger struct {
	StartTime time.Time
}

DateTrigger 定义单次任务:到期执行一次后任务终止。 DateTrigger fires once at StartTime (immediately if already past).

func (*DateTrigger) Build

func (t *DateTrigger) Build(p PeriodicTask) (Trigger, error)

Build 由任务的 StartTime 构造实例。Builds an instance from the job.

func (*DateTrigger) Next

func (t *DateTrigger) Next(prev time.Time) time.Time

Next 起始时间已过则立即执行,否则等待到起始时刻。 Fires immediately when StartTime is in the past.

func (*DateTrigger) Type

func (t *DateTrigger) Type() string

Type 返回内置类型名。Returns the built-in type name.

type DelayQueue

type DelayQueue struct {
	C chan interface{} // 到期元素输出通道 / channel delivering expired elements
	// contains filtered or unexported fields
}

DelayQueue 为无界阻塞延迟队列,元素到期后才能取出,队首是最早到期者。 DelayQueue is an unbounded blocking queue whose elements can only be taken once their delay has expired; the head expired furthest in the past.

func New

func New(size int) *DelayQueue

New 创建延迟队列,size 为堆初始容量。 Creates a delay queue with the given initial heap capacity.

func (*DelayQueue) Offer

func (dq *DelayQueue) Offer(elem interface{}, expiration int64)

Offer 按到期时间插入元素;若成为队首则唤醒 Poll。 Inserts an element by expiration time and wakes Poll when it becomes the head.

func (*DelayQueue) Poll

func (dq *DelayQueue) Poll(exitC chan struct{}, nowF func() int64)

Poll 循环等待元素到期并发送到 C;exitC 关闭时退出。 Loopingly waits for elements to expire and sends them to C; exits when exitC closes.

type Expression

type Expression struct {
	// contains filtered or unexported fields
}

Expression 表示一个已解析的 cron 时间表达式。 Expression represents a parsed cron time expression.

func MustParse

func MustParse(cronLine string) *Expression

MustParse returns a new Expression pointer. It expects a well-formed cron expression. If a malformed cron expression is supplied, it will `panic`. See <https://github.com/gorhill/cronexpr#implementation> for documentation about what is a well-formed cron expression from this library's point of view.

func Parse

func Parse(cronLine string) (*Expression, error)

Parse returns a new Expression pointer. An error is returned if a malformed cron expression is supplied. See <https://github.com/gorhill/cronexpr#implementation> for documentation about what is a well-formed cron expression from this library's point of view.

func (*Expression) Next

func (expr *Expression) Next(fromTime time.Time) time.Time

Next returns the closest time instant immediately following `fromTime` which matches the cron expression `expr`.

The `time.Location` of the returned time instant is the same as that of `fromTime`.

The zero value of time.Time is returned if no matching time instant exists or if a `fromTime` is itself a zero value.

func (*Expression) NextN

func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time

NextN returns a slice of `n` closest time instants immediately following `fromTime` which match the cron expression `expr`.

The time instants in the returned slice are in chronological ascending order. The `time.Location` of the returned time instants is the same as that of `fromTime`.

A slice with len between [0-`n`] is returned, that is, if not enough existing matching time instants exist, the number of returned entries will be less than `n`.

type IntervalTrigger

type IntervalTrigger struct {
	Interval  time.Duration
	StartTime time.Time
	EndTime   time.Time // 零值表示不限 / zero means no end
}

IntervalTrigger 定义定时任务:按固定间隔循环执行。 IntervalTrigger fires repeatedly with a fixed interval.

func (*IntervalTrigger) Build

func (t *IntervalTrigger) Build(p PeriodicTask) (Trigger, error)

Build 解析 Interval 字符串并构造实例;间隔非法时返回错误。 Parses the interval string; returns an error when it is not a positive duration.

func (*IntervalTrigger) Next

func (t *IntervalTrigger) Next(prev time.Time) time.Time

Next 越过 EndTime 返回零值(任务终止),否则按间隔递推。 Returns the zero time once EndTime has passed, otherwise prev+Interval.

func (*IntervalTrigger) Type

func (t *IntervalTrigger) Type() string

Type 返回内置类型名。Returns the built-in type name.

type Options

type Options struct {
	Storage           StorageMode
	DB                *gorm.DB          // 仅 DB 模式使用 / used in DB mode only
	Tick              time.Duration     // 0 表示默认 1s / 0 means 1s
	WheelSize         int64             // 0 表示默认 24 / 0 means 24
	MaxConcurrency    int               // 全局并发上限,<=0 不限 / global concurrency limit, <=0 unlimited
	ConcurrencyPolicy ConcurrencyPolicy // 并发满时的策略 / policy when the limit is reached
}

Options 为调度器配置,Storage 显式决定任务是否落库: StorageDB 时必须提供 DB;StorageMemory 时忽略 DB,任务一律不落库。 MaxConcurrency 为全局同时执行任务数上限,<=0 表示不限制。 ConcurrencyPolicy 决定达到上限时的行为(等待或跳过)。 Options configures the scheduler; Storage explicitly selects memory or DB mode.

type PeriodicTask

type PeriodicTask struct {
	Name              string      // 任务名称 / job name
	Function          interface{} // 待调用的函数或方法 / function to invoke
	Args              Args        // 调用参数 / call arguments
	StartTime         time.Time   // 起始时间 / start time
	EndTime           time.Time   // 结束时间,零值表示不限 / end time, zero means unlimited
	Cron              string      // cron 表达式(TriggerType=cron) / cron expression
	Interval          string      // 间隔字符串,如 "5s"(TriggerType=interval) / interval string
	LimitedAliveCount int64       // 并发存活上限,<=0 不限 / max concurrent runs, <=0 means unlimited
	TriggerType       string      // 触发器类型名 / trigger type name
	TriggerConfig     string      // 触发器配置串 / trigger configuration string
	Trigger           Trigger     `gorm:"-"` // 可选的触发器实例(优先于 TriggerType)/ optional trigger instance
}

PeriodicTask 描述一个计划任务。 TriggerType 取内置类型(date/interval/cron)或 RegisterTrigger 注册的自定义类型名; TriggerConfig 为自定义类型的配置串,由对应 Trigger.Build 解析。 也可直接注入 Trigger 实例(内存模式免注册),此时类型名自动取其 Type()。 启用数据库模式时,落库的是类型名与配置串,服务重启后可自动恢复。

PeriodicTask describes a scheduled job. TriggerType is either a built-in type (date/interval/cron) or a custom type registered via RegisterTrigger, and TriggerConfig is the configuration string passed to Trigger.Build. A Trigger instance may be injected directly to skip registration.

type SafeMapOfInt

type SafeMapOfInt struct {
	sync.RWMutex
	M map[interface{}]int64
}

SafeMapOfInt 是并发安全的计数表(键为任务 key,值为当前存活执行数)。 SafeMapOfInt is a concurrency-safe counter map keyed by job key.

func (*SafeMapOfInt) Add

func (sm *SafeMapOfInt) Add(s interface{})

Add 将计数加一,读改写原子完成。 Add increments the counter atomically.

func (*SafeMapOfInt) Delete

func (sm *SafeMapOfInt) Delete(s interface{})

Delete 将计数减一,减到 0 时删除该键。 Delete decrements the counter, removing the key when it reaches zero.

func (*SafeMapOfInt) GetValue

func (sm *SafeMapOfInt) GetValue(s interface{}) int64

GetValue 返回计数,不存在时返回 0。 GetValue returns the counter, or 0 when the key is absent.

func (*SafeMapOfInt) Len

func (sm *SafeMapOfInt) Len() int

Len 返回计数非零的键数量,即当前正在执行的任务数。 Len returns the number of keys with a positive counter, i.e. running jobs.

func (*SafeMapOfInt) Total

func (sm *SafeMapOfInt) Total() int64

Total 返回所有键的计数之和,即当前正在执行的实例总数。 Total returns the sum of all counters, i.e. the number of running executions.

type Scheduler

type Scheduler struct {
	Any map[string]interface{} // 函数名 -> 反射方法 / function name to reflect.Value
	DB  *gorm.DB

	RunningCount *SafeMapOfInt
	// contains filtered or unexported fields
}

Scheduler 为调度器门面。 存储模式在启动时选定:内存模式任务不落库,DB 模式可在重启后恢复。 Scheduler is the scheduler facade. The storage mode is fixed at startup.

func NewScheduler

func NewScheduler(db *gorm.DB, tick time.Duration, wheelSize int64) *Scheduler

NewScheduler 创建调度器,按 DB 是否为 nil 隐式选择存储模式(等价于显式指定 Storage)。 db 为 nil 即内存模式;tick 为时间轮切片精度;wheelSize 为时间轮槽位数。 注意:添加任务前必须先调用 Start。 NewScheduler creates a scheduler, inferring the storage mode from db.

func NewSchedulerWithOptions

func NewSchedulerWithOptions(opt Options) (*Scheduler, error)

NewSchedulerWithOptions 按配置创建调度器,在启动时显式选择内存或落库模式。 Tick/WheelSize 为 0 时使用默认值。 NewSchedulerWithOptions creates a scheduler with an explicit storage mode.

func (*Scheduler) ActiveCount

func (s *Scheduler) ActiveCount() int

RunningCount 返回正在执行的任务数(按任务去重)。 注意:与字段 RunningCount 同名冲突,故命名为 ActiveCount。 RunningCount of distinct jobs currently running. Named ActiveCount to avoid clashing with the RunningCount field.

func (*Scheduler) AddTask

func (s *Scheduler) AddTask(key string, p PeriodicTask) error

AddTask 添加任务;触发器校验失败时不会落库。 AddTask adds a job. The job is not persisted when trigger validation fails.

func (*Scheduler) AddTaskFunction

func (s *Scheduler) AddTaskFunction(structs ...interface{})

AddTaskFunction 注册结构体的所有方法,使其可被任务按函数名调用。 参数必须为结构体指针。 AddTaskFunction registers all methods of the given struct pointers so that jobs can invoke them by function name.

func (*Scheduler) ContinueTask

func (s *Scheduler) ContinueTask(key string) error

ContinueTask 恢复已暂停的任务。 ContinueTask resumes a paused job.

func (*Scheduler) InitFromDB

func (s *Scheduler) InitFromDB()

InitFromDB 从数据库恢复所有未停止的任务到内存(仅 DB 模式有效)。 InitFromDB restores all non-stopped jobs from the database (DB mode only).

func (*Scheduler) MaxConcurrency

func (s *Scheduler) MaxConcurrency() int

MaxConcurrency 返回当前全局并发上限,<=0 表示不限制。 MaxConcurrency returns the current global concurrency limit; <=0 means unlimited.

func (*Scheduler) Mode

func (s *Scheduler) Mode() string

Mode 返回当前存储模式名称(memory / db)。 Mode returns the current storage mode name.

func (*Scheduler) PendingCount

func (s *Scheduler) PendingCount() int

PendingCount 返回待执行任务数(已启用、未暂停)。 PendingCount returns the number of enabled, non-paused jobs.

func (*Scheduler) RemoveTask

func (s *Scheduler) RemoveTask(key string) error

RemoveTask 删除任务(内存 + 数据库)。 RemoveTask removes a job from memory and the database.

func (*Scheduler) ResetTask

func (s *Scheduler) ResetTask(key string, p PeriodicTask) error

ResetTask 重置任务;任务不存在时等价于新增。 ResetTask updates a job, or adds it when it does not exist yet.

func (*Scheduler) SetMaxConcurrency

func (s *Scheduler) SetMaxConcurrency(n int)

SetMaxConcurrency 动态调整全局并发上限,<=0 表示不限制,立即生效。 缩限时已在执行的任务不会被中断,仅阻止新的执行。 SetMaxConcurrency resizes the global concurrency limit at runtime. Shrinking does not interrupt running jobs; it only blocks new ones.

func (*Scheduler) Start

func (s *Scheduler) Start()

Start 启动调度器(幂等):启动时间轮与事件消费循环,并从数据库恢复任务。 注意:必须先 Start 再添加任务。 Start starts the scheduler (idempotent). Add tasks only after Start.

func (*Scheduler) StartTaskNow

func (s *Scheduler) StartTaskNow(key string) error

StartTaskNow 立即执行一次任务,不改变原调度计划。 StartTaskNow runs a job immediately without changing its schedule.

func (*Scheduler) Stats

func (s *Scheduler) Stats() Stats

Stats 返回调度器运行统计快照。 Stats returns a snapshot of the scheduler counters.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop 停止调度器(幂等)。正在执行的任务不会被等待。 Stop stops the scheduler (idempotent). Running jobs are not awaited.

func (*Scheduler) StopTask

func (s *Scheduler) StopTask(key string) error

StopTask 暂停任务(内存 + 数据库)。 StopTask pauses a job.

type Stats

type Stats struct {
	Total          int   // 内存中的任务总数(含已暂停)/ total jobs in memory, including paused
	Pending        int   // 待执行任务数(已启用、未暂停)/ pending jobs, enabled and not paused
	Stopped        int   // 已暂停任务数 / paused jobs
	Running        int   // 正在执行的任务数(按任务去重)/ running jobs, deduplicated by key
	Instances      int64 // 正在执行的实例总数(含同一任务的多次并发)/ running executions
	MaxConcurrency int   // 全局并发上限,0 表示不限 / global limit, 0 means unlimited
	Skipped        int64 // 因并发满被跳过的执行次数 / runs dropped because the limit was reached
}

Stats 返回调度器当前的运行统计。 Stats returns a snapshot of the scheduler runtime counters.

type StorageMode

type StorageMode int

StorageMode 决定计划任务是否落库,启动时选定。 StorageMode decides whether jobs are persisted; chosen at startup.

const (
	StorageMemory StorageMode = iota // 内存模式:不落库 / memory mode: no persistence
	StorageDB                        // 落库模式:写入 periodic_task 表 / DB mode: persisted
)

func (StorageMode) String

func (m StorageMode) String() string

String 返回存储模式名称。Returns the mode name.

type Timer

type Timer struct {
	// contains filtered or unexported fields
}

Timer 表示一个待触发的任务,到期后执行其函数。 Timer is a single pending job executed when it expires.

type TimingWheel

type TimingWheel struct {
	// contains filtered or unexported fields
}

TimingWheel 是层级时间轮实现(移植自 nsq 的 timing wheel)。 只有当前层能容纳的到期时间留在本层,其余交给上层溢出轮, 因此可以低成本支持跨度远大于 wheelSize*tick 的调度。

TimingWheel is a hierarchical timing wheel (ported from nsq's timing wheel). Expirations beyond the current wheel are delegated to an overflow wheel.

func NewTimingWheel

func NewTimingWheel(tick time.Duration, wheelSize int64) *TimingWheel

NewTimingWheel 创建时间轮,tick 须不小于 1ms,wheelSize 须大于 0。 Creates a timing wheel; tick must be >= 1ms and wheelSize > 0.

type Trigger

type Trigger interface {
	Type() string                        // 类型名 / type name
	Next(time.Time) time.Time            // 下次执行时间,零值表示终止 / next fire time, zero to stop
	Build(PeriodicTask) (Trigger, error) // 由任务配置构造实例 / build an instance from the job
}

Trigger 定义触发器接口。 任务每次执行后调用 Next 计算下次执行时间,返回零值 time.Time{} 表示任务终止; Build 由 PeriodicTask 构造真实例;Type 返回类型名(内置名或 RegisterTrigger 注册名), 用于持久化与重启恢复。

Trigger decides when a job runs. Next returns the zero time.Time to stop. Build constructs a configured instance from a PeriodicTask, and Type returns the type name used for persistence and restart recovery.

func BuildTrigger

func BuildTrigger(p PeriodicTask) (Trigger, error)

BuildTrigger 构造任务的触发器实例: ① PeriodicTask.Trigger 已注入实例时,直接由其 Build 产出(内存模式免注册); ② 否则按 TriggerType 查内置/注册的类型原型,由原型 Build 产出(支持持久化恢复)。

BuildTrigger constructs the trigger of a job: an injected instance wins, otherwise the registered prototype for TriggerType builds it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址