Details of Go timer cron

  • 2020-06-15 09:14:28
  • OfStack

What is cron

cron means to plan tasks, or to schedule tasks. I make an appointment with the system, and you run one task (job) every few minutes or minutes, and that's it.

cron expression

The cron expression is a good thing to use not only in Java's quartZ, but also in the Go language. I haven't used cron with Linux, but the web says Linux can also be configured with the ES18en-ES19en command. Both Go and Java are accurate to seconds, but not Linux.

The cron expression represents a collection of 1 time, represented by 6 space-separated fields:

字段名 是否必须 允许的值  允许的特定字符
秒(Seconds) 0-59 * / , -
分(Minute) 0-59 * / , -
时(Hours) 0-23 * / , -
日(Day of month) 1-31 * / , - ?
月(Month) 1-12 或 JAN-DEC * / , -
星期(Day of week) 0-6 或 SUM-SAT * / , - ?

1. The values of the month (Month) and week (Day of week) fields are case-insensitive, as SUN, Sun and sun are the same.

2. The week (Day of week) field, if not provided, 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 specification

1) Asterisk (*)

Indicates that the cron expression matches all the values of the field. If the asterisk (month) is used in the fifth field, it indicates each month

2) Slash (/)

Represents the growth interval. For example, the value of the first field (minutes) is 3-59/15, which means that the execution starts once every 3 minutes, and then every 15 minutes (i.e., 3, 18, 33, 48), which can also be expressed here as: 3/15

3) Comma (,)

Used to enumerate values, such as the sixth field values MON,WED,FRI, to indicate Monday 1.5 execution

4) Hyphen (-)

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

5) question mark (?)

Used only for days (Day of month) and days (Day of week), indicating that no value is specified and can be used instead of *

6) L, W, #

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

cron for example

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

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

1 execution per day at 23:00:0 0 23 * * ?

Every day at 1 am: 0 0 1 * * ?

Execute once at 1 am on the 1st of every month: 0 0 1 1 * ?

At 26 points,29 points,33 points: 0 26,29,33 * * * ?

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

Download and install

Console input go get github.com/robfig/cron Go download Go for timed tasks, as long as it's yours $GOPATH It's configured

The source code parsing

Description of file directory


constantdelay.go   #1 The simplest second level timing system. with cron Has nothing to do 
constantdelay_test.go # test 
cron.go        #Cron System. management 1 A series of cron Timed task ( Schedule Job ) 
cron_test.go     # test 
doc.go        # documentation 
LICENSE        # Power of attorney  
parser.go       # Parser, parse cron Format string city 1 A specific timer ( Schedule ) 
parser_test.go    # test 
README.md       #README
spec.go        # Single timer ( Schedule ) structure. How to calculate their own under 1 Secondary trigger time 
spec_test.go     # test 

cron.go

Structure:


// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may be started, stopped, and the entries may
// be inspected while running. 
// Cron Keep track of any number of entries, calling the associated func Schedule specification. It can be started, stopped, and checked for entries that can be run simultaneously. 
type Cron struct {
  entries []*Entry      //  task 
  stop   chan struct{}   //  Call the way to stop 
  add   chan *Entry    //  How to add a new task 
  snapshot chan []*Entry   //  How to request a snapshot of a task 
  running bool        //  Is it running 
  ErrorLog *log.Logger    //  Error log ( The new attribute )
  location *time.Location   //  In the area ( The new attribute )    
}

// Entry consists of a schedule and the func to execute on that schedule.
//  Entry includes schedules and those that can be executed on the schedule func
type Entry struct {
    //  The timer 
  Schedule Schedule
  //  Next execution time 
  Next time.Time
  //  Last execution time 
  Prev time.Time
  //  task 
  Job Job
}

Key methods:


//  Start task 
// Start the cron scheduler in its own go-routine, or no-op if already started.
func (c *Cron) Start() {
  if c.running {
    return
  }
  c.running = true
  go c.run()
}
//  End task 
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
func (c *Cron) Stop() {
  if !c.running {
    return
  }
  c.stop <- struct{}{}
  c.running = false
}

