IT教程 ·

Golang robfig/cron 实现剖析

工作,拼的都是脏和累

​    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()来注册,本身写要领Run(),如许能够经由过程Run所属的范例来通报所需数据,背面引见都会说成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屡次挪用时的非常处置惩罚等,值得今后在实践中自创。

 

 

.NET Core之单元测试(二):使用内存数据库处理单元测试中的数据库依赖

参与评论