​    robfig/cron是GO语言中一个定时执行注册任务的package,  最近我在工程中使用到了它,由于它的实现优雅且简单(主要是简单),所以将源码过了一遍,记录和分享在此。

文档:http://godoc.org/github.com/robfig/cron,repo: https://github.com/robfig/cron

  • 基本玩法

Demo代码如下,先用cron.New()初始化一个实例,然后调用AddFunc(spec string, cmd func()) 注册你希望调用的func,第一个参数为调度的时间策略,第二个参数为到时间后执行的方法。robfig/cron支持非常多样的时间策略(下面的代码举了一些例子),最后通过cron.Start()方法启动。

func TestCronDemo(t *testing.T) {
c := cron.New()
// 通过AddFunc注册
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
c.AddFunc("@every 5m", func() { fmt.Println("every 5m, start 5m fron now") })
// 通过AddJob注册
// var cJob cronJobDemo
// c.AddJob("@every 5s", cJob)
// 启动
c.Start()
// 停止
c.Stop()
}

type cronJobDemo int

func (c cronJobDemo) Run() {
fmt.Println("5s func trigger")
return
}
 上面代码中,第9、10行的代码调用方法AddJob(spec string, cmd Job)也可以实现AddFunc注册的功能,Job是interface,需要入参类型实现方法:Run()。实际上,方法AddFunc内部将参数cmd 进行了包装(wrapper),然后也是调用方法AddJob进行注册。

后面介绍都会说成AddJob,等效于AddFunc。

  • AddJob后发生了什么? (主要的数据结构)

    对于Cron的整体逻辑,最关键的两个数据结构就是struct Entry和Cron。

  每当你用AddJob注册一个定时调用策略,就会为这个策略生成一个唯一的Entry,不难想象,Entry里会存储被执行的时间、需要被调度执行的实体Job。

生成entry后,再将entry放到struct Cron的entry列表里,Cron的结构里,主要是一些用来和外部交互的channel,比如通过channel添加、删除entry等。详见下面的代码。

// Entry 数据结构,每一个被调度实体一个
type Entry struct {
// 唯一id,用于查询和删除
ID EntryID
// 本Entry的调度时间,不是绝对时间,在生成entry时会计算出来
Schedule Schedule
// 本entry下次需要执行的绝对时间,会一直被更新
// 被封装的含义是Job可以多层嵌套,可以实现基于需要执行Job的额外处理
// 比如抓取Job异常、如果Job没有返回下一个时间点的Job是还是继续执行还是delay
Next time.Time
// 上一次被执行时间,主要用来查询
Prev time.Time
// WrappedJob 是真实执行的Job实体
WrappedJob Job
// Job 主要给用户查询
Job Job
}
// Cron 数据结构,为robfig/cron的运行实体使用的s数据结构
type Cron struct {
entries []*Entry // 调度执行实体列表
// chain 用来定义entry里的warppedJob使用什么逻辑(e.g. skipIfLastRunning)
// 即一个cron里所有entry只有一个封装逻辑
chain Chain
stop chan struct{} // 停止整个cron的channel
add chan *Entry // 增加一个entry的channel
remove chan EntryID // 移除一个entry的channel
snapshot chan chan []Entry // 获取entry整体快照的channel
running bool // 代表是否已经在执行,是cron为使用者提供的动态修改entry的接口准备的
logger Logger // 封装golang的log包
runningMu sync.Mutex // 用来修改运行中的cron数据,比如增加entry,移除entry
location *time.Location // 地理位置
parser ScheduleParser // 对时间格式的解析,为interface, 可以定制自己的时间规则。
nextID EntryID // entry的全局ID,新增一个entry就加1
jobWaiter sync.WaitGroup // run job时会进行add(1), job 结束会done(),stop整个cron,以此保证所有job都能退出
}
 需要注意的是,WrappedJob和chain这两个成员,这是Cron实现的Job封装逻辑,目前是解决实际调度Job的异常处理。比如你希望自己的上一个时间点的JobA没有结束,下一个时间点的JobA就不执行,这个“不执行”的逻辑实现就定义在chain,初始化时通过chain将JobA进行封装写入WrappedJob,那么每次JobA调用前会先执行封装逻辑,进行判断。
  • Start后发生了什么? (程序的主体)

cron.Start()执行后,cron的后台程序(方法run())就开始运行了。而它的主体,就是一个定时器的实现和到时后的job运行,加上cron里的数据维护。

cron的定时器实现是一个简洁而典型的业务层实现,着重了解下,具体的流程图可见下图。

它的关键和值得学习之处是:

    • 每个entry都包含自己下一次执行的绝对时间
    • 先对entries按下次执行时间升序排序,只需要对第一个entry启动定时器
    • 定时器到时,只轮询entries里需要执行的entries,不需要全部轮询。
    • 且 执行的是当前时间之前的所有job,容错高;
    • 第一个定时器处理结束开启下次定时器时,也只需要更新执行过的entries的下次执行时间,不需要更新所有的entries

上面的逻辑说完,程序主体已经清晰,除此之外,程序主体里的定时器监听和其他多个channel共用了select-case,这些channel在struct Cron里能看到,实现了entries的动态添加、删除、entries快照获取等功能。代码结构如下:

将这些操作通过channel让程序主体来操作,可以有效的减少互斥锁的使用,也会引入问题,会导致有的job执行时间不是非常精准,导致某些entry被遗漏:

    • 比如最近的jobA的timer在1ms后就要到时,此时加入一个entry,耗时3ms
    • 添加完entry后,再重新启动timer(还是jobA的timer,此处还利 用了golang的time.NewTimer(d Duration)的入参为负数会立即到时的特点)
    • 下次到时的时间必然不是jobA期待的执行时间(理论上晚了2ms)