//  Perform timed tasks 
// Run the scheduler.. this is private just due to the need to synchronize
// access to the 'running' state variable.
func (c *Cron) run() {
  // Figure out the next activation times for each entry.
  now := time.Now().In(c.location)
  for _, entry := range c.entries {
    entry.Next = entry.Schedule.Next(now)
  }
    //  An infinite loop 
  for {
      // Based on the 1 The execution times are sorted, and which tasks are below 1 Second executed, at the front of the queue .sort It's used for sorting 
    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
    }

    timer := time.NewTimer(effective.Sub(now))
    select {
    case now = <-timer.C: //  Perform the current task 
      now = now.In(c.location)
      // Run every entry whose next time was this effective time.
      for _, e := range c.entries {
        if e.Next != effective {
          break
        }
        go c.runWithRecovery(e.Job)
        e.Prev = e.Next
        e.Next = e.Schedule.Next(now)
      }
      continue

    case newEntry := <-c.add: //  Add a new task 
      c.entries = append(c.entries, newEntry)
      newEntry.Next = newEntry.Schedule.Next(time.Now().In(c.location))

    case <-c.snapshot: //  The snapshots 
      c.snapshot <- c.entrySnapshot()

    case <-c.stop:  //  Stop the task 
      timer.Stop()
      return
    }

    // 'now' should be updated after newEntry and snapshot cases.
    now = time.Now().In(c.location)
    timer.Stop()
  }
}

spec.go

Structure and key methods:


// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
type SpecSchedule struct {
  //  The locks in the expression indicate seconds, minutes, hours, days, months, weeks, each of them uint64
  // Dom:Day of Month,Dow:Day of week
  Second, Minute, Hour, Dom, Month, Dow uint64
}

// bounds provides a range of acceptable values (plus a map of name to value).
//  Defines the structure of the expression 
type bounds struct {
  min, max uint
  names  map[string]uint
}


// The bounds for each field.
//  So you can see the range of each expression 
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,
    }}
)

const (
    // Set the top bit if a star was included in the expression.
    starBit = 1 << 63
)

If you look at all of this stuff up here some of you may be wondering why is it that in seconds these are all defining unit64 and defining a constant starBit = 1 < < This is 63. This is the logical operator. Means that base 2 1 moves 63 bits to the left. The reasons are as follows:

cron expression is used to express the 1 series of time, and time is unable to escape its interval, minutes, seconds 0-59, hours 0-23, days/months 0-31, days/weeks 0-6, months 0-11. These are essentially 1 set of points, or 1 interval of integers. Then for any integer interval, the following partial rule of cron can be described.

* | & # 63; Any of the points on that interval. (Pay extra attention to day/week, day/month interactions.) Pure Numbers, 1 specific point. a, b, all points (n) that correspond to a + n * b on the interval > = 0). - Two separated Numbers corresponding to all points in the interval determined by these two Numbers. L | W needs a special judgment for a specific time, and cannot universally correspond to the points on the interval.

At this point, it is clear why robfig/cron does not support L | W. After removing these two rules, the rest of the rules can actually be generalized using the exhaustive representation 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 an overstatement to define unit64

The following is the method of cron expression in go:


/* 
  ------------------------------------------------------------
   The first 64 Bit mark arbitrary   .   Used for   day / weeks   .   day  /  month   Mutual interference. 
  63 - 0  for   Said interval  [63 , 0]  the   every 1 A point. 
  ------------------------------------------------------------

   Suppose the interval is  0 - 63  .   Here is an example   : 

   Such as  0/3  Is represented as follows   :  ( Means every two digits is 1)
  * / ?    
  +---+--------------------------------------------------------+
  | 0 | 1 0 0 1 0 0 1 ~~ ~~          1 0 0 1 0 0 1 |
  +---+--------------------------------------------------------+  
    63 ~ ~                      ~~ 0

   Such as  2-5  Is represented as follows   :  ( That means from right to left 2-5 All over the place 1)
  * / ?    
  +---+--------------------------------------------------------+
  | 0 | 0 0 0 0 ~ ~   ~~      ~  0 0 0 1 1 1 1 0 0 |
  +---+--------------------------------------------------------+  
    63 ~ ~                      ~~ 0

  Such as  *  Is represented as follows   :  ( That means zero for all of the positions 1)
  * / ?    
  +---+--------------------------------------------------------+
  | 1 | 1 1 1 1 1 ~ ~         ~  1 1 1 1 1 1 1 1 1 |
  +---+--------------------------------------------------------+  
    63 ~ ~                      ~~ 0 
*/

