cron式の説明 + robfig/cronのソースコード分解
robfiig/cron ソースコード解析
Cron式
参考Wiki
https://en.wikipedia.org/wiki/Cron
robfiig/cron プロジェクト情報
ダウンロードアドレス
https://github.com/robfig/cron.git
ファイルディレクトリの説明
constantdelay.go # One of the simplest second-level timing systems. Not related to cron
constantdelay_test.go #test
cron.go #Cron system. Manage a series of cron timed jobs (Schedule Job)
cron_test.go #Test
doc.go #Documentation
LICENSE #Authorization
parser.go #parser, parse cron format string city a specific timer (Schedule)
parser_test.go #test
README.md #README
spec.go #Single timer (Schedule) structure. How to calculate your next trigger time
spec_test.go #test
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 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 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 * * 0
@daily (or @midnight) | Run once a day, midnight | 0 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>" quot; "<items>
この時点で、もし私がcron式をパースするとしたら、以下のステップを踏むでしょう。
-
cron
空白を使用して、分離されたitems
. -
items
を使用することで,
を逆アセンブルします。item
. -
item
網羅的なテストを使用する(以下のいずれか1つだけが合法である)。-
という文字は1つしかないのでしょうか?
*
または?
. -
を使用することは可能ですか?
/
または-
2つの数値に分解する。 -
は、数字にプラス
L
またはW
エンディングです。 - 純粋な数字
-
という文字は1つしかないのでしょうか?
- ルールを一つずつ記述し、ルールの解析を完了させる
スタンドアロンルールの表現方法
cron式は一連の時間を表現するために使用され、時間はそれ自身の間隔、分、秒0〜59、時間0〜23、日/月0〜31、日/週0〜6、月0〜11を逃れることはできません。これらは基本的に点の集合、つまり整数の区間である。したがって、任意の整数区間について、cronのルールのいくつかを次のように記述することができます。
-
* | ?
任意,区間内のすべての点に対応する。(さらに、day/weekとday/monthは互いに干渉しあうことに注意)。 - 特定の点に対応する純粋な数値。
-
/
2つの数a , bの除算で、a + n * b (n >= 0 ) に一致する区間上のすべての点。 -
-
分割された2つの数。この2つの数で決まる区間内のすべての点に対応する。 -
L | W
特定の時間に特化する必要があり、区間内のポイントに一般化できない。
この時点で
robfig/cron
なぜ
L | W
その理由は明確です。この2つの規則を取り除くと、残りの規則は点の網羅的なリストを使って一般的な方法で表現することができる。最大の区間が60点しかないことを考えると
uint64
整数の各ビットで点を表現するのが適切である。ここでは
robfig/cron
の表現です。
/*
------------------------------------------------------------
The 64th marker is arbitrary and is used for day/week and day/month interferences.
63 - 0 for each point of the interval [63 , 0].
------------------------------------------------------------
Assuming that the interval is 0 - 63, the following example is given.
For example, 0/3 is represented as follows.
* / ?
+--+--------------------------------------------------------+
| 0 | 1 0 0 1 0 0 1 ~~ ~~ 1 0 0 1 0 0 1 |
+--+--------------------------------------------------------+
63 ~ ~ ~~ 0
For example, 2-5 is represented as follows.
* / ?
+--+--------------------------------------------------------+
| 0 | 0 0 0 0 0 ~ ~ ~~ ~ 0 0 0 0 1 1 1 1 1 0 0 |
+--+--------------------------------------------------------+
63 ~ ~ ~~ 0
For example, * is represented as follows.
* / ?
+--+--------------------------------------------------------+
| 1 | 1 1 1 1 1 1 ~ ~ ~ 1 1 1 1 1 1 1 1 1 1 1 1 |
+--+--------------------------------------------------------+
63 ~ ~ ~~ 0
*/
タイマーの基本機能
時刻がルールに合っているか
ルールがあれば、ある時点がルールに合致するかどうかは、対応するビットが1であるかどうかで判断できる。
ルールにマッチする次の時間を事前に決定する
ある時刻が与えられたとき、そのルールに合致する次の時刻を見つけるのも簡単です。
- 各フィールドの次に対象となる値を、大きいものから順に探す。
- 各時間フィールドを順番に、条件を満たすかどうかを判断するために1つずつ追加します。
このトラバーサルは間隔が狭いので非常に効率的ですが、ずっと下を見ないように注意してください。最も悲観的なケースは、うるう年の2月29日でしょう。5年以上先の探索は不正な表現ということになります。
robfiig/cron ソースコード解析
以上の説明を具体的なコードに置き換えてみましょう。
基本的なデータ構造
#cronのルールを表す構造体
spec.go
// Each time field is a uint64
type SpecSchedule struct {
Second, Minute, Hour, Dom, Month, Dow uint64
}
// For 64-bit arbitrary identifiers
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
SpecSchedule
parser.go
package cron
import (
"fmt"
"log"
"math"
"strconv"
"strings"
"time"
)
// Parse the string into SpecSchedule . SpecSchedule conforms to the Schedule interface
func Parse(spec string) (_ Schedule, err error) {
// Process the panic . Do not let the idiom crash. Instead, output it as an error
defer func() {
if recovered := recover(); recovered ! = nil {
err = fmt.Errorf("%v", recovered)
}
}()
// handle special special strings directly.
if spec[0] == '@' {
return parseDescriptor(spec), nil
}
// cron uses whitespace to disassemble separate items.
fields := strings.Fields(spec)
if len(fields) ! = 5 && len(fields) ! = 6 {
// Parse each item by rule
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
}
// Parse items.
func getField(field string, r bounds) uint64 {
var bits uint64
// Items are broken out using ",".
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
// use the exhaustive method to detect each one
bits |= getRange(expr, r)
}
return bits
}
// use the exhaustive method to detect each one
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
// Is there only one character that is * or ?
if lowAndHigh[0] == "*" || lowAndHigh[0] == "? " {
start = r.min
end = r.max
extra_star = starBit
} else {
//can "-" be broken into two numbers
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)
}
}
//can "/" be decomposed into two numbers
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)
}
// Convert to points .
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
}
// Helper function . Parse a predefined name or number
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)
}
// helper functions . String - Number
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)
}
// helper function to set each point specifically
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
}
// Helper function . Set the points of the interval + arbitrary flags
func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}
// Parse the predefined names
func parseDescriptor(spec string) Schedule {
switch spec {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Minute: 1 << min
次にそのルールに合致したときを事前に判断する
spec.go
func (s *SpecSchedule) Next(t time.Time) time.Time {
// Rounding at the second level
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
// Determine if a field is added or not, if so, then the field at the next level needs to be zeroed.
added := false
// The future is not explored more than 5 years into the future
yearLimit := t.Year() + 5
// the next level of the field accumulation to reset, need to re-accumulate plus a level of the field when the goto point
// For example, if you want to find the 31st of each month, April matches the month field, but there is no 31st in April. After traversing every day of April, you can only request to re-accumulate the month.
WRAP:
if t.Year() > yearLimit {
return time.Time{}
}
// month
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, 0, 0, t.Location())
}
t = t.AddDate(0, 1, 0)
// Wrapped around.
if t.Month() == time.January {
goto WRAP
}
}
// Days, handling days/month and days/week at a time
for !dayMatches(s, t) {
if !added {
added = true
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, 0, t.Location())
}
t = t.AddDate(0, 0, 1)
if t.Day() == 1 {
goto WRAP
}
}
// time
for 1<<uint(t.Hour())&s.Hour == 0 {
if !added {
added = true
t = t.Truncate(time.Hour)
}
t = t.Add(1 * time.Hour)
if t.Hour() == 0 {
goto WRAP
}
}
// divide
for 1<<uint(t.Minute())&s.Minute == 0 {
if !added {
added = true
t = t.Truncate(time.Minute)
}
t = t.Add(1 * time.Minute)
if t.Minute() == 0 {
goto WRAP
}
}
// seconds
for 1<<uint(t.Second())&s.Second == 0 {
if !added {
added = true
t = t.Truncate(time.Second)
}
t = t.Add(1 * time.Second)
if t.Second() == 0 {
goto WRAP
}
}
return t
}
// Process both days/month and days/week at once. If any of the two are present, then both must match the other to be considered a match
func dayMatches(s *SpecSchedule, t time.Time) bool {
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch 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 {
//Given a time, give the next execution time
Next(time.Time) time.Time
}
タスクの抽象化
cron.go
type Job interface {
Run()
}
タイマタスク
cron.go
type Entry struct {
// , timer
Schedule Schedule
// Next execution time
Next time.
// last execution time
Prev time.
// Task
Job Job
Job }
マネージャー
cron.go
type Cron struct {
entries []*Entry // Tasks
stop chan struct {} // The way to call stop
add chan *Entry // The way to add a new task
snapshot chan []*Entry // A way to request a snapshot of a task
running bool // Whether it is running or not
ErrorLog *log.Logger // Error log
}
// Start and end all tasks
func (c *Cron) Start() {
c.running = true
go c.running()
}
func (c *Cron) Stop() {
if !c.running {
return
}
c.stop <- struct{}{}
c.running = false
}
//running interface
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)
}
// infinite loop
for {
// Determine which tasks are next to be executed by sorting on the next execution time, preventing them from being at the front of the queue
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
Next}
select {
case now = <-time.After(effective.Sub(now)):
// Run the task that needs to be executed
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: //add new task
c.entries = append(c.entries, newEntry)
newEntry.Next = newEntry.Schedule.Next(time.Now().Local())
case <-c.snapshot: // Get snapshot while running
c.snapshot <- c.entrySnapshot()
case <-c.stop: // stop
return
}
// 'now' should be updated after newEntry and snapshot cases.
now = time.Now().Local()
}
}
// Helper functions. Handle panic, single-task panic does not cause global crashes
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()
}
// Sorting helper class. Support 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タイマータスクのサポートライブラリを書けばいいと思います。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
ハートビート・エフェクトのためのHTML+CSS
-
HTML ホテル フォームによるフィルタリング
-
HTML+cssのボックスモデル例(円、半円など)「border-radius」使いやすい
-
HTMLテーブルのテーブル分割とマージ(colspan, rowspan)
-
ランダム・ネームドロッパーを実装するためのhtmlサンプルコード
-
Html階層型ボックスシャドウ効果サンプルコード
-
QQの一時的なダイアログボックスをポップアップし、友人を追加せずにオンラインで話す効果を達成する方法
-
sublime / vscodeショートカットHTMLコード生成の実装
-
HTMLページを縮小した後にスクロールバーを表示するサンプルコード
-
html のリストボックス、テキストフィールド、ファイルフィールドのコード例