Usage of Go cron Timing Task

Keywords: Go crontab snapshot github Linux

What is cron?

cron means: to plan tasks, to put it plainly, is to schedule tasks. I have an appointment with the system. It's as simple as running a task at a few minutes or seconds or every few minutes.

cron expression

Cron expression is a good thing. It can be used not only in Java quartZ, but also in Go. I haven't used cron in Linux, but the Internet says that Linux can also use crontab -e command to configure timing tasks. Both the Go language and Java can be accurate to seconds, but not in Linux.

The cron expression represents a collection of times and is represented by fields separated by six spaces:

Field name Is it necessary? Permissible values Permissible specific characters
Seconds (Seconds) yes 0-59 * / , -
Sub (Minute) yes 0-59 * / , -
Time (Hours) yes 0-23 * / , -
Day of month yes 1-31 * / , - ?
Month (Month) yes 1-12 or JAN-DEC * / , -
Day of week no 0-6 or SUM-SAT * / , - ?

  

  

 

 

 

Note:

The values of the Month and Day of week fields are case-insensitive, such as sun, sun and sun.

2. If the Day of week field is not provided, it is equivalent to*

 # ┌───────────── min (0 - 59)
 # │ ┌────────────── hour (0 - 23)
 # │ │ ┌─────────────── day of month (1 - 31)
 # │ │ │ ┌──────────────── month (1 - 12)
 # │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to
 # │ │ │ │ │                  Saturday, or use names; 7 is also Sunday)
 # │ │ │ │ │
 # │ │ │ │ │
 # * * * * *  command to execute

cron Specific Character Description

1) asterisk (*)

Represents that the cron expression matches all the values of the field. For example, in the fifth field, an asterisk (month) is used to indicate each month.

2) diagonal (/)

For growth intervals, if the value of the first field (minutes) is 3-59/15, it means that the execution starts at the 3rd minute of an hour and then every 15 minutes (i.e. at 3, 18, 33, 48), which can also be expressed as: 3/15

3) comma (,)

Enumeration values, such as field 6 values MON,WED,FRI, for Monday, Wednesday, and Five

4) hyphen (-)

Represents a range, such as a value of 9-17 for the third field, which means 9 am to 5 pm per hour (including 9 and 17)

5) Question mark (?)

Use only for days of month and weeks, meaning no specified value, and can be used instead.*

  6)L,W,#

There is no use of L, W, # in Go, as explained below.

An example of cron

Execute every five seconds: */5* * * * *?

Execute every 1 minute: 0 */1 * * *?

Execution at 23:00 a day: 0 023 * *?

Execution at 1:00 a.m. every day: 0.01 * *?

Execution at 1:00 a.m. on the 1st of each month: 0.01 1*?

One execution at 26, 29 and 33 points: 0.26, 29, 33* * *?

Every day at 0:00, 13:00, 18:00 and 21:00: 00, 13,18,21*?

Download and install

The console enters go get github.com/robfig/cron to download the Go package for the timing task, provided your $GOPATH is configured.

Source code analysis

Explanation of File Catalogue

 1 constantdelay.go      #A simple second-level timing system. and cron Irrelevant
 2 constantdelay_test.go #test
 3 cron.go               #Cron System. Managing a series of cron Timing tasks( Schedule Job)
 4 cron_test.go          #test
 5 doc.go                #Description document
 6 LICENSE               #Certificate of authorization 
 7 parser.go             #Parser, parsing cron Format String City A Specific Timer( Schedule)
 8 parser_test.go        #test
 9 README.md             #README
10 spec.go               #Single timer( Schedule)Structure. How to calculate your next trigger time
11 spec_test.go          #test

  cron.go

Structure:

 1 // Cron keeps track of any number of entries, invoking the associated func as
 2 // specified by the schedule. It may be started, stopped, and the entries may
 3 // be inspected while running. 
 4 // Cron Keep track of any number of entries and call related func Schedule specified. It can be started, stopped and entries that can be run while checking.
 5 type Cron struct {
 6     entries  []*Entry        // task
 7     stop     chan struct{}      // The Way to Stop
 8     add      chan *Entry        // How to add new tasks
 9     snapshot chan []*Entry      // How to request a task snapshot
10     running  bool               // Is it running?
11     ErrorLog *log.Logger        // error log(New attribute)
12     location *time.Location     // Location(New attribute)       
13 }

 

 1 // Entry consists of a schedule and the func to execute on that schedule.
 2 // Entry includes timetables and can be executed on timetables func
 3 type Entry struct {
 4         // timer
 5     Schedule Schedule
 6     // Next execution time
 7     Next time.Time
 8     // Last execution time
 9     Prev time.Time
10     // task
11     Job Job
12 }

 