parser.go

The class that parses the string to SpecSchedule.


package cron

import (
  "fmt"
  "math"
  "strconv"
  "strings"
  "time"
)

// Configuration options for creating a parser. Most options specify which
// fields should be included, while others enable features. If a field is not
// included the parser will assume a default value. These options do not change
// the order fields are parse in.
type ParseOption int

const (
  Second   ParseOption = 1 << iota // Seconds field, default 0
  Minute               // Minutes field, default 0
  Hour                // Hours field, default 0
  Dom                 // Day of month field, default *
  Month                // Month field, default *
  Dow                 // Day of week field, default *
  DowOptional             // Optional day of week field, default *
  Descriptor             // Allow descriptors such as @monthly, @weekly, etc.
)

var places = []ParseOption{
  Second,
  Minute,
  Hour,
  Dom,
  Month,
  Dow,
}

var defaults = []string{
  "0",
  "0",
  "0",
  "*",
  "*",
  "*",
}

// A custom Parser that can be configured.
type Parser struct {
  options  ParseOption
  optionals int
}

// Creates a custom Parser with custom options.
//
// // Standard parser without descriptors
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
// sched, err := specParser.Parse("0 0 15 */3 *")
//
// // Same as above, just excludes time fields
// subsParser := NewParser(Dom | Month | Dow)
// sched, err := specParser.Parse("15 */3 *")
//
// // Same as above, just makes Dow optional
// subsParser := NewParser(Dom | Month | DowOptional)
// sched, err := specParser.Parse("15 */3")
//
func NewParser(options ParseOption) Parser {
  optionals := 0
  if options&DowOptional > 0 {
    options |= Dow
    optionals++
  }
  return Parser{options, optionals}
}

// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
// It accepts crontab specs and features configured by NewParser.
//  Parses a string into SpecSchedule  .  SpecSchedule Conform to the Schedule interface 

func (p Parser) Parse(spec string) (Schedule, error) {
  //  Handle special special strings directly 
  if spec[0] == '@' && p.options&Descriptor > 0 {
    return parseDescriptor(spec)
  }

  // Figure out how many fields we need
  max := 0
  for _, place := range places {
    if p.options&place > 0 {
      max++
    }
  }
  min := max - p.optionals

  // cron The blank space is used to disassemble the independent ones items . 
  fields := strings.Fields(spec)

  //  Verify the expression value range 
  if count := len(fields); count < min || count > max {
    if min == max {
      return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)
    }
    return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)
  }

  // Fill in missing fields
  fields = expandFields(fields, p.options)

  var err error
  field := func(field string, r bounds) uint64 {
    if err != nil {
      return 0
    }
    var bits uint64
    bits, err = getField(field, r)
    return bits
  }

  var (
    second   = field(fields[0], seconds)
    minute   = field(fields[1], minutes)
    hour    = field(fields[2], hours)
    dayofmonth = field(fields[3], dom)
    month   = field(fields[4], months)
    dayofweek = field(fields[5], dow)
  )
  if err != nil {
    return nil, err
  }
  //  Returns what is needed SpecSchedule
  return &SpecSchedule{
    Second: second,
    Minute: minute,
    Hour:  hour,
    Dom:  dayofmonth,
    Month: month,
    Dow:  dayofweek,
  }, nil
}

func expandFields(fields []string, options ParseOption) []string {
  n := 0
  count := len(fields)
  expFields := make([]string, len(places))
  copy(expFields, defaults)
  for i, place := range places {
    if options&place > 0 {
      expFields[i] = fields[n]
      n++
    }
    if n == count {
      break
    }
  }
  return expFields
}

var standardParser = NewParser(
  Minute | Hour | Dom | Month | Dow | Descriptor,
)

