关于定时任务:解析-Golang-定时任务库-gron-设计和原理

4次阅读

共计 3543 个字符,预计需要花费 9 分钟才能阅读完成。

解析 Golang 定时工作库 gron 设计和原理

简略说,每一个位都代表了一个工夫维度,* 代表选集,所以,下面的语义是:在每天早上的 4 点 05 分触发工作。
但 cron 毕竟只是一个操作系统级别的工具,如果定时工作失败了,或者压根没启动,cron 是没法提醒开发者这一点的。并且,cron 和 正则表达式都有一种魔力,不知道大家是否感同身受,这里引用共事的一句名言:

这世界上有些语言非常相似: shell 脚本, es 查问的那个 dsl 语言, 定时工作的 crontab, 正则表达式. 他们相似就相似在每次要写的时候基本都得从新现学一遍。

刚巧,最近看到了 gron 这个开源我的项目,它是用 Golang 实现一个并发安全的定时工作库。实现非常简略精美,代码量也不多。明天咱们就来一起拆散源码看一下,怎么基于 Golang 的能力做进去一个【定时工作库】。

Gron provides a clear syntax for writing and deploying cron jobs.

gron

gron 是一个泰国小哥在 2016 年开源的作品,它的个性就在于非常简略和清晰的语义来定义【定时工作】,你不必再去记 cron 的语法。咱们来看下作为使用者怎么上手。
首先,咱们还是一个 go get 安装依赖:
$ go get github.com/roylee0704/gron
复制代码
假设咱们期望在【时机】到了当前,要做的工作是打印一个字符串,每一个小时执行一次,咱们就可能这样:

package main

import (
    "fmt"
    "time"
    "github.com/roylee0704/gron"
)

func main() {c := gron.New()
    c.AddFunc(gron.Every(1*time.Hour), func() {fmt.Println("runs every hour.")
    })
    c.Start()}

复制代码
非常简略,而且即便是在 c.Start 之后咱们依然可能增加新的定时工作进去。反对了很好的扩展性。

定时参数

留意到咱们调用 gron.New().AddFunc() 时传入了一个 gron.Every(1*time.Hour)。
这里其实你可能传入任何一个 time.Duration,从而把调度间隔从 1 小时调整到 1 分钟以至 1 秒。
除此之外,gron 还很贴心地封装了一个 xtime 包用来把常见的 time.Duration 封装起来,这里咱们开箱即用。

import "github.com/roylee0704/gron/xtime"

gron.Every(1 * xtime.Day)
gron.Every(1 * xtime.Week)

复制代码
很多时候咱们不只仅某个工作在当天运行,还心愿是咱们指定的时刻,而不是依赖程序启动工夫,机械地加 24 hour。gron 对此也做了很好的反对:

gron.Every(30 * xtime.Day).At("00:00")
gron.Every(1 * xtime.Week).At("23:59")

复制代码
咱们只需指定 At(“hh:mm”) 就可能实现在指定工夫执行。

源码解析

这一节咱们来看看 gron 的实现原理。
所谓定时工作,其实蕴含两个层面:

触发器。即咱们心愿这个工作在什么工夫点,什么周期被触发;
工作。即咱们在触发之后,心愿执行的工作,类比到咱们下面示例的 fmt.Println。

对这两个概念的封装和扩大是一个定时工作库必须考虑的。
而同时,咱们是在 Golang 的协程上跑程序的,意味着这会是一个长期运行的协程,否则你即便指定了【一个月后干 XXX】这个工作,程序两天后挂了,也就无奈实现你的诉求了。
所以,咱们还心愿有一个 manager 的角色,来治理咱们的一组【定时工作】,如何调度,什么时候启动,怎么停止,启动了当前还想加新工作是否反对。

Cron

在 gron 的体系里,Cron 对象(咱们下面通过 gron.New 创建进去的)就是咱们的 manager,而底层的一个个【定时工作】则对应到 Cron 对象中的一个个 Entry:

// Cron provides a convenient interface for scheduling job such as to clean-up
// database entry every month.
//
// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may also be started, stopped and the entries
// may be inspected.
type Cron struct {entries []*Entry
    running bool
    add     chan *Entry
    stop    chan struct{}}

// New instantiates new Cron instant c.
func New() *Cron {
    return &Cron{stop: make(chan struct{}),
        add:  make(chan *Entry),
    }
}

复制代码

entries 就是定时工作的核心能力,它记录了一组【定时工作】;
running 用来标识这个 Cron 是否已经启动;
add 是一个 channel,用来反对在 Cron 启动后,新增的【定时工作】;
stop 同样是个 channel,留意到是空结构体,用来管制 Cron 的停止。这个其实是经典写法了,对日常开发也有借鉴意义,咱们待会儿会好好看一下。

咱们观察到,当调用 gron.New() 方法后,失去的是一个指向 Cron 对象的指针。此时只是初始化了 stop 和 add 两个 channel,没有启动调度。

Entry

重头戏来了,Cron 外面的 []*Entry 其实就代表了一组【定时工作】,每个【定时工作】可能简化理解为 < 触发器,工作 > 组成的一个 tuple。

// Entry consists of a schedule and the job to be executed on that schedule.
type Entry struct {
    Schedule Schedule
    Job      Job

    // the next time the job will run. This is zero time if Cron has not been
    // started or invalid schedule.
    Next time.Time

    // the last time the job was run. This is zero time if the job has not been
    // run.
    Prev time.Time
}

// Schedule is the interface that wraps the basic Next method.
//
// Next deduces next occurring time based on t and underlying states.
type Schedule interface {Next(t time.Time) time.Time
}

// Job is the interface that wraps the basic Run method.
//
// Run executes the underlying func.
type Job interface {Run()
}

复制代码

Schedule 代表了一个【触发器】,或者说一个定时策略。它只蕴含一个 Next 方法,接受一个工夫点,业务要返回下一次触发调动的工夫点。
Job 则是对【工作】的抽象,只需要实现一个 Run 方法,没有入参出参。

除了这两个核心依赖外,Entry 结构还蕴含了【前一次执行工夫点】和【下一次执行工夫点】,这个目前可能忽略,只是为了辅助代码用。
按照工夫排序

// byTime is a handy wrapper to chronologically sort entries.
type byTime []*Entry

func (b byTime) Len() int      { return len(b) }
func (b byTime) Swap(i, j int) {b[i], b[j] = b[j], b[i] }

// Less reports `earliest` time i should sort before j.
// zero time is not `earliest` time.
func (b byTime) Less(i, j int) bool {if b[i].Next.IsZero() {return false}
    if b[j].Next.IsZero() {return true}

    return b[i].Next.Before(b[j].Next)
}

复制代码
这里是对 Entry 列表的简略封装,因为咱们可能同时有多个 Entry 需要调度,处理的次序很重要。这里实现了 sort 的接口, 有了 Len(), Swap(), Less() 咱们就可能用 sort.Sort() 来排序了。
此处的排序策略是按照工夫大小。

正文完
 0