Key methodologies:

 1 //  Start task
 2 // Start the cron scheduler in its own go-routine, or no-op if already started.
 3 func (c *Cron) Start() {
 4     if c.running {
 5         return
 6     }
 7     c.running = true
 8     go c.run()
 9 }
10 // End task
11 // Stop stops the cron scheduler if it is running; otherwise it does nothing.
12 func (c *Cron) Stop() {
13     if !c.running {
14         return
15     }
16     c.stop <- struct{}{}
17     c.running = false
18 }
19 
20 // Perform timed tasks
21 // Run the scheduler.. this is private just due to the need to synchronize
22 // access to the 'running' state variable.
23 func (c *Cron) run() {
24     // Figure out the next activation times for each entry.
25     now := time.Now().In(c.location)
26     for _, entry := range c.entries {
27         entry.Next = entry.Schedule.Next(now)
28     }
29         // Infinite cycle
30     for {
31             //By sorting the next execution time, determine which tasks will be executed next time, and guard against the front of the queue.sort It's used for sorting.
32         sort.Sort(byTime(c.entries))
33 
34         var effective time.Time
35         if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
36             // If there are no entries yet, just sleep - it still handles new entries
37             // and stop requests.
38             effective = now.AddDate(10, 0, 0)
39         } else {
40             effective = c.entries[0].Next
41         }
42 
43         timer := time.NewTimer(effective.Sub(now))
44         select {
45         case now = <-timer.C:  // Execute current tasks
46             now = now.In(c.location)
47             // Run every entry whose next time was this effective time.
48             for _, e := range c.entries {
49                 if e.Next != effective {
50                     break
51                 }
52                 go c.runWithRecovery(e.Job)
53                 e.Prev = e.Next
54                 e.Next = e.Schedule.Next(now)
55             }
56             continue
57 
58         case newEntry := <-c.add:  // Add new tasks
59             c.entries = append(c.entries, newEntry)
60             newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location))
61 
62         case <-c.snapshot:  // Take Snapshot
63             c.snapshot <- c.entrySnapshot()
64 
65         case <-c.stop:   // Stop task
66             timer.Stop()
67             return
68         }
69 
70         // 'now' should be updated after newEntry and snapshot cases.
71         now = time.Now().In(c.location)
72         timer.Stop()
73     }
74 }

 

      spec.go

Structures and key methods:

 1 // SpecSchedule specifies a duty cycle (to the second granularity), based on a
 2 // traditional crontab specification. It is computed initially and stored as bit sets.
 3 type SpecSchedule struct {
 4     // Locks in expressions indicate seconds, minutes, hours, days, months, weeks, each of which is uint64
 5     // Dom:Day of Month,Dow:Day of week
 6     Second, Minute, Hour, Dom, Month, Dow uint64
 7 }
 8 
 9 // bounds provides a range of acceptable values (plus a map of name to value).
10 // The structure of an expression is defined.
11 type bounds struct {
12     min, max uint
13     names    map[string]uint
14 }
15 
16 
17 // The bounds for each field.
18 // So you can see the scope of each expression.
19 var (
20        seconds = bounds{0, 59, nil}
21        minutes = bounds{0, 59, nil}
22        hours   = bounds{0, 23, nil}
23        dom     = bounds{1, 31, nil}
24        months  = bounds{1, 12, map[string]uint{
25               "jan": 1,
26               "feb": 2,
27               "mar": 3,
28               "apr": 4,
29               "may": 5,
30               "jun": 6,
31               "jul": 7,
32               "aug": 8,
33               "sep": 9,
34               "oct": 10,
35               "nov": 11,
36               "dec": 12,
37        }}
38        dow = bounds{0, 6, map[string]uint{
39               "sun": 0,
40               "mon": 1,
41               "tue": 2,
42               "wed": 3,
43               "thu": 4,
44               "fri": 5,
45               "sat": 6,
46        }}
47 )
48 
49 const (
50        // Set the top bit if a star was included in the expression.
51        starBit = 1 << 63
52 )

 

Looking at the above, one must wonder why these definitions are unit64 and a constant starBit = 1 << 63, which is a logical operator. Represents that binary 1 moves 63 bits to the left. The reasons are as follows:

