參考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 表達式(不是很嚴謹。。。)
::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
::= | "0"
::= {}
::= "*" | | "-" | "/"
::= | "?" | "L" | "W" | "#"
- ::=
| ;
::= {- ","}
- ;
::= " "" "" "" "" "
至此, 如果我打算解析一個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< 0
dowMatch bool = 1< 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計時器任務支持庫了。