robfiig/cron 源码剖析

Cron 表达式

参考wiki
https://en.wikipedia.org/wiki/Cron

robfiig/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          #测试

robfiig/cron 目前实现的需求

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 schedulesYou 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 , # 这些特殊字符。 至于原因,下面将具体实现代码的时候会给出。

cron表达式解析

cron 的BNF表达式以及引出的解析方式

首先,让我试着使用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表达式, 应该遵循以下步骤 :

  1. cron利用空白拆解出独立的items
  2. items利用,拆解出item
  3. item 利用穷举法一一检测( 符合且仅符合下面的一条才能合法) :
    1. 是否仅有一个字符是 * 或者
    2. 是否可以使用/或者-分解为俩数字 。
    3. 是否以数字加L或者W结尾。
    4. 纯数字。
  4. 将规则一一描述,完成规则解析

如何表示一个独立的规则

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年就意味着是个非法的表达式。

robfiig/cron 源码剖析

让我将上面的讲解一一的对应到具体的代码中 :

基本数据结构体

#表示cron规则的结构体

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 uintnames    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 cronimport ("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, "*")}// 按照规则解析每个itemsschedule := &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 uintrangeAndStep     = strings.Split(expr, "/")lowAndHigh       = strings.Split(rangeAndStep[0], "-")singleDigit      = len(lowAndHigh) == 1)var extra_star uint64//是否仅有一个字符是 * 或者 ?。 if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {start = r.minend = r.maxextra_star = starBit} else {//是否可以"-"分解为俩数字 start = parseIntOrName(lowAndHigh[0], r.names)switch len(lowAndHigh) {case 1:end = startcase 2:end = parseIntOrName(lowAndHigh[1], r.names)default:log.Panicf("Too many hyphens: %s", expr)}}//是否可以"/"分解为俩数字 switch len(rangeAndStep) {case 1:step = 1case 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 = truet = 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 = truet = 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 = truet = 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 = truet = 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 > 0dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0)if s.Dom&starBit > 0 || s.Dow&starBit > 0 {return domMatch && dowMatch}return domMatch || dowMatch
}

计时任务管理类 cron.go

这里已经和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 = truego 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.Timeif 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.Nexte.Next = e.Schedule.Next(effective)}continuecase 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 << 10buf := 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 []*Entryfunc (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计时器任务支持库了。

cron表达式解析 + robfig/cron 源码剖析相关推荐

  1. Vue.js 运行原理解析、vue源码剖析

    Vue.js 运行机制全局概览 全局概览 这一节笔者将为大家介绍一下 Vue.js 内部的整个流程,希望能让大家对全局有一个整体的印象,然后我们再来逐个模块进行讲解.从来没有了解过 Vue.js 实现 ...

  2. 详细解析:HashMap源码剖析

    目录: 一.HashMap概述二.HashMap的数据结构三.HashMap源码分析 1.关键属性 2.构造方法 3.存储数据 4.调整大小 5.数据读取 6.HashMap的性能参数 7.Fail- ...

  3. Python源码剖析[19] —— 执行引擎之一般表达式(2)

    Python源码剖析 --Python执行引擎之一般表达式(2) 本文作者: Robert Chen(search.pythoner@gmail.com ) 3.2     Simple.py 前面我 ...

  4. Python源码剖析[16] —— Pyc文件解析

    Python源码剖析[16] -- Pyc文件解析 2008-02-28 18:29:55|  分类: Python |举报 |字号 订阅 Python源码剖析 --Pyc文件解析 本文作者: Rob ...

  5. pymavlink 源码剖析(一)之XML文件的数据解析

    文章目录 1 引言 2 pymavlink 的代码自动生成方法 3 XML 文件的数据解析 3.1 XML 文件预处理 3.2 解析 XML 的数据 3.2.1 依据协议版本初始化一些版本特征变量 3 ...

  6. AWS Lambda中的Cron表达式解析器

    目录 介绍 解释代码 解析函数 特别案例 AWS设置 AWS Lambda AWS API网关 AWS Amplify 下载 index.html - 884 B​​​​​​​ 下载 index.js ...

  7. 老李推荐:第8章2节《MonkeyRunner源码剖析》MonkeyRunner启动运行过程-解析处理命令行参数...

    老李推荐:第8章2节<MonkeyRunner源码剖析>MonkeyRunner启动运行过程-解析处理命令行参数 MonkeyRunnerStarter是MonkeyRunner启动时的入 ...

  8. cron表达式解析 获取N条结果集

    这是我的cron表达式解析工具类: public class CronDateUtil {public static List<String> parser(String cronExpr ...

  9. K8s基础知识学习笔记及部分源码剖析

    K8s基础知识学习笔记及部分源码剖析 在学习b站黑马k8s视频资料的基础上,查阅了配套基础知识笔记和源码剖析,仅作个人学习和回顾使用. 参考资料: 概念 | Kubernetes 四层.七层负载均衡的 ...

最新文章

  1. mysql三表查询数据重复_解决mybatis三表连接查询数据重复的问题
  2. Python地信专题 | 基于geopandas的空间数据分析-文件IO篇
  3. 边记边学PHP-(十五)MySQL数据库基础操作2
  4. LeetCode之Power of Two
  5. 【BZOJ1069】【SCOI2007】—最大土地面积(凸包+旋转卡壳)
  6. Android 去除标题栏和状态栏的方法
  7. UEFI---(NT32)的第一个代码
  8. 计算机html二级难度,计算机二级考试越来越难的实锤!真实数据告诉你到底难在哪里?...
  9. 从内存模型角度说明volatile与synchronized在并发特性方面的区别
  10. 【边喝caffee边Caffe 】(二)python版本的报错:Mean shape incompatible with input shape
  11. php svg 汉字 笔顺,html5 svg汉字书写笔画特效
  12. project2019安装教程
  13. Win7系统怎么开启远程桌面?Win7远程桌面怎么用
  14. 吐血总结《Mysql从入门到入魔》,图文并茂
  15. Java--JSON嵌套JSON中带‘\‘字符的解决方式
  16. 2018/8/16第二周
  17. Android项目Jenkins配置(自定义参数构建,构建完成后360加固+自动下载签名+多渠道配置,自动乐固加固+签名,自动上传蒲公英,自动上传OSS,自动发送钉钉消息,自动发送企业微信应用)
  18. 浏览器放大缩小,页面布局不变,浏览器放大百分比,页面放大到左上角,浏览器缩小百分比,页面缩小至中间(类似csdn官网效果)
  19. java mybatis多层collection嵌套查询
  20. SVN 如何解决冲突?

热门文章

  1. 关于音乐制作的一些网站
  2. mac上启动nginx遇到80端口被占用的解决方法(猜了好久的坑我擦)
  3. 为什么要写引言,引言的目的是啥?
  4. (附源码)springboot汽车配件销售管理系统 毕业设计131650
  5. 小飞计算器(小飞可编程复数计算器)用户手册
  6. ORACLE建用户赋予权限
  7. Hystrix熔断器简介
  8. Python获取前一天的日期,前一个月的第一天,前一个月的最后一天
  9. X-pack 为 Elasticsearch 安全保驾护航
  10. 计算机网络学习笔记(一)应用层、传输层和网络层数据平面