Cron expression is used to express a series of time, and time can not escape its own interval, minute, second 0 - 59, hour 0 - 23, day / month 0 - 31, day / week 0 - 6, month 0 - 11. These are essentially a set of points, or an integer interval. Then for any integer interval, the following rules of cron can be described.

  • *| Arbitrary, corresponding to all points in the interval. (Pay extra attention to day/week, day/month interference.)
  • Pure numbers correspond to a specific point.
  • / The two digits a, b, which are divided, correspond to all points of a + n * b on the interval (n >= 0).
  • - The two digits divided correspond to all points in the interval determined by the two digits.
  • L | W needs to make special judgments about specific time, which can not be used to correspond to the points on the interval.

 

So far, the reason why robfig/cron does not support L | W is clear. After removing these two rules, the rest of the rules can be expressed by exhaustion of points. Considering that the maximum interval is only 60 points, it is appropriate to use each bit of an uint64 integer to represent a point. So it's not too much to define unit64.

The following is the method of cron expression in go:

/* 
   ------------------------------------------------------------
   The 64-bit mark is arbitrary and is used for day/week and day/month interference.
   63 - 0 represents each point of the interval [63, 0].
   ------------------------------------------------------------ 

   Assuming that the interval is 0 - 63, there are the following examples: 

   For example, 0/3 is expressed as follows: (1 every two)
   * / ?       
   +---+--------------------------------------------------------+
   | 0 | 1 0 0 1 0 0 1  ~~  ~~                    1 0 0 1 0 0 1 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 

   For example, 2-5 is expressed as follows: (1) from right to left, 2-5 bits are all 1)
   * / ?       
   +---+--------------------------------------------------------+
   | 0 | 0 0 0 0 ~  ~      ~~            ~    0 0 0 1 1 1 1 0 0 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 

  For example, * is expressed as follows: (1 for all positions)
   * / ?       
   +---+--------------------------------------------------------+
   | 1 | 1 1 1 1 1 ~  ~                  ~    1 1 1 1 1 1 1 1 1 |
   +---+--------------------------------------------------------+   
        63 ~ ~                                           ~~ 0 
*/

  parser.go

