参考wiki
https://en.wikipedia.org/wiki/Cron
https://github.com/robfig/cron.git
constantdelay.go #一个最简单的秒级别定时系统。与cron无关
constantdelay_test.go #测试
cron.go #Cron系统。管理一系列的cron定时任务(Schedule Job)
cron_test.go #测试
doc.go #说明文档
LICENSE #授权书
parser.go #解析器,解析cron格式字符串城一个具体的定时器(Schedule)
parser_test.go #测试
README.md #README
spec.go #单个定时器(Schedule)结构体。如何计算自己的下一次触发时间
spec_test.go #测试
doc.go
CRON Expression Format
A cron expression represents a set of times, using 6 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Predefined schedules
You may use one of several pre-defined schedules in place of a cron expression.
Entry | Description | Equivalent To
----- | ----------- | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 *
@monthly | Run once a month, midnight, first of month | 0 0 0 1 * *
@weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0
@daily (or @midnight) | Run once a day, midnight | 0 0 0 * * *
@hourly | Run once an hour, beginning of hour | 0 0 * * * *
可以看到这里并没有实现 L , W , #
这些特殊字符。 至于原因,下面将具体实现代码的时候会给出。
首先,让我试着使用EBNF来定义下cron 表达式(不是很严谨。。。)
<non_zero> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
<num> ::= <non_zero> | "0"
<number> ::= <non_zero> {<num>}
<normal_item> ::= "*" | <number> | <number> "-"<number> | <number> "/" <number>
<days_item> ::= <normal_item> | "?" | <number>"L" | <number>"W" | <number>"#"
<item> ::= <normal_item> | <days_item>;
<items> ::= {<item>","} <item> ;
<cron> ::= <items>" "<items>" "<items>" "<items>" "<items>" "<items>
至此, 如果我打算解析一个cron表达式, 应该遵循以下步骤 :
cron
利用空白拆解出独立的items
。items
利用,
拆解出item
。item
利用穷举法一一检测( 符合且仅符合下面的一条才能合法) : *
或者 ?
。 /
或者-
分解为俩数字 。L
或者W
结尾。cron表达式是用来表示一系列时间的,而时间是无法逃脱自己的区间的 , 分,秒 0 - 59 , 时 0 - 23 , 天/月 0 - 31 , 天/周 0 - 6 , 月0 - 11 。 这些本质上都是一个点集合,或者说是一个整数区间。 那么对于任意的整数区间 , 可以描述cron的如下部分规则。
* | ?
任意 , 对应区间上的所有点。 ( 额外注意 日/周 , 日 / 月 的相互干扰。)/
分割的两个数字 a , b, 区间上符合 a + n * b 的所有点 ( n >= 0 )。 -
分割的两个数字, 对应这两个数字决定的区间内的所有点。L | W
需要对于特定的时间特殊判断, 无法通用的对应到区间上的点。 至此, robfig/cron
为什么不支持 L | W
的原因已经明了了。去除这两条规则后, 其余的规则其实完全可以使用点的穷举来通用表示。 考虑到最大的区间也不过是60个点,那么使用一个uint64
的整数的每一位来表示一个点便很合适了。下面是robfig/cron
的表示方法 :
/* ------------------------------------------------------------ 第64位标记任意 , 用于 日/周 , 日 / 月 的相互干扰。 63 - 0 为 表示区间 [63 , 0] 的 每一个点。 ------------------------------------------------------------ 假设区间是 0 - 63 , 则有如下的例子 : 比如 0/3 的表示如下 : * / ? +---+--------------------------------------------------------+ | 0 | 1 0 0 1 0 0 1 ~~ ~~ 1 0 0 1 0 0 1 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 比如 2-5 的表示如下 : * / ? +---+--------------------------------------------------------+ | 0 | 0 0 0 0 ~ ~ ~~ ~ 0 0 0 1 1 1 1 0 0 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 比如 * 的表示如下 : * / ? +---+--------------------------------------------------------+ | 1 | 1 1 1 1 1 ~ ~ ~ 1 1 1 1 1 1 1 1 1 | +---+--------------------------------------------------------+ 63 ~ ~ ~~ 0 */
有一个规则后, 判断一个时间点是否符合规则其实就是对应位是否为1 。
给定一个时间后, 寻找下一个符合符合规则的时间也很简单 :
由于区间小,这个遍历的效率是很高的, 但是要注意, 不要一直查找下去, 最最悲观的情况大概是闰年的2月29 。 只要对未来的探究超过5年就意味着是个非法的表达式。
让我将上面的讲解一一的对应到具体的代码中 :
spec.go
// 每个时间字段是一个uint64
type SpecSchedule struct {
Second, Minute, Hour, Dom, Month, Dow uint64
}
//对64位 任意 的标识
const (
// Set the top bit if a star was included in the expression.
starBit = 1 << 63
)
spec.go
type bounds struct {
min, max uint
names map[string]uint
}
// The bounds for each field.
var (
seconds = bounds{0, 59, nil}
minutes = bounds{0, 59, nil}
hours = bounds{0, 23, nil}
dom = bounds{1, 31, nil}
months = bounds{1, 12, map[string]uint{
"jan": 1,
"feb": 2,
"mar": 3,
"apr": 4,
"may": 5,
"jun": 6,
"jul": 7,
"aug": 8,
"sep": 9,
"oct": 10,
"nov": 11,
"dec": 12,
}}
dow = bounds{0, 6, map[string]uint{
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
}}
)
SpecSchedule
parser.go
package cron
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
)
// 将字符串解析成为SpecSchedule 。 SpecSchedule符合Schedule接口
func Parse(spec string) (_ Schedule, err error) {
// 处理panic 。 不让成语crash。 而是当作error输出
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("%v", recovered)
}
}()
// 直接处理特殊的特殊的字符串,
if spec[0] == '@' {
return parseDescriptor(spec), nil
}
// cron利用空白拆解出独立的items。
fields := strings.Fields(spec)
if len(fields) != 5 && len(fields) != 6 {
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
}
// 添加缺省的 日/周
if len(fields) == 5 {
fields = append(fields, "*")
}
// 按照规则解析每个items
schedule := &SpecSchedule{
Second: getField(fields[0], seconds),
Minute: getField(fields[1], minutes),
Hour: getField(fields[2], hours),
Dom: getField(fields[3], dom),
Month: getField(fields[4], months),
Dow: getField(fields[5], dow),
}
return schedule, nil
}
// 解析items 。
func getField(field string, r bounds) uint64 {
var bits uint64
// items利用 "," 拆解出 item 。
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
// 利用穷举法一一检测
bits |= getRange(expr, r)
}
return bits
}
// 利用穷举法一一检测
func getRange(expr string, r bounds) uint64 {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
)
var extra_star uint64
//是否仅有一个字符是 * 或者 ?。
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra_star = starBit
} else {
//是否可以"-"分解为俩数字
start = parseIntOrName(lowAndHigh[0], r.names)
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end = parseIntOrName(lowAndHigh[1], r.names)
default:
log.Panicf("Too many hyphens: %s", expr)
}
}
//是否可以"/"分解为俩数字
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step = mustParseInt(rangeAndStep[1])
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
log.Panicf("Too many slashes: %s", expr)
}
//转化为点 。
if start < r.min {
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
return getBits(start, end, step) | extra_star
}
// 辅助函数 。 解析预定义的名字或者数字
func parseIntOrName(expr string, names map[string]uint) uint {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt
}
}
return mustParseInt(expr)
}
//辅助函数 。 字符串 - 数字
func mustParseInt(expr string) uint {
num, err := strconv.Atoi(expr)
if err != nil {
log.Panicf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
}
return uint(num)
}
// 辅助函数 具体的将每个点设置好
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// 辅助函数 。 设置区间的点 + 任意标志
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// 解析预定义的名字
func parseDescriptor(spec string) Schedule {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}
case "@monthly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}
case "@weekly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}
case "@daily", "@midnight":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: 1 << hours.min,
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
case "@hourly":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << minutes.min,
Hour: all(hours),
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}
const every = "@every "
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if err != nil {
log.Panicf("Failed to parse duration %s: %s", spec, err)
}
return Every(duration)
}
log.Panicf("Unrecognized descriptor: %s", spec)
return nil
}
spec.go
func (s *SpecSchedule) Next(t time.Time) time.Time {
// 秒级别的取整
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
// 判断一个字段是否被累加,如果是, 那么它的下一级别的字段需要归 0 。
added := false
//到未来的探寻不超过5年
yearLimit := t.Year() + 5
// 下一级别的字段累加到重置,需要重新累加上一级别的字段的时候的goto点
// 比如要找每个月的31号的时候, 4月是符合月份字段的规定的,但是4月的没有31号。 遍历尽4月的每一天后,只能请求重新累加月份。
WRAP:
if t.Year() > yearLimit {
return time.Time{}
}
// 月
for 1<<uint(t.Month())&s.Month == 0 {
// If we have to add a month, reset the other parts to 0.
if !added {
added = true
// Otherwise, set the date at the beginning (since the current time is irrelevant).
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 1, 0)
// Wrapped around.
if t.Month() == time.January {
goto WRAP
}
}
// 天 , 一次处理 天/月 和 天/周
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 0, 1)
if t.Day() == 1 {
goto WRAP
}
}
// 时
for 1<<uint(t.Hour())&s.Hour == 0 {
if !added {
added = true
t = t.Truncate(time.Hour)
}
t = t.Add(1 * time.Hour)
if t.Hour() == 0 {
goto WRAP
}
}
// 分
for 1<<uint(t.Minute())&s.Minute == 0 {
if !added {
added = true
t = t.Truncate(time.Minute)
}
t = t.Add(1 * time.Minute)
if t.Minute() == 0 {
goto WRAP
}
}
// 秒
for 1<<uint(t.Second())&s.Second == 0 {
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(1 * time.Second)
if t.Second() == 0 {
goto WRAP
}
}
return t
}
//一次处理 天/月 和 天/周 。 如果两者中有任意, 那么必须同时符合另一个才算是匹配
func dayMatches(s *SpecSchedule, t time.Time) bool {
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
)
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}
这里已经和cron无关了, 仅仅是如何能够运行一个稳定的, 以维护的, 方便使用的定时任务管理类。
cron.go
type Schedule interface { //给定一个时间,给出下一个执行的时间 Next(time.Time) time.Time }
cron.go
type Job interface { Run() }
cron.go
type Entry struct { // 、计时器 Schedule Schedule // 下次执行时间 Next time.Time // 上次执行时间 Prev time.Time // 任务 Job Job }
cron.go
type Cron struct {
entries []*Entry // 任务们
stop chan struct{} // 叫停止的途径
add chan *Entry // 添加新任务的方式
snapshot chan []*Entry // 请求获取任务快照的方式
running bool // 是否运行中
ErrorLog *log.Logger // 出错日志
}
// 开始和结束全部任务
func (c *Cron) Start() {
c.running = true
go c.run()
}
func (c *Cron) Stop() {
if !c.running {
return
}
c.stop <- struct{}{}
c.running = false
}
//运行接口
func (c *Cron) run() {
// Figure out the next activation times for each entry.
now := time.Now().Local()
for _, entry := range c.entries {
entry.Next = entry.Schedule.Next(now)
}
// 无限循环
for {
//通过对下一个执行时间进行排序,判断那些任务是下一次被执行的,防在队列的前面
sort.Sort(byTime(c.entries))
var effective time.Time
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
// If there are no entries yet, just sleep - it still handles new entries
// and stop requests.
effective = now.AddDate(10, 0, 0)
} else {
effective = c.entries[0].Next
}
select {
case now = <-time.After(effective.Sub(now)):
// 运行需要执行的任务
for _, e := range c.entries {
if e.Next != effective {
break
}
go c.runWithRecovery(e.Job)
e.Prev = e.Next
e.Next = e.Schedule.Next(effective)
}
continue
case newEntry := <-c.add: //添加新任务
c.entries = append(c.entries, newEntry)
newEntry.Next = newEntry.Schedule.Next(time.Now().Local())
case <-c.snapshot: // 运行中获取快照
c.snapshot <- c.entrySnapshot()
case <-c.stop: //停止
return
}
// 'now' should be updated after newEntry and snapshot cases.
now = time.Now().Local()
}
}
// 辅助函数 。 处理panic , 单个任务的panic不会导致全局的crash
func (c *Cron) runWithRecovery(j Job) {
defer func() {
if r := recover(); r != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.logf("cron: panic running job: %v\n%s", r, buf)
}
}()
j.Run()
}
// 排序辅助类。 支持len swap less
type byTime []*Entry
func (s byTime) Len() int { return len(s) }
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byTime) Less(i, j int) bool {
// Two zero times should return false.
// Otherwise, zero is "greater" than any other time.
// (To sort it at the end of the list.)
if s[i].Next.IsZero() {
return false
}
if s[j].Next.IsZero() {
return true
}
return s[i].Next.Before(s[j].Next)
}
其他的琐碎接口就不做讲解,至此 , 相信大家可以自己写出自己的cron计时器任务支持库了。