当然,channel的操作首先是非常简洁省时的,其次,定时器实现里,会扫描所有当前时间之前的entries来执行,增加了容错性

    • 值得称赞的细节

      • interface的使用

        struct Entry里的Schedule和Cron里的ScheduleParser都是interface,意味着我们是可以自己定制注册job时的时间策略的格式的,只要自己实现时间策略的解析和获取方法就好

        这让我想起了以前看过golang里什么时候用interface和struct的讨论,我觉得这是个很好的例子:预期对同一个接口有多个实现时就抽象成interface,不知道该不该用就用struct。

      • wrapper的实现

        上面有提到,通过对Job的封装,cron实现了同一个job多次调用时的异常处理等,值得以后在实践中借鉴。

最后是我加了一点注释的代码,https://github.com/jiangz222/cron/tree/comments-v3

Golang robfig/cron 实现解析的更多相关文章

  1. golang reflect包使用解析

    golang reflect包使用解析 参考 Go反射编码 2个重要的类型 Type Value 其中Type是interface类型,Value是struct类型,意识到这一点很重要 Type和Va ...

  2. go语言 robfig/cron包 实现定时 调用

    package main import ( "github.com/robfig/cron" "time" "fmt" "os&q ...

  3. golang使用yaml格式解析构建配置文件

    现在主流的配置文件格式有这么几种,xml.yaml.config…  xml就算了,太挫了,太土, 太繁琐… config 就是mysql,apache my.cnf的那种格式,这个格式适合功能分层, ...

  4. golang使用simplejson库解析复杂json

    cnblogs原创 golang自带的json解析库encoding/json提供了json字符串到json对象的相互转换,在json字符串比较简单的情况下还是挺好用的,但是当json字符串比较复杂或 ...

  5. golang 开源项目: 配置解析模块--config

    在golang中,配置文件经常使用json格式.json格式的语法,有些繁琐,尤其是出现嵌套的时候,每一块都需要大括号包裹,看起来很臃肿. 本着简单易用的原则,个人开发了一个配置解析模块config, ...

  6. Golang通过结构体解析和封装XML

    Golang解析和封装XML 解析XML成结构体Demo package main import ( "encoding/xml" "fmt" ) //我们通过 ...

  7. 参考MySQL Internals手册,使用Golang写一个简单解析binlog的程序

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. MySQL作为最流行的开源关系型数据库,有大量的拥趸.其生态已经相当完善,各项特性在圈内都有大量研究.每次新特性发布,都会 ...

  8. Cron表达式解析

    每一个域可出现的字符如下:Seconds:          可出现     ", - * /"     四个字符,有效范围为0-59的整数Minutes:          可出 ...

  9. python apsheduler cron 参数解析

    from:https://apscheduler.readthedocs.io/en/v2.1.2/cronschedule.html Cron-style scheduling This is th ...

随机推荐

  1. dfs - 卡一个无符号长整形

    Given a positive integer n, write a program to find out a nonzero multiple m of n whose decimal repr ...

  2. dp - LIS

    某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能超过前一发的高度.某天,雷达捕捉到敌国的导弹来袭.由于 ...

  3. 用户输入- Unity3D游戏开发培训

    用户输入- Unity3D游戏开发培训   作者:Jesai 时间:2018-02-12 14:28:45 用户输入Input 鼠标按键: -方法:GetMouseButton(); -方法:GetM ...

  4. C#图片采集软件 自动翻页 自动分类(收集美图必备工具)(一)

    网站管理员希望将别人的整站数据下载到自己的网站里或者将别人网站的一些内容保存到自己的服务器上.从内容中抽取相关的字段,发布到自己的网站系统中.有时需要将网页相关的文件也保存到本地,如图片.附件等. 图 ...

  5. Java入门 - 语言基础 - 02.开发环境配置

    原文地址:http://www.work100.net/training/java-environment-setup.html 更多教程:光束云 - 免费课程 开发环境配置 序号 文内章节 视频 1 ...

  6. Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name

    目前有发现的两种情况 第一种:是在继承jpa的时候检查实体类@id和@Entity引进的包是否是 import javax.persistence.Id imprt javax.persistence ...

  7. [Other]THUWC2020 游记

    Dec. 20th 一下飞机,\(\text{FJ}\) 选手感觉 \(\text{BJ}\) 好冷 下午去了鸟巢,晚上回 \(\text{GLHT}\) 酒店吃泡面 写了洛谷上的线段树分治模板题之后 ...

  8. 可视化限流管理,Sentinel 控制台启动和接入

    Sentinel 的使用可以分为核心库和控制台两个部分. 核心库不依赖任何框架/库,集成了主流框架,可以进行单机限流降级等功能, 控制台Dashboard提供了可视化的管理限流规则.对集群进行监控,集 ...

  9. Windows 7原版映像中添加usb3.0驱动

    最近用软碟通制作了一个win7原版映像,但是在装新系统的时候发现了一个问题,进入安装界面后,显示没有找到驱动器,但是明明是差了U盘的,通过“shift+f12”调出命令行窗口,输入disk list命 ...

  10. ls命名 | Linux统计文件夹内的文件个数

    ls命名 man ls -R 递归列出全部的目录内容 recusive -a 列出所有的文件(包括以 . 开头的隐藏文件) all -r 逆序排列 reverse -t 按照时间信息排序 time - ...