Resolve strings into SpecSchedule classes.

 

  

  1 package cron
  2 
  3 import (
  4     "fmt"
  5     "math"
  6     "strconv"
  7     "strings"
  8     "time"
  9 )
 10 
 11 // Configuration options for creating a parser. Most options specify which
 12 // fields should be included, while others enable features. If a field is not
 13 // included the parser will assume a default value. These options do not change
 14 // the order fields are parse in.
 15 type ParseOption int
 16 
 17 const (
 18     Second      ParseOption = 1 << iota // Seconds field, default 0
 19     Minute                              // Minutes field, default 0
 20     Hour                                // Hours field, default 0
 21     Dom                                 // Day of month field, default *
 22     Month                               // Month field, default *
 23     Dow                                 // Day of week field, default *
 24     DowOptional                         // Optional day of week field, default *
 25     Descriptor                          // Allow descriptors such as @monthly, @weekly, etc.
 26 )
 27 
 28 var places = []ParseOption{
 29     Second,
 30     Minute,
 31     Hour,
 32     Dom,
 33     Month,
 34     Dow,
 35 }
 36 
 37 var defaults = []string{
 38     "0",
 39     "0",
 40     "0",
 41     "*",
 42     "*",
 43     "*",
 44 }
 45 
 46 // A custom Parser that can be configured.
 47 type Parser struct {
 48     options   ParseOption
 49     optionals int
 50 }
 51 
 52 // Creates a custom Parser with custom options.
 53 //
 54 //  // Standard parser without descriptors
 55 //  specParser := NewParser(Minute | Hour | Dom | Month | Dow)
 56 //  sched, err := specParser.Parse("0 0 15 */3 *")
 57 //
 58 //  // Same as above, just excludes time fields
 59 //  subsParser := NewParser(Dom | Month | Dow)
 60 //  sched, err := specParser.Parse("15 */3 *")
 61 //
 62 //  // Same as above, just makes Dow optional
 63 //  subsParser := NewParser(Dom | Month | DowOptional)
 64 //  sched, err := specParser.Parse("15 */3")
 65 //
 66 func NewParser(options ParseOption) Parser {
 67     optionals := 0
 68     if options&DowOptional > 0 {
 69         options |= Dow
 70         optionals++
 71     }
 72     return Parser{options, optionals}
 73 }
 74 
 75 // Parse returns a new crontab schedule representing the given spec.
 76 // It returns a descriptive error if the spec is not valid.
 77 // It accepts crontab specs and features configured by NewParser.
 78 // Resolve strings into SpecSchedule .  SpecSchedule accord with Schedule Interface
 79 
 80 func (p Parser) Parse(spec string) (Schedule, error) {
 81   // Direct processing of special strings
 82     if spec[0] == '@' && p.options&Descriptor > 0 {
 83         return parseDescriptor(spec)
 84     }
 85 
 86     // Figure out how many fields we need
 87     max := 0
 88     for _, place := range places {
 89         if p.options&place > 0 {
 90             max++
 91         }
 92     }
 93     min := max - p.optionals
 94 
 95     // cron Use blanks to disassemble independent items. 
 96     fields := strings.Fields(spec)
 97 
 98     // Validation expression range
 99     if count := len(fields); count < min || count > max {
100         if min == max {
101             return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
102         }
103         return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
104     }
105 
106     // Fill in missing fields
107     fields = expandFields(fields, p.options)
108 
109     var err error
110     field := func(field string, r bounds) uint64 {
111         if err != nil {
112             return 0
113         }
114         var bits uint64
115         bits, err = getField(field, r)
116         return bits
117     }
118 
119     var (
120         second     = field(fields[0], seconds)
121         minute     = field(fields[1], minutes)
122         hour       = field(fields[2], hours)
123         dayofmonth = field(fields[3], dom)
124         month      = field(fields[4], months)
125         dayofweek  = field(fields[5], dow)
126     )
127     if err != nil {
128         return nil, err
129     }
130     // Return the required SpecSchedule
131     return &SpecSchedule{
132         Second: second,
133         Minute: minute,
134         Hour:   hour,
135         Dom:    dayofmonth,
136         Month:  month,
137         Dow:    dayofweek,
138     }, nil
139 }
140 
141 func expandFields(fields []string, options ParseOption) []string {
142     n := 0
143     count := len(fields)
144     expFields := make([]string, len(places))
145     copy(expFields, defaults)
146     for i, place := range places {
147         if options&place > 0 {
148             expFields[i] = fields[n]
149             n++
150         }
151         if n == count {
152             break
153         }
154     }
155     return expFields
156 }
157 
158 var standardParser = NewParser(
159     Minute | Hour | Dom | Month | Dow | Descriptor,
160 )
161 
162 // ParseStandard returns a new crontab schedule representing the given standardSpec
163 // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
164 // pass 5 entries representing: minute, hour, day of month, month and day of week,
165 // in that order. It returns a descriptive error if the spec is not valid.
166 //
167 // It accepts
168 //   - Standard crontab specs, e.g. "* * * * ?"
169 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
170 // This means that not only can you use it cron Expressions can also be used@midnight @every Other methods
171 
172 func ParseStandard(standardSpec string) (Schedule, error) {
173     return standardParser.Parse(standardSpec)
174 }
175 
176 var defaultParser = NewParser(
177     Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
178 )
179 
180 // Parse returns a new crontab schedule representing the given spec.
181 // It returns a descriptive error if the spec is not valid.
182 //
183 // It accepts
184 //   - Full crontab specs, e.g. "* * * * * ?"
185 //   - Descriptors, e.g. "@midnight", "@every 1h30m"
186 func Parse(spec string) (Schedule, error) {
187     return defaultParser.Parse(spec)
188 }
189 
190 // getField returns an Int with the bits set representing all of the times that
191 // the field represents or error parsing field value.  A "field" is a comma-separated
192 // list of "ranges".
193 func getField(field string, r bounds) (uint64, error) {
194     var bits uint64
195     ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
196     for _, expr := range ranges {
197         bit, err := getRange(expr, r)
198         if err != nil {
199             return bits, err
200         }
201         bits |= bit
202     }
203     return bits, nil
204 }
205 
206 // getRange returns the bits indicated by the given expression:
207 //   number | number "-" number [ "/" number ]
208 // or error parsing range.
209 func getRange(expr string, r bounds) (uint64, error) {
210     var (
211         start, end, step uint
212         rangeAndStep     = strings.Split(expr, "/")
213         lowAndHigh       = strings.Split(rangeAndStep[0], "-")
214         singleDigit      = len(lowAndHigh) == 1
215         err              error
216     )
217 
218     var extra uint64
219     if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
220         start = r.min
221         end = r.max
222         extra = starBit
223     } else {
224         start, err = parseIntOrName(lowAndHigh[0], r.names)
225         if err != nil {
226             return 0, err
227         }
228         switch len(lowAndHigh) {
229         case 1:
230             end = start
231         case 2:
232             end, err = parseIntOrName(lowAndHigh[1], r.names)
233             if err != nil {
234                 return 0, err
235             }
236         default:
237             return 0, fmt.Errorf("Too many hyphens: %s", expr)
238         }
239     }
240 
241     switch len(rangeAndStep) {
242     case 1:
243         step = 1
244     case 2:
245         step, err = mustParseInt(rangeAndStep[1])
246         if err != nil {
247             return 0, err
248         }
249 
250         // Special handling: "N/step" means "N-max/step".
251         if singleDigit {
252             end = r.max
253         }
254     default:
255         return 0, fmt.Errorf("Too many slashes: %s", expr)
256     }
257 
258     if start < r.min {
259         return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
260     }
261     if end > r.max {
262         return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
263     }
264     if start > end {
265         return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
266     }
267     if step == 0 {
268         return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
269     }
270 
271     return getBits(start, end, step) | extra, nil
272 }
273 
274 // parseIntOrName returns the (possibly-named) integer contained in expr.
275 func parseIntOrName(expr string, names map[string]uint) (uint, error) {
276     if names != nil {
277         if namedInt, ok := names[strings.ToLower(expr)]; ok {
278             return namedInt, nil
279         }
280     }
281     return mustParseInt(expr)
282 }
283 
284 // mustParseInt parses the given expression as an int or returns an error.
285 func mustParseInt(expr string) (uint, error) {
286     num, err := strconv.Atoi(expr)
287     if err != nil {
288         return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
289     }
290     if num < 0 {
291         return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
292     }
293 
294     return uint(num), nil
295 }
296 
297 // getBits sets all bits in the range [min, max], modulo the given step size.
298 func getBits(min, max, step uint) uint64 {
299     var bits uint64
300 
301     // If step is 1, use shifts.
302     if step == 1 {
303         return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
304     }
305 
306     // Else, use a simple loop.
307     for i := min; i <= max; i += step {
308         bits |= 1 << i
309     }
310     return bits
311 }
312 
313 // all returns all bits within the given bounds.  (plus the star bit)
314 func all(r bounds) uint64 {
315     return getBits(r.min, r.max, 1) | starBit
316 }
317 
318 // parseDescriptor returns a predefined schedule for the expression, or error if none matches.
319 func parseDescriptor(descriptor string) (Schedule, error) {
320     switch descriptor {
321     case "@yearly", "@annually":
322         return &SpecSchedule{
323             Second: 1 << seconds.min,
324             Minute: 1 << minutes.min,
325             Hour:   1 << hours.min,
326             Dom:    1 << dom.min,
327             Month:  1 << months.min,
328             Dow:    all(dow),
329         }, nil
330 
331     case "@monthly":
332         return &SpecSchedule{
333             Second: 1 << seconds.min,
334             Minute: 1 << minutes.min,
335             Hour:   1 << hours.min,
336             Dom:    1 << dom.min,
337             Month:  all(months),
338             Dow:    all(dow),
339         }, nil
340 
341     case "@weekly":
342         return &SpecSchedule{
343             Second: 1 << seconds.min,
344             Minute: 1 << minutes.min,
345             Hour:   1 << hours.min,
346             Dom:    all(dom),
347             Month:  all(months),
348             Dow:    1 << dow.min,
349         }, nil
350 
351     case "@daily", "@midnight":
352         return &SpecSchedule{
353             Second: 1 << seconds.min,
354             Minute: 1 << minutes.min,
355             Hour:   1 << hours.min,
356             Dom:    all(dom),
357             Month:  all(months),
358             Dow:    all(dow),
359         }, nil
360 
361     case "@hourly":
362         return &SpecSchedule{
363             Second: 1 << seconds.min,
364             Minute: 1 << minutes.min,
365             Hour:   all(hours),
366             Dom:    all(dom),
367             Month:  all(months),
368             Dow:    all(dow),
369         }, nil
370     }
371 
372     const every = "@every "
373     if strings.HasPrefix(descriptor, every) {
374         duration, err := time.ParseDuration(descriptor[len(every):])
375         if err != nil {
376             return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
377         }
378         return Every(duration), nil
379     }
380 
381     return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
382 }

 

Project application

   

package main

import (
    "github.com/robfig/cron"
    "log"
)

func main() {
    i := 0
    c := cron.New()
    spec := "*/5 * * * * ?"
    c.AddFunc(spec, func() {
        i++
        log.Println("cron running:", i)
    })
    c.AddFunc("@every 1h1m", func() {
        i++
        log.Println("cron running:", i)
    })
    c.Start()
}

Note: @every is a special usage, which is a characteristic usage in Go. The same goes for @yearly@annually@monthly@weekly@daily@midnight@hourly. I hope you can explore for yourselves.

  

Reference website:

http://blog.studygolang.com/2014/02/go_crontab/

http://blog.csdn.net/cchd0001/article/details/51076922

https://en.wikipedia.org/wiki/Cron

https://github.com/robfig/cron

Posted by liquidmind on Wed, 20 Mar 2019 09:51:28 -0700