Golang robfig/cron 實現解析

https://www.cnblogs.com/jiangz222/p/12345566.htmlhtml


​    robfig/cron是GO語言中一個定時執行註冊任務的package,  最近我在工程中使用到了它,因爲它的實現優雅且簡單(主要是簡單),因此將源碼過了一遍,記錄和分享在此。git

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

  • 基本玩法golang

        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 }
 上面代碼中,第九、10行的代碼調用方法AddJob(spec string, cmd Job)也能夠實現AddFunc註冊的功能,Job是interface,須要入參類型實現方法:Run()。實際上,方法AddFunc內部將參數cmd 進行了包裝(wrapper),而後也是調用方法AddJob進行註冊。

     若是實際工程中定時執行的邏輯較爲複雜,推薦使用方法AddJob()來註冊,本身寫方法Run(),這樣能夠經過Run所屬的類型來傳遞所需數據,後面介紹都會說成AddJob,等效於AddFunc。app

  • AddJob後發生了什麼? (主要的數據結構)學習

    對於Cron的總體邏輯,最關鍵的兩個數據結構就是struct Entry和Cron。spa

  每當你用AddJob註冊一個定時調用策略,就會爲這個策略生成一個惟一的Entry,不難想象,Entry裏會存儲被執行的時間、須要被調度執行的實體Job。code

        生成entry後,再將entry放到struct Cron的entry列表裏,Cron的結構裏,主要是一些用來和外部交互的channel,好比經過channel添加、刪除entry等。詳見下面的代碼。htm

// 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

相關文章
相關標籤/搜索