├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── example └── example.go ├── scheduler.go └── scheduler_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.11.x 5 | - 1.12.x 6 | - master 7 | 8 | env: 9 | - CODECOV_TOKEN="8b07e4a4-c97b-44db-bda3-025a367ea1ae" 10 | 11 | script: 12 | - go test -v -coverprofile=coverage.txt -covermode=atomic github.com/prprprus/scheduler 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | ![scheduler6.png](https://i.loli.net/2019/09/21/CbpFx7TIvSNM1EP.png) 2 | 3 | ![build status](https://travis-ci.org/prprprus/scheduler.svg?branch=master) 4 | [![codecov](https://codecov.io/gh/prprprus/scheduler/branch/master/graph/badge.svg)](https://codecov.io/gh/prprprus/scheduler) 5 | [![godoc](https://img.shields.io/badge/godoc-godoc-blue.svg)](https://godoc.org/github.com/prprprus/scheduler) 6 | [![license](https://img.shields.io/badge/license-license-yellow.svg)](https://github.com/prprprus/scheduler/blob/master/LICENSE) 7 | 8 | [英文文档](https://github.com/prprprus/scheduler) 9 | 10 | ## 介绍 11 | 12 | scheduler 是 Go 语言实现的作业调度工具包。它提供了一种简单、人性化的方式去调度 Go 函数,包括延迟和周期性两种调度方式。 13 | 14 | 灵感来源于 Linux [cron](https://opensource.com/article/17/11/how-use-cron-linux) 15 | , [schedule](https://github.com/dbader/schedule) 和 [gocron](https://github.com/jasonlvhit/gocron) 。 16 | 17 | ## 功能 18 | 19 | - 延迟执行,精确到一秒钟 20 | - 周期性执行,精确到一秒钟,类似 cron 的风格,但是更加的灵活 21 | - 取消 job 22 | - 失败重试(暂时重试一次) 23 | 24 | ## 安装 25 | 26 | ``` 27 | go get github.com/prprprus/scheduler 28 | ``` 29 | 30 | ## 例子 31 | 32 | job 函数 33 | 34 | ```Go 35 | func task1(name string, age int) { 36 | fmt.Printf("run task1, with arguments: %s, %d\n", name, age) 37 | } 38 | 39 | func task2() { 40 | fmt.Println("run task2, without arguments") 41 | } 42 | ``` 43 | 44 | ### 延迟调度 45 | 46 | 延迟调度支持四种模式:按秒、分、小时、天。 47 | 48 | 作为特例,任务将通过 `s.Delay().Do(task)` 立即执行。 49 | 50 | ```Go 51 | package main 52 | 53 | import ( 54 | "fmt" 55 | 56 | "github.com/prprprus/scheduler" 57 | ) 58 | 59 | func main() { 60 | s, err := scheduler.NewScheduler(1000) 61 | if err != nil { 62 | panic(err) // just example 63 | } 64 | 65 | // delay with 1 second, job function with arguments 66 | s.Delay().Second(1).Do(task1, "prprprus", 23) 67 | 68 | // delay with 1 minute, job function without arguments 69 | s.Delay().Minute(1).Do(task2) 70 | 71 | // delay with 1 hour 72 | s.Delay().Hour(1).Do(task2) 73 | 74 | // special: execute immediately 75 | s.Delay().Do(task2) 76 | 77 | // cancel job 78 | jobID := s.Delay().Day(1).Do(task2) 79 | err = s.CancelJob(jobID) 80 | if err != nil { 81 | panic(err) 82 | } else { 83 | fmt.Println("cancel delay job success") 84 | } 85 | } 86 | ``` 87 | 88 | ### 周期性调度 89 | 90 | 类似 cron 的风格,同样会包括秒、分、小时、天、星期、月,但是它们之间的顺序和数量不需要固定成一个死格式。你可以按照你的个人喜好去进行排列组合。例如,`Second(3).Minute(35).Day(6)` 91 | 和 `Minute(35).Day(6).Second(3)` 的效果是一样的。不需要再去记格式了!🎉👏 92 | 93 | 但是为了可读性,推荐按照从小到大(或者从大到小)的顺序使用。 94 | 95 | 注意:`Day()` 和 `Weekday()` 避免同时出现,除非你清楚知道这天是星期几。 96 | 97 | 作为特例,任务将通过 `s.Every().Do(task)` 每秒被执行一次。 98 | 99 | ```Go 100 | package main 101 | 102 | import ( 103 | "fmt" 104 | 105 | "github.com/prprprus/scheduler" 106 | ) 107 | 108 | func main() { 109 | s, err := scheduler.NewScheduler(1000) 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | // Specifies time to execute periodically 115 | s.Every().Second(45).Minute(20).Hour(13).Day(23).Weekday(3).Month(6).Do(task1, "prprprus", 23) 116 | s.Every().Second(15).Minute(40).Hour(16).Weekday(4).Do(task2) 117 | s.Every().Second(1).Do(task1, "prprprus", 23) 118 | 119 | // special: executed once per second 120 | s.Every().Do(task2) 121 | 122 | // cancel job 123 | jobID := s.Every().Second(1).Minute(1).Hour(1).Do(task2) 124 | err = s.CancelJob(jobID) 125 | if err != nil { 126 | panic(err) 127 | } else { 128 | fmt.Println("cancel periodically job success") 129 | } 130 | } 131 | ``` 132 | 133 | ## Documentation 134 | 135 | [完整的文档](https://godoc.org/github.com/prprprus/scheduler) 136 | 137 | ## Contribution 138 | 139 | 非常感谢你对该项目感兴趣,你的帮助对我来说是非常宝贵的。你可以提交 issue、pull requests 以及 fork,建议在 pull requests 之前先提交一个 issue 哈。 140 | 141 | ## License 142 | 143 | [LICENSE](https://github.com/prprprus/scheduler/blob/master/LICENSE) 详情. 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![scheduler6.png](https://i.loli.net/2019/09/21/CbpFx7TIvSNM1EP.png) 2 | 3 | ![build status](https://travis-ci.org/prprprus/scheduler.svg?branch=master) 4 | [![codecov](https://codecov.io/gh/prprprus/scheduler/branch/master/graph/badge.svg)](https://codecov.io/gh/prprprus/scheduler) 5 | [![godoc](https://img.shields.io/badge/godoc-godoc-blue.svg)](https://godoc.org/github.com/prprprus/scheduler) 6 | [![license](https://img.shields.io/badge/license-license-yellow.svg)](https://github.com/prprprus/scheduler/blob/master/LICENSE) 7 | 8 | [中文文档](https://github.com/prprprus/scheduler/blob/master/README-zh.md) 9 | 10 | ## Introduction 11 | 12 | The scheduler is a job scheduling package for Go. It provides a simple, human-friendly way to schedule the execution of 13 | the go function and includes delay and periodic. 14 | 15 | Inspired by Linux [cron](https://opensource.com/article/17/11/how-use-cron-linux), 16 | [schedule](https://github.com/dbader/schedule) and [gocron](https://github.com/jasonlvhit/gocron). 17 | 18 | ## Features 19 | 20 | - Delay execution, accurate to a second 21 | - Periodic execution, accurate to a second, like the cron style but more flexible 22 | - Cancel job 23 | - Failure retry 24 | 25 | ## Installation 26 | 27 | ``` 28 | go get github.com/prprprus/scheduler 29 | ``` 30 | 31 | ## Example 32 | 33 | job function 34 | 35 | ```Go 36 | func task1(name string, age int) { 37 | fmt.Printf("run task1, with arguments: %s, %d\n", name, age) 38 | } 39 | 40 | func task2() { 41 | fmt.Println("run task2, without arguments") 42 | } 43 | ``` 44 | 45 | ### Delay 46 | 47 | Delayed supports four modes: seconds, minutes, hours, and days. 48 | 49 | As a special case, the task will be executed immediately via `s.Delay().Do(task)` . 50 | 51 | ```Go 52 | package main 53 | 54 | import ( 55 | "fmt" 56 | 57 | "github.com/prprprus/scheduler" 58 | ) 59 | 60 | func main() { 61 | s, err := scheduler.NewScheduler(1000) 62 | if err != nil { 63 | panic(err) // just example 64 | } 65 | 66 | // delay with 1 second, job function with arguments 67 | s.Delay().Second(1).Do(task1, "prprprus", 23) 68 | 69 | // delay with 1 minute, job function without arguments 70 | s.Delay().Minute(1).Do(task2) 71 | 72 | // delay with 1 hour 73 | s.Delay().Hour(1).Do(task2) 74 | 75 | // special: execute immediately 76 | s.Delay().Do(task2) 77 | 78 | // cancel job 79 | jobID := s.Delay().Day(1).Do(task2) 80 | err = s.CancelJob(jobID) 81 | if err != nil { 82 | panic(err) 83 | } else { 84 | fmt.Println("cancel delay job success") 85 | } 86 | } 87 | ``` 88 | 89 | ### Every 90 | 91 | Like the cron style, it also includes seconds, minutes, hours, days, weekday, and months, but the order and number are 92 | not fixed. You can freely arrange and combine them according to your own preferences. For example, the effects 93 | of `Second(3).Minute(35).Day(6)` and `Minute(35).Day(6).Second(3)` are the same. No need to remember the format! 🎉👏 94 | 95 | But for the readability, recommend the chronological order from small to large (or large to small). 96 | 97 | Note: `Day()` and `Weekday()` avoid simultaneous occurrences unless you know that the day is the day of the week. 98 | 99 | As a special case, the task will be executed once per second via `s.Every().Do(task)`. 100 | 101 | ```Go 102 | package main 103 | 104 | import ( 105 | "fmt" 106 | 107 | "github.com/prprprus/scheduler" 108 | ) 109 | 110 | func main() { 111 | s, err := scheduler.NewScheduler(1000) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | // Specifies time to execute periodically 117 | s.Every().Second(45).Minute(20).Hour(13).Day(23).Weekday(3).Month(6).Do(task1, "prprprus", 23) 118 | s.Every().Second(15).Minute(40).Hour(16).Weekday(4).Do(task2) 119 | s.Every().Second(1).Do(task1, "prprprus", 23) 120 | 121 | // special: executed once per second 122 | s.Every().Do(task2) 123 | 124 | // cancel job 125 | jobID := s.Every().Second(1).Minute(1).Hour(1).Do(task2) 126 | err = s.CancelJob(jobID) 127 | if err != nil { 128 | panic(err) 129 | } else { 130 | fmt.Println("cancel periodically job success") 131 | } 132 | } 133 | ``` 134 | 135 | ## Documentation 136 | 137 | [Full documentation](https://godoc.org/github.com/prprprus/scheduler) 138 | 139 | ## Contribution 140 | 141 | Thank you for your interest in the contribution of the scheduler, your help and contribution is very valuable. 142 | 143 | You can submit an issue and pull requests and fork, please submit an issue before submitting pull requests. 144 | 145 | ## License 146 | 147 | See [LICENSE](https://github.com/prprprus/scheduler/blob/master/LICENSE) for more information. 148 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/prprprus/scheduler" 8 | ) 9 | 10 | func task1(name string, age int) { 11 | fmt.Printf("run task1, with arguments: %s, %d\n", name, age) 12 | } 13 | 14 | func task2() { 15 | fmt.Println("run task2, without arguments") 16 | } 17 | 18 | func main() { 19 | s, err := scheduler.NewScheduler(1000) 20 | if err != nil { 21 | panic(err) // just example 22 | } 23 | 24 | fmt.Println("-------------------------------------------------") 25 | fmt.Println() 26 | 27 | // delay with 1 second, job function with arguments 28 | s.Delay().Second(1).Do(task1, "prprprus", 23) 29 | 30 | // delay with 1 minute, job function without arguments 31 | s.Delay().Minute(1).Do(task2) 32 | 33 | // delay with 1 hour 34 | s.Delay().Hour(1).Do(task2) 35 | 36 | // special: execute immediately 37 | s.Delay().Do(task2) 38 | 39 | // cancel job 40 | jobID := s.Delay().Day(1).Do(task2) 41 | err = s.CancelJob(jobID) 42 | if err != nil { 43 | panic(err) 44 | } else { 45 | fmt.Println("cancel delay job success") 46 | } 47 | 48 | time.Sleep(3 * time.Second) 49 | fmt.Println() 50 | fmt.Println("--------------------------------------------------") 51 | fmt.Println() 52 | 53 | // Specifies time to execute periodically 54 | s.Every().Second(45).Minute(20).Hour(13).Day(23).Weekday(3).Month(6).Do(task1, "prprprus", 23) 55 | s.Every().Second(15).Minute(40).Hour(16).Weekday(4).Do(task2) 56 | s.Every().Second(1).Do(task1, "prprprus", 23) 57 | 58 | // special: executed once per second 59 | s.Every().Do(task2) 60 | 61 | // cancel job 62 | jobID = s.Every().Second(1).Minute(1).Hour(1).Do(task2) 63 | err = s.CancelJob(jobID) 64 | if err != nil { 65 | panic(err) 66 | } else { 67 | fmt.Println("cancel periodically job success") 68 | } 69 | 70 | fmt.Println() 71 | fmt.Println("--------------------------------------------------") 72 | } 73 | -------------------------------------------------------------------------------- /scheduler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, prprprus All rights reserved. 2 | // Use of this source code is governed by a BSD-style . 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package scheduler provides a simple, humans-friendly way to schedule the execution of the go function. 6 | // It includes delay execution and periodic execution. 7 | package scheduler 8 | 9 | import ( 10 | "crypto/md5" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "reflect" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | const ( 20 | // Delay represents job type, the job will be delayed execute once according to job sched 21 | Delay = "Delay" 22 | 23 | // Every represents job type, the job will be cycled execute according to job sched 24 | Every = "Every" 25 | 26 | // Key of job sched 27 | Second = "Second" 28 | Minute = "Minute" 29 | Hour = "Hour" 30 | Day = "Day" 31 | Weekday = "Weekday" 32 | Month = "Month" 33 | 34 | // EveryRune is value of job sched, like "*" in cron, represents every 35 | // second/minute/hour/day/weekday/month. 36 | EveryRune = -1 37 | ) 38 | 39 | var ( 40 | // defaultJobSetSize default size for job set 41 | defaultJobSetSize = 5000 42 | 43 | // maxJobSetSize maximum size for job set 44 | maxJobSetSize = 10000 45 | 46 | // ErrPendingJob is returned when the pending job not exist 47 | ErrPendingJob = errors.New("pending job not exist") 48 | 49 | // ErrOverlength is returned when the job size over maxJobSetSize variable 50 | ErrOverlength = errors.New("job set size overlength") 51 | 52 | // ErrJobType is returned when the job type not exist, 53 | // job type is one of Delay and Every. 54 | ErrJobType = errors.New("job type not exist") 55 | 56 | // ErrJobSched is returned when the job sched not exist, 57 | // under normal circumstances, this error will not occur, 58 | // unless the key definition of job sched is incorrectly modified. 59 | ErrJobSched = errors.New("job sched not exist") 60 | 61 | // ErrTimeNegative is returned when the time argument is negative 62 | ErrTimeNegative = errors.New("time argument can not be negative") 63 | 64 | // ErrDupJobID is returned when the generateID generates the same id 65 | ErrDupJobID = errors.New("Duplicate job id") 66 | 67 | // ErrAlreadyComplayed is returned when cancel a completed job 68 | ErrAlreadyComplayed = errors.New("Job hash been completed") 69 | 70 | // ErrCancelJob is returned when time.Timer.Stop function occur error 71 | ErrCancelJob = errors.New("cancel job failed") 72 | 73 | // ErrRangeSecond is returned when Second method argument is not int 74 | ErrRangeSecond = errors.New("argument 0 <= n <= 59 in Second method") 75 | 76 | // ErrRangeMinute is returned when Minute method argument is not int 77 | ErrRangeMinute = errors.New("argument 0 <= n <= 59 in Minute method") 78 | 79 | // ErrRangeHour is returned when Hour method argument is not int 80 | ErrRangeHour = errors.New("argument 0 <= n <= 23 in Hour method") 81 | 82 | // ErrRangeDay is returned when Day method argument is not int 83 | ErrRangeDay = errors.New("argument 1 <= n <= 31 in Day method") 84 | 85 | // ErrRangeWeekday is returned when Weekday method argument is not int 86 | ErrRangeWeekday = errors.New("argument 0 <= n <= 6 in Weekday method") 87 | 88 | // ErrRangeMonth is returned when Month method argument is not int 89 | ErrRangeMonth = errors.New("argument 1 <= n <= 12 in Month method") 90 | 91 | // EmptyJobType represents an empty job type 92 | EmptyJobType = "" 93 | 94 | // EmptySched represents an empty job sched 95 | EmptySched = map[string]int{} 96 | 97 | // jobSet is a instance of JobSet 98 | jobSet = &JobSet{ 99 | lock: new(sync.Mutex), 100 | pendingSet: map[string]*Job{}, 101 | completedSet: map[string]bool{}, 102 | } 103 | ) 104 | 105 | // JobSet 106 | 107 | // JobSet stores pending jobs and completed jobs and it is concurrent safly. 108 | type JobSet struct { 109 | lock *sync.Mutex // ensure concurrent safe 110 | pendingSet map[string]*Job // storage pending jobs 111 | completedSet map[string]bool // storage completed jobs 112 | } 113 | 114 | // setJobDone When the job function is executed then set job done. 115 | func (js *JobSet) setJobDone(id string) { 116 | js.lock.Lock() 117 | defer js.lock.Unlock() 118 | 119 | job := js.pendingSet[id] 120 | 121 | // note: ignore with job type is Every 122 | if job.Type != Every { 123 | if _, ok := js.completedSet[id]; ok { 124 | panic(ErrDupJobID) 125 | } 126 | 127 | delete(js.pendingSet, id) 128 | js.completedSet[id] = true 129 | } 130 | } 131 | 132 | // Job 133 | 134 | // JobTimer is the wrapper for time.Timer, one job corresponds to a JobTimer. 135 | type JobTimer struct { 136 | ID string // unique id 137 | timer *time.Timer // wrapper time.Timer 138 | } 139 | 140 | // JobTicker is the wrapper for time.Ticker, one job corresponds to a JobTicker. 141 | type JobTicker struct { 142 | ID string // unique id 143 | ticker *time.Ticker // wrapper time.Ticker 144 | } 145 | 146 | // Job is an abstraction of a scheduling task. 147 | type Job struct { 148 | ID string // unique id 149 | Type string // job type 150 | 151 | // Sched is a job sched, like cron style but the order of time is not 152 | // fixed, can be arranged and combined at will. 153 | Sched map[string]int 154 | 155 | fn interface{} // job function 156 | args []interface{} // function args 157 | JTimer *JobTimer // JobTimer 158 | JTicker *JobTicker // JobTicker 159 | } 160 | 161 | // Second method set Second key for job sched. 162 | func (j *Job) Second(n int) *Job { 163 | if n < 0 || n > 59 { 164 | panic(ErrRangeSecond) 165 | } 166 | 167 | j.Sched[Second] = n 168 | return j 169 | } 170 | 171 | // Minute method set Minute key for job sched. 172 | func (j *Job) Minute(n int) *Job { 173 | if n < 0 || n > 59 { 174 | panic(ErrRangeMinute) 175 | } 176 | 177 | j.Sched[Minute] = n 178 | return j 179 | } 180 | 181 | // Hour method set Hour key for job sched. 182 | func (j *Job) Hour(n int) *Job { 183 | if n < 0 || n > 23 { 184 | panic(ErrRangeHour) 185 | } 186 | 187 | j.Sched[Hour] = n 188 | return j 189 | } 190 | 191 | // Day method set Day key for job sched. 192 | func (j *Job) Day(n int) *Job { 193 | if n < 1 || n > 31 { 194 | panic(ErrRangeDay) 195 | } 196 | 197 | j.Sched[Day] = n 198 | return j 199 | } 200 | 201 | // Weekday method set Weekday key for job sched. 202 | func (j *Job) Weekday(n int) *Job { 203 | if n < 0 || n > 6 { 204 | panic(ErrRangeWeekday) 205 | } 206 | 207 | j.Sched[Weekday] = n 208 | return j 209 | } 210 | 211 | // Month method set Month key for job sched. 212 | func (j *Job) Month(n int) *Job { 213 | if n < 1 || n > 12 { 214 | panic(ErrRangeMonth) 215 | } 216 | 217 | j.Sched[Month] = n 218 | return j 219 | } 220 | 221 | // Do according to the job type and job sched execute job. 222 | func (j *Job) Do(fn interface{}, args ...interface{}) (jobID string) { 223 | j.fn = fn 224 | j.args = args 225 | 226 | switch j.Type { 227 | case Delay: 228 | // convert to second. Not support Weekday and Month 229 | var second int 230 | for k := range j.Sched { 231 | switch k { 232 | case Second: 233 | second = j.Sched[Second] 234 | case Minute: 235 | second = j.Sched[Minute] * 60 236 | case Hour: 237 | second = j.Sched[Hour] * 60 * 60 238 | case Day: 239 | second = j.Sched[Day] * 60 * 60 * 24 240 | default: 241 | panic(ErrJobSched) 242 | } 243 | } 244 | // initial job.JTimer (note: can not put it in a new goroutine) 245 | j.JTimer = new(JobTimer) 246 | j.JTimer.ID = generateID() 247 | j.JTimer.timer = time.NewTimer(time.Duration(second) * time.Second) 248 | go func() { 249 | // wait... 250 | <-j.JTimer.timer.C 251 | // run job function 252 | j.run() 253 | // set job done 254 | jobSet.setJobDone(j.ID) 255 | }() 256 | case Every: 257 | // initial job.JTicker (note: also can not put it in a new goroutine) 258 | j.JTicker = new(JobTicker) 259 | j.JTicker.ID = generateID() 260 | j.JTicker.ticker = time.NewTicker(1 * time.Second) 261 | go func() { 262 | // begin ticktock... 263 | for t := range j.JTicker.ticker.C { 264 | _ = t 265 | if (j.Sched[Second] == -1 || j.Sched[Second] == time.Now().Second()) && 266 | (j.Sched[Minute] == -1 || j.Sched[Minute] == time.Now().Minute()) && 267 | (j.Sched[Hour] == -1 || j.Sched[Hour] == time.Now().Hour()) && 268 | (j.Sched[Day] == -1 || j.Sched[Day] == time.Now().Day()) && 269 | (j.Sched[Weekday] == -1 || j.Sched[Weekday] == int(time.Now().Weekday())) && 270 | (j.Sched[Month] == -1 || j.Sched[Month] == int(time.Now().Month())) { 271 | // run job function 272 | j.run() 273 | // set job done 274 | jobSet.setJobDone(j.ID) 275 | } 276 | } 277 | }() 278 | default: 279 | panic(ErrJobType) 280 | } 281 | 282 | return j.ID 283 | } 284 | 285 | // run funtion by reflect. 286 | func (j *Job) run() { 287 | rFn := reflect.ValueOf(j.fn) 288 | rArgs := make([]reflect.Value, len(j.args)) 289 | for i, v := range j.args { 290 | rArgs[i] = reflect.ValueOf(v) 291 | } 292 | 293 | // retry 294 | defer func() { 295 | if err := recover(); err != nil { 296 | time.Sleep(5 * time.Second) // wait for five seconds 297 | rFn.Call(rArgs) 298 | } 299 | }() 300 | 301 | rFn.Call(rArgs) 302 | } 303 | 304 | // Scheduler 305 | 306 | // Scheduler is responsible for scheduling jobs. 307 | type Scheduler struct { 308 | jobSetSize int // custom size for job set, can not overlength maxJobSetSize 309 | js *JobSet // JobSet 310 | } 311 | 312 | // NewScheduler new Scheduler instance. 313 | func NewScheduler(jss int) (*Scheduler, error) { 314 | if jss > maxJobSetSize { 315 | return nil, ErrOverlength 316 | } 317 | if jss <= 0 { 318 | jss = defaultJobSetSize 319 | } 320 | 321 | s := &Scheduler{ 322 | jobSetSize: jss, 323 | js: jobSet, 324 | } 325 | return s, nil 326 | } 327 | 328 | // Delay method schedule job with Delay mode. 329 | func (s *Scheduler) Delay() *Job { 330 | s.js.lock.Lock() 331 | defer s.js.lock.Unlock() 332 | 333 | // temporarily handle like this 334 | if len(s.js.pendingSet) >= 10000 { 335 | panic("pending set is full") 336 | } 337 | 338 | // create job 339 | id := generateID() 340 | j := &Job{ 341 | ID: id, 342 | Type: Delay, 343 | Sched: InitJobSched(Delay), 344 | } 345 | 346 | // put in pending job set 347 | s.js.pendingSet[id] = j 348 | return j 349 | } 350 | 351 | // Every method schedule job with Every mode. 352 | func (s *Scheduler) Every() *Job { 353 | s.js.lock.Lock() 354 | defer s.js.lock.Unlock() 355 | 356 | // temporarily handle like this 357 | if len(s.js.pendingSet) >= 10000 { 358 | panic("pending set is full") 359 | } 360 | 361 | // create job 362 | id := generateID() 363 | j := &Job{ 364 | ID: id, 365 | Type: Every, 366 | // Sched[...] = -1 <=> cron * 367 | Sched: InitJobSched(Every), 368 | } 369 | 370 | // put in pending job set 371 | s.js.pendingSet[id] = j 372 | return j 373 | } 374 | 375 | // PendingJob get pending job by id. 376 | func (s *Scheduler) PendingJob(id string) (*Job, error) { 377 | s.js.lock.Lock() 378 | defer s.js.lock.Unlock() 379 | 380 | if job, ok := s.js.pendingSet[id]; ok { 381 | return job, nil 382 | } 383 | return nil, ErrPendingJob 384 | } 385 | 386 | // JobType get job type. 387 | func (s *Scheduler) JobType(id string) (string, error) { 388 | job, err := s.PendingJob(id) 389 | if err != nil { 390 | return EmptyJobType, err 391 | } 392 | 393 | return job.Type, nil 394 | } 395 | 396 | // JobSched get job sched. 397 | func (s *Scheduler) JobSched(id string) (map[string]int, error) { 398 | job, err := s.PendingJob(id) 399 | if err != nil { 400 | return EmptySched, err 401 | } 402 | 403 | return job.Sched, nil 404 | } 405 | 406 | // JobDone Check if the job is completed. 407 | func (s *Scheduler) JobDone(id string) (bool, error) { 408 | s.js.lock.Lock() 409 | defer s.js.lock.Unlock() 410 | 411 | if _, ok := s.js.completedSet[id]; ok { 412 | return true, nil 413 | } 414 | return false, nil 415 | } 416 | 417 | // CancelJob can cancel the job before scheduling. 418 | func (s *Scheduler) CancelJob(id string) error { 419 | // can not cancel a completed job 420 | if _, ok := s.js.completedSet[id]; ok { 421 | return ErrAlreadyComplayed 422 | } 423 | // can not cancel a nonexistent job 424 | if _, ok := s.js.pendingSet[id]; !ok { 425 | return ErrPendingJob 426 | } 427 | 428 | // cancel by job type 429 | job := s.js.pendingSet[id] 430 | switch job.Type { 431 | case Delay: 432 | ok := job.JTimer.timer.Stop() 433 | if ok { 434 | return nil 435 | } 436 | return ErrCancelJob 437 | case Every: 438 | job.JTicker.ticker.Stop() 439 | return nil 440 | default: 441 | return ErrJobType 442 | } 443 | } 444 | 445 | // util 446 | 447 | // generateID generate job id 448 | func generateID() string { 449 | h := md5.New() 450 | io.WriteString(h, time.Now().String()) 451 | id := fmt.Sprintf("%x", h.Sum(nil)) 452 | return id 453 | } 454 | 455 | // InitJobSched initiate job sched by job type 456 | func InitJobSched(jobType string) map[string]int { 457 | if jobType == Delay { 458 | return map[string]int{} 459 | } 460 | return map[string]int{ 461 | Second: EveryRune, 462 | Minute: EveryRune, 463 | Hour: EveryRune, 464 | Day: EveryRune, 465 | Weekday: EveryRune, 466 | Month: EveryRune, 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /scheduler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, prprprus All rights reserved. 2 | // Use of this source code is governed by a BSD-style . 3 | // license that can be found in the LICENSE file. 4 | 5 | package scheduler 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func task1(name, age string, res *[]string) { 14 | *res = append(*res, name, age) 15 | } 16 | 17 | func task2() { 18 | s := "hello-world, task2, without arguments" 19 | _ = s 20 | } 21 | 22 | // JobSet 23 | 24 | func TestJobDone(t *testing.T) { 25 | s, _ := NewScheduler(10) 26 | jobID := s.Delay().Second(0).Do(task2) 27 | time.Sleep(1 * time.Second) 28 | ok, _ := s.JobDone(jobID) 29 | if !ok { 30 | t.Errorf("job has been completed, JobDone method error") 31 | } 32 | 33 | jobID = s.Delay().Hour(1).Do(task2) 34 | time.Sleep(1 * time.Second) 35 | ok, _ = s.JobDone(jobID) 36 | if ok { 37 | t.Errorf("job not completed, JobDone method error") 38 | } 39 | } 40 | 41 | // Scheduler 42 | 43 | func TestNewScheduler(t *testing.T) { 44 | _, err := NewScheduler(-1) 45 | if err != nil { 46 | t.Errorf("maxJobSetSize can be negative") 47 | } 48 | _, err = NewScheduler(10001) 49 | if err == nil { 50 | t.Errorf("maxJobSetSize overlength") 51 | } 52 | } 53 | 54 | func TestPendingJob(t *testing.T) { 55 | s, _ := NewScheduler(10) 56 | jobID := s.Delay().Minute(10).Do(task2) 57 | _, err := s.PendingJob(jobID) 58 | if err != nil { 59 | t.Errorf("pending job should be exists") 60 | } 61 | 62 | _, err = s.PendingJob(generateID()) 63 | if err == nil { 64 | t.Errorf("pending job should not exists") 65 | } 66 | } 67 | 68 | func TestJobType(t *testing.T) { 69 | s, _ := NewScheduler(10) 70 | jobID := s.Delay().Day(3).Do(task2) 71 | _, err := s.JobType(jobID) 72 | if err != nil { 73 | t.Errorf("job type should be exists") 74 | } 75 | 76 | _, err = s.JobType(generateID()) 77 | if err == nil { 78 | t.Errorf("job type should not exists") 79 | } 80 | } 81 | 82 | func TestSched(t *testing.T) { 83 | s, _ := NewScheduler(10) 84 | jobID := s.Delay().Day(1).Do(task2) 85 | _, err := s.JobSched(jobID) 86 | if err != nil { 87 | t.Errorf("job sched should be exists") 88 | } 89 | 90 | _, err = s.JobSched(generateID()) 91 | if err == nil { 92 | t.Errorf("job sched should not exists") 93 | } 94 | } 95 | 96 | func TestCancelJob(t *testing.T) { 97 | s, _ := NewScheduler(10) 98 | 99 | // cancel completed job 100 | jobID := s.Delay().Second(0).Do(task2) 101 | time.Sleep(1 * time.Second) 102 | err := s.CancelJob(jobID) 103 | if err == nil { 104 | t.Errorf("job completed, can not cancel") 105 | } 106 | 107 | // cancel nonexistent job 108 | err = s.CancelJob(generateID()) 109 | if err == nil { 110 | t.Errorf("job not exists, can not cancel") 111 | } 112 | 113 | jobID = s.Delay().Minute(30).Do(task2) 114 | err = s.CancelJob(jobID) 115 | if err != nil { 116 | t.Errorf("job should be cancel") 117 | } 118 | } 119 | 120 | // Job 121 | 122 | func TestSecond(t *testing.T) { 123 | defer func() { 124 | if err := recover(); err != nil && err == ErrRangeSecond { 125 | return 126 | } 127 | }() 128 | 129 | s, _ := NewScheduler(10) 130 | j := s.Every().Second(233) 131 | if j.Sched[Second] != 233 { 132 | t.Errorf("set second error") 133 | } 134 | 135 | // panic 136 | j.Second(-1) 137 | } 138 | 139 | func TestMinute(t *testing.T) { 140 | defer func() { 141 | if err := recover(); err != nil && err == ErrRangeMinute { 142 | return 143 | } 144 | }() 145 | 146 | s, _ := NewScheduler(10) 147 | j := s.Every().Minute(59) 148 | if j.Sched[Minute] != 59 { 149 | t.Errorf("set minute error") 150 | } 151 | 152 | // panic 153 | j.Minute(-1) 154 | } 155 | 156 | func TestHour(t *testing.T) { 157 | defer func() { 158 | if err := recover(); err != nil && err == ErrRangeHour { 159 | return 160 | } 161 | }() 162 | 163 | s, _ := NewScheduler(10) 164 | j := s.Every().Hour(12) 165 | if j.Sched[Hour] != 12 { 166 | t.Errorf("set hour error") 167 | } 168 | 169 | // panic 170 | j.Hour(-1) 171 | } 172 | 173 | func TestDay(t *testing.T) { 174 | defer func() { 175 | if err := recover(); err != nil && err == ErrRangeDay { 176 | return 177 | } 178 | }() 179 | 180 | s, _ := NewScheduler(10) 181 | j := s.Every().Day(24) 182 | if j.Sched[Day] != 24 { 183 | t.Errorf("set day error") 184 | } 185 | 186 | // panic 187 | j.Day(-1) 188 | } 189 | 190 | func TestWeekday(t *testing.T) { 191 | defer func() { 192 | if err := recover(); err != nil && err == ErrRangeWeekday { 193 | return 194 | } 195 | }() 196 | 197 | s, _ := NewScheduler(10) 198 | j := s.Every().Weekday(5) 199 | if j.Sched[Weekday] != 5 { 200 | t.Errorf("set weekday error") 201 | } 202 | 203 | // panic 204 | j.Weekday(-1) 205 | } 206 | 207 | func TestMonth(t *testing.T) { 208 | defer func() { 209 | if err := recover(); err != nil && err == ErrRangeMonth { 210 | return 211 | } 212 | }() 213 | 214 | s, _ := NewScheduler(10) 215 | j := s.Every().Month(1) 216 | if j.Sched[Month] != 1 { 217 | t.Errorf("set month error") 218 | } 219 | 220 | // panic 221 | j.Month(-1) 222 | } 223 | 224 | func TestDo(t *testing.T) { 225 | // Delay with arguments 226 | res1 := []string{} 227 | res2 := []string{"tiger", "23"} 228 | s, _ := NewScheduler(10) 229 | s.Delay().Second(1).Do(task1, "tiger", "23", &res1) 230 | time.Sleep(2 * time.Second) 231 | for i, v := range res1 { 232 | if v != res2[i] { 233 | t.Errorf("Do method error with Delay") 234 | } 235 | } 236 | 237 | // Every with arguments 238 | res1 = []string{} 239 | res2 = []string{"cat", "5"} 240 | s, _ = NewScheduler(10) 241 | jobID := s.Every().Second(1).Do(task1, "cat", "5", &res1) 242 | time.Sleep(2 * time.Second) 243 | err := s.CancelJob(jobID) 244 | if err != nil { 245 | panic(err) 246 | } 247 | for i, v := range res1 { 248 | if v != res2[i] { 249 | t.Errorf("Do method error with Every") 250 | } 251 | } 252 | } 253 | 254 | // util 255 | 256 | func TestInitJobSched(t *testing.T) { 257 | // Delay 258 | s, _ := NewScheduler(10) 259 | j := s.Delay() 260 | if reflect.TypeOf(j.Sched) != reflect.TypeOf(map[string]int{}) && len(j.Sched) == 0 { 261 | t.Errorf("Initial Delay job sched failed") 262 | } 263 | 264 | // Every 265 | s, _ = NewScheduler(10) 266 | j = s.Every() 267 | for k, v := range j.Sched { 268 | if k != Second && k != Minute && k != Hour && k != Day && k != Weekday && k != Month { 269 | t.Errorf("Initial Every job sched failed") 270 | } 271 | if v != -1 { 272 | t.Errorf("Initial Every job sched failed") 273 | } 274 | } 275 | } 276 | --------------------------------------------------------------------------------