// ParseStandard returns a new crontab schedule representing the given standardSpec
// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
// pass 5 entries representing: minute, hour, day of month, month and day of week,
// in that order. It returns a descriptive error if the spec is not valid.
//
// It accepts
//  - Standard crontab specs, e.g. "* * * * ?"
//  - Descriptors, e.g. "@midnight", "@every 1h30m"
//  It's not just that you can use it cron Expressions can also be used @midnight @every Methods such as 

func ParseStandard(standardSpec string) (Schedule, error) {
  return standardParser.Parse(standardSpec)
}

var defaultParser = NewParser(
  Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,
)

// Parse returns a new crontab schedule representing the given spec.
// It returns a descriptive error if the spec is not valid.
//
// It accepts
//  - Full crontab specs, e.g. "* * * * * ?"
//  - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) (Schedule, error) {
  return defaultParser.Parse(spec)
}

// getField returns an Int with the bits set representing all of the times that
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
  var bits uint64
  ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
  for _, expr := range ranges {
    bit, err := getRange(expr, r)
    if err != nil {
      return bits, err
    }
    bits |= bit
  }
  return bits, nil
}

// getRange returns the bits indicated by the given expression:
//  number | number "-" number [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
  var (
    start, end, step uint
    rangeAndStep   = strings.Split(expr, "/")
    lowAndHigh    = strings.Split(rangeAndStep[0], "-")
    singleDigit   = len(lowAndHigh) == 1
    err       error
  )

  var extra uint64
  if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
    start = r.min
    end = r.max
    extra = starBit
  } else {
    start, err = parseIntOrName(lowAndHigh[0], r.names)
    if err != nil {
      return 0, err
    }
    switch len(lowAndHigh) {
    case 1:
      end = start
    case 2:
      end, err = parseIntOrName(lowAndHigh[1], r.names)
      if err != nil {
        return 0, err
      }
    default:
      return 0, fmt.Errorf("Too many hyphens: %s", expr)
    }
  }

  switch len(rangeAndStep) {
  case 1:
    step = 1
  case 2:
    step, err = mustParseInt(rangeAndStep[1])
    if err != nil {
      return 0, err
    }

    // Special handling: "N/step" means "N-max/step".
    if singleDigit {
      end = r.max
    }
  default:
    return 0, fmt.Errorf("Too many slashes: %s", expr)
  }

  if start < r.min {
    return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
  }
  if end > r.max {
    return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
  }
  if start > end {
    return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
  }
  if step == 0 {
    return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)
  }

  return getBits(start, end, step) | extra, nil
}

// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
  if names != nil {
    if namedInt, ok := names[strings.ToLower(expr)]; ok {
      return namedInt, nil
    }
  }
  return mustParseInt(expr)
}

// mustParseInt parses the given expression as an int or returns an error.
func mustParseInt(expr string) (uint, error) {
  num, err := strconv.Atoi(expr)
  if err != nil {
    return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
  }
  if num < 0 {
    return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
  }

  return uint(num), nil
}

// getBits sets all bits in the range [min, max], modulo the given step size.
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
}

// all returns all bits within the given bounds. (plus the star bit)
func all(r bounds) uint64 {
  return getBits(r.min, r.max, 1) | starBit
}

// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
func parseDescriptor(descriptor string) (Schedule, error) {
  switch descriptor {
  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),
    }, nil

  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),
    }, nil

  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,
    }, nil

  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),
    }, nil

  case "@hourly":
    return &SpecSchedule{
      Second: 1 << seconds.min,
      Minute: 1 << minutes.min,
      Hour:  all(hours),
      Dom:  all(dom),
      Month: all(months),
      Dow:  all(dow),
    }, nil
  }

  const every = "@every "
  if strings.HasPrefix(descriptor, every) {
    duration, err := time.ParseDuration(descriptor[len(every):])
    if err != nil {
      return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
    }
    return Every(duration), nil
  }

  return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
}

Application in project


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: the usage of @every is quite special. This is the usage of Go. And the same thing with @yearly @annually @monthly @weekly @daily @midnight @hourly and I won't go into that. I want you to explore on your own.


Related articles: