Introduction to the

Cron a library for managing scheduled tasks. Use Go to implement the crontab command in Linux. A similar Go library, Gron, was introduced earlier. Gron code is small and good for learning. But it is relatively simple and no longer maintained. Cron is recommended for scheduled tasks.

Quick to use

The text code uses Go Modules.

Create directory and initialize:

$ mkdir cron && cd cron
$ go mod init github.com/darjun/go-daily-lib/cron
Copy the code

Install cron. The latest stable version is V3:

$ go get -u github.com/robfig/cron/v3
Copy the code

Use:

package main

import (
  "fmt"
  "time"

  "github.com/robfig/cron/v3"
)

func main(a) {
  c := cron.New()

  c.AddFunc("@every 1s".func(a) {
    fmt.Println("tick every 1 second")
  })

  c.Start()
  time.Sleep(time.Second * 5)}Copy the code

As simple as this, create a CRon object that manages scheduled tasks.

Call the AddFunc() method of the cron object to add a scheduled task to the manager. AddFunc() takes two arguments, parameter 1 specifying the firing time rule as a string, and parameter 2 is a no-parameter function that is called each time it is fired. @every 1s indicates that it is triggered once every second, and @every is followed by a time interval indicating how often it is triggered. For example, @every 1h indicates that the alarm is triggered every hour, and @every 1m2s indicates that the alarm is triggered every 1 minute and 2 seconds. Any format supported by time.parseDuration () can be used here.

Call c.start () to start the timing loop.

Note that since C.start () starts a new goroutine for loop detection, we add a line at the end of the code to time.sleep (time.second * 5) to prevent the main goroutine from exiting.

Run effect, output a string every 1s:

$ go run main.go 
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
tick every 1 second
Copy the code

Time format

Similar to the Crontab command in Linux, the Cron library supports five space-separated fields to indicate the time. The meanings of these five domains are as follows:

  • Minutes: Minutes: indicates the value range[0] 59 -Special characters are supported- * /;
  • Hours: Hour: indicates the value range[23] 0 -Special characters are supported- * /;
  • Day of month: Day of a month, value range[1-31]Special characters are supported* /, -?;
  • Month: month: indicates the value range[1-12]Or use month abbreviations[JAN-DEC]Special characters are supported- * /;
  • Day of week: Weekly calendar, value range[0-6]Or initials[JUN-SAT]Special characters are supported* /, -?.

Note that both month and calendar names are case insensitive, meaning that SUN/ SUN/ SUN means the same thing (both are Sundays).

The meanings of special characters are as follows:

  • *Use:*Can match any value, such as setting the month field (fourth) to*, represents every month.
  • /: used to specify a rangeStep length, for example, set the small time domain (second) to3-59/15The second trigger is triggered in the 18th minute, and the third trigger is triggered in 33 minutes… Until the minutes are greater than 59;
  • .: is used to enumerate discrete values and multiple ranges, such as setting the field of the calendar (fifth) toMON,WED,FRIRepresents Monday, Wednesday, and Friday;
  • -: is used to represent ranges, such as setting the field of hours (first) to9-17Indicates 9 a.m. to 17 p.m. (including 9 and 17);
  • ?: can be used only in the fields of monthly and weekly calendars*Is any day in a month or a week.

Knowing the rules, we can define any time:

  • 30 * * * *: The value is 30 in the minute domain*For any. Trigger at 30 minutes per hour;
  • 30 3-6,20-23 * * *: minute domain is 30, small time domain3-6, 20-23It means 3 to 6 and 20 to 23. Triggered at 30 minutes at 3,4,5,6,20,21,22,23;
  • 0, 0, 1, 1 star: 1 (4th) month 1 (3rd) 0 (2nd) 0 (1st) minute triggered.

Once you have memorized the order of these fields, you can easily master the format with a few more practice sessions. Once you’re familiar with the rules, you’ll be comfortable with the crontab command.

func main(a) {
  c := cron.New()

  c.AddFunc("30 * * * *".func(a) {
    fmt.Println("Every hour on the half hour")
  })

  c.AddFunc("30 3-6,20-23 * * *".func(a) {
    fmt.Println("On the half hour of 3-6am, 8-11pm")
  })

  c.AddFunc("0 0 1 1 *".func(a) {
    fmt.Println("Jun 1 every year")
  })

  c.Start()

  for {
    time.Sleep(time.Second)
  }
}
Copy the code

Predefined time rules

For ease of use, Cron predefined some time rules:

  • @yearly: You can also write@annuallyIs 0 o ‘clock on the first day of each year. Is equivalent to0, 0, 1, 1 star;
  • @monthly: indicates 0 o ‘clock on the first day of each month. Is equivalent to0, 0, 1 * *;
  • @weekly: indicates 0 on the first day of each week. Note that the first day is Sunday, that is, the end of Saturday and the beginning of Sunday. Is equivalent to0, 0 * * 0;
  • @daily: You can also write@midnightIs 0 o ‘clock every day. Is equivalent to0 0 * * *;
  • @hourly: indicates the start of an hour. Is equivalent to0 * * * *.

Such as:

func main(a) {
  c := cron.New()

  c.AddFunc("@hourly".func(a) {
    fmt.Println("Every hour")
  })

  c.AddFunc("@daily".func(a) {
    fmt.Println("Every day on midnight")
  })

  c.AddFunc("@weekly".func(a) {
    fmt.Println("Every week")
  })

  c.Start()

  for {
    time.Sleep(time.Second)
  }
}
Copy the code

The code above is just a demonstration; the actual run may take a very long time to produce output.

Fixed time interval

Cron supports fixed time intervals in the following format:

@every <duration>
Copy the code

Trigger every duration.

will be parsed by calling time.parseDuration (), so any format supported by ParseDuration will work. For example, 1 h30m10s. In the quick start section, we demonstrated the use of @every, so we don’t need to go over it here.

The time zone

By default, all times are based on the current time zone. Of course, we can also specify the time zone, there are two ways:

  • Add one before the time stringCRON_TZ=+ Specific time zone. The format of the specific time zone is earliercarbon“Is detailed in the article. Tokyo time zoneAsia/Tokyo, the New York time zone isAmerica/New_York;
  • createcronObject to add a time zone optioncron.WithLocation(location).locationfortime.LoadLocation(zone)Loaded time zone object,zoneThe value is in the specific time zone format. Or call an already created onecronThe object’sSetLocation()Method to set the time zone.

Example:

func main(a) {
  nyc, _ := time.LoadLocation("America/New_York")
  c := cron.New(cron.WithLocation(nyc))
  c.AddFunc("0 6 * * ?".func(a) {
    fmt.Println("Every 6 o'clock at New York")
  })

  c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?".func(a) {
    fmt.Println("Every 6 o'clock at Tokyo")
  })

  c.Start()

  for {
    time.Sleep(time.Second)
  }
}
Copy the code

Jobinterface

In addition to directly using no-argument functions as callbacks, Cron also supports the Job interface:

// cron.go
type Job interface {
  Run()
}
Copy the code

We define a structure that implements the interface Job:

type GreetingJob struct {
  Name string
}

func (g GreetingJob) Run(a) {
  fmt.Println("Hello ", g.Name)
}
Copy the code

Add the GreetingJob object to the timing manager by calling the AddJob() method of the cron object:

func main(a) {
  c := cron.New()
  c.AddJob("@every 1s", GreetingJob{"dj"})
  c.Start()

  time.Sleep(5 * time.Second)
}
Copy the code

Operation effect:

$ go run main.go 
Hello  dj
Hello  dj
Hello  dj
Hello  dj
Hello  dj
Copy the code

Using a custom structure allows tasks to carry status (the Name field).

The AddJob() method is actually called inside the AddFunc() method. First, Cron defines a new type FuncJob based on the func() type:

// cron.go
type FuncJob func(a)
Copy the code

Then let FuncJob implement the Job interface:

// cron.go
func (f FuncJob) Run(a) {
  f()
}
Copy the code

In the AddFunc() method, pass the callback to type FuncJob, and then call the AddJob() method:

func (c *Cron) AddFunc(spec string, cmd func(a)) (EntryID, error) {
  return c.AddJob(spec, FuncJob(cmd))
}
Copy the code

Thread safety

Cron creates a new Goroutine to perform the trigger callback. If these callbacks require concurrent access to some resources or data, we need to synchronize them explicitly.

Custom time format

Cron supports flexible time formats. If the default format does not meet your requirements, you can define your own time format. The time rule string is parsed by the Cron.parser object. Let’s take a look at how the default parser works.

Define each domain first:

// parser.go
const (
  Second         ParseOption = 1 << iota
  SecondOptional                        
  Minute                                
  Hour                                  
  Dom                                   
  Month                                 
  Dow                                   
  DowOptional                           
  Descriptor                            
)
Copy the code

In addition to Minute, Hour, Dom(Day of Month), month, and Dow(Day of Week), Second is supported. The relative order is fixed:

// parser.go
var places = []ParseOption{
  Second,
  Minute,
  Hour,
  Dom,
  Month,
  Dow,
}

var defaults = []string{
  "0"."0"."0"."*"."*"."*",}Copy the code

The default time format uses five fields.

We can call Cron.newParser () to create our own Parser object, passing in bit-format which fields to use. For example, the following Parser uses six fields and supports Second:

parser := cron.NewParser(
  cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)
Copy the code

Call cron.withParser (parser) to create an option passed to the constructor cron.new (), which can be used to specify seconds:

c := cron.New(cron.WithParser(parser))
c.AddFunc("1 * * * * *".func (a) {
  fmt.Println("every 1 second")
})
c.Start()
Copy the code

Here the time format must use six fields, in the same order as the const definition above.

Because the above time format is so common, Cron defines a handy function:

// option.go
func WithSeconds(a) Option {
  return WithParser(NewParser(
    Second | Minute | Hour | Dom | Month | Dow | Descriptor,
  ))
}
Copy the code

Note that Descriptor means support for @every, @hour, etc. With WithSeconds(), we don’t have to create the Parser object manually:

c := cron.New(cron.WithSeconds())
Copy the code

options

Cron object creation uses the option mode, and we have already described three options:

  • WithLocation: Specifies the time zone.
  • WithParser: Use a custom parser;
  • WithSeconds: Let the time format support seconds, actually called internallyWithParser.

Cron also offers two other options:

  • WithLogger: customLogger;
  • WithChain: Job wrapper.

WithLogger

WithLogger can be set up inside cron to use our custom Logger:

func main(a) {
  c := cron.New(
    cron.WithLogger(
      cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
  c.AddFunc("@every 1s".func(a) {
    fmt.Println("hello world")
  })
  c.Start()

  time.Sleep(5 * time.Second)
}
Copy the code

The call to cron.verbosprintFlogger () wraps the log.logger, which details the scheduling process inside cron:

$ go run main.go
cron: 2020/06/26 07:09:14 start
cron: 2020/06/26 07:09:14 schedule.now= 2020-06-26T07: 09:14 + 08:00,entry= 1,next= 2020-06-26T07: 09:15 + 08:00cron: 2020/06/26 07:09:15 wake.now= 2020-06-26T07: 09:15 + 08:00cron: 2020/06/26 07:09:15 run.now= 2020-06-26T07: 09:15 + 08:00,entry= 1,next= 2020-06-26T07: 09:16 + 08:00hello world
cron: 2020/06/26 07:09:16 wake.now= 2020-06-26T07: 09:16 + 08:00cron: 2020/06/26 07:09:16 run.now= 2020-06-26T07: 09:16 + 08:00,entry= 1,next= 2020-06-26T07: 09:17 + 08:00hello world
cron: 2020/06/26 07:09:17 wake.now= 2020-06-26T07: 09:17 + 08:00cron: 2020/06/26 07:09:17 run.now= 2020-06-26T07: 09:17 + 08:00,entry= 1,next= 2020-06-26T07: 09:18 + 08:00hello world
cron: 2020/06/26 07:09:18 wake.now= 2020-06-26T07: 09:18 + 08:00hello world
cron: 2020/06/26 07:09:18 run.now= 2020-06-26T07: 09:18 + 08:00,entry= 1,next= 2020-06-26T07: 09:19 + 08:00cron: 2020/06/26 07:09:19 wake.now= 2020-06-26T07: 09:19 + 08:00hello world
cron: 2020/06/26 07:09:19 run.now= 2020-06-26T07: 09:19 + 08:00,entry= 1,next= 2020-06-26T07: 09:20 + 08:0Copy the code

Let’s see what the default Logger looks like:

// logger.go
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))

func PrintfLogger(l interface{ Printf(string.interface{}) }) Logger {
  return printfLogger{l, false}}func VerbosePrintfLogger(l interface{ Printf(string.interface{}) }) Logger {
  return printfLogger{l, true}}type printfLogger struct {
  logger  interface{ Printf(string.interface{}) }
  logInfo bool
}
Copy the code

WithChain

The Job wrapper can add some logic before and after executing the actual Job:

  • capturepanic;
  • ifJobThe last operation is not finished, the execution of this time is postponed;
  • ifJobIf the last run is not introduced, skip this run.
  • Record eachJobThe implementation of.

We can think of Chain as middleware for a Web processor. Encapsulate a layer of logic around the Job’s execution logic. Our encapsulation logic needs to be written as a function that passes in a Job type and returns the encapsulated Job. Cron defines a type JobWrapper for this function:

// chain.go
type JobWrapper func(Job) Job
Copy the code

These JobWrappers are then grouped together using a Chain object:

type Chain struct {
  wrappers []JobWrapper
}

func NewChain(c ... JobWrapper) Chain {
  return Chain{c}
}
Copy the code

Call the Chain’s Then(job) method to apply the JobWrapper and return the final ‘job’ :

func (c Chain) Then(j Job) Job {
  for i := range c.wrappers {
    j = c.wrappers[len(c.wrappers)-i- 1](j)
  }
  return j
}
Copy the code

Note the order in which JobWrapper is applied.

built-inJobWrapper

Cron has three of the most used JobWrapper wrapper built in:

  • Recover: Capture the interiorJobPanic;
  • DelayIfStillRunning: If the last task is not completed (it takes too long), the system waits until the last task is completed.
  • SkipIfStillRunning: If the last task is not completed, the execution of this task is skipped.

The following are introduced respectively.

Recover

Here’s how to use it:

type panicJob struct {
  count int
}

func (p *panicJob) Run(a) {
  p.count++
  if p.count == 1 {
    panic("oooooooooooooops!!!")
  }

  fmt.Println("hello world")}func main(a) {
  c := cron.New()
  c.AddJob("@every 1s", cron.NewChain(cron.Recover(cron.DefaultLogger)).Then(&panicJob{}))
  c.Start()

  time.Sleep(5 * time.Second)
}
Copy the code

When panicJob is triggered for the first time, panic is triggered. Because of the cron.recover () protection, subsequent tasks can be performed:

go run main.go 
cron: 2020/06/27 14:02:00 panic.error=oooooooooooooops!!!!!!!!! .stack=...
goroutine 18 [running] :github.com/robfig/cron/v3.Recover.func1. 1.1 (0x514ee0. 0xc0000044a0)
        D: /code/golang/pkg/mod/github.com/robfig/cron/v3@v30.1 /.chain.go: 45 + 0xbc
panic(0x4cf380. 0x513280)
        C: /Go/src/runtime/panic.go: 969 + 0x174
main. (*panicJob).Run(0xc0000140e8)
        D: /code/golang/src/github.com/darjun/go-daily-lib/cron/recover/main.go: 17 + 0xba
github.com/robfig/cron/v3.Recover.func11 ()D: /code/golang/pkg/mod/github.com/robfig/cron/v3@v30.1 /.chain.go: 53 + 0x6f
github.com/robfig/cron/v3.FuncJob.Run(0xc000070390)
        D: /code/golang/pkg/mod/github.com/robfig/cron/v3@v30.1 /.cron.go: 136 + 0x2c
github.com/robfig/cron/v3. (*Cron).startJob.func1(0xc00005c0a0. 0x514d20. 0xc000070390)
        D: /code/golang/pkg/mod/github.com/robfig/cron/v3@v30.1 /.cron.go: 312 + 0x68
created by github.com/robfig/cron/v3. (*Cron).startJob
        D: /code/golang/pkg/mod/github.com/robfig/cron/v3@v30.1 /.cron.go: 310 + 0x7a
hello world
hello world
hello world
hello world
Copy the code

Let’s look at the implementation of cron.recover (), which is very simple:

// cron.go
func Recover(logger Logger) JobWrapper {
  return func(j Job) Job {
    return FuncJob(func(a) {
      defer func(a) {
        if r := recover(a); r ! =nil {
          const size = 64 << 10
          buf := make([]byte, size)
          buf = buf[:runtime.Stack(buf, false)]
          err, ok := r.(error)
          if! ok { err = fmt.Errorf("%v", r)
          }
          logger.Error(err, "panic"."stack"."... \n"+string(buf))
        }
      }()
      j.Run()
    })
  }
}
Copy the code

Add the recover() call before executing the inner Job logic. If there is panic during job.run () execution. Here recover() captures the output call stack.

DelayIfStillRunning

Let’s see how to use it first:

type delayJob struct {
  count int
}

func (d *delayJob) Run(a) {
  time.Sleep(2 * time.Second)
  d.count++
  log.Printf("%d: hello world\n", d.count)
}

func main(a) {
  c := cron.New()
  c.AddJob("@every 1s", cron.NewChain(cron.DelayIfStillRunning(cron.DefaultLogger)).Then(&delayJob{}))
  c.Start()

  time.Sleep(10 * time.Second)
}
Copy the code

Above we added a 2s delay to Run() and the output interval becomes 2s instead of timed 1s:

$ go run main.go 
2020/06/27 14:11:16 1: hello world
2020/06/27 14:11:18 2: hello world
2020/06/27 14:11:20 3: hello world
2020/06/27 14:11:22 4: hello world
Copy the code

Look at the source code:

// chain.go
func DelayIfStillRunning(logger Logger) JobWrapper {
  return func(j Job) Job {
    var mu sync.Mutex
    return FuncJob(func(a) {
      start := time.Now()
      mu.Lock()
      defer mu.Unlock()
      if dur := time.Since(start); dur > time.Minute {
        logger.Info("delay"."duration", dur)
      }
      j.Run()
    })
  }
}
Copy the code

First, define a Mutex shared by the task sync.Mutex. Obtain the lock before each task execution and release the lock after execution. Therefore, before the end of the previous task, the next task can not obtain the lock successfully, so as to ensure the serial execution of the task.

SkipIfStillRunning

Let’s see how it works first:

type skipJob struct {
  count int32
}

func (d *skipJob) Run(a) {
  atomic.AddInt32(&d.count, 1)
  log.Printf("%d: hello world\n", d.count)
  if atomic.LoadInt32(&d.count) == 1 {
    time.Sleep(2 * time.Second)
  }
}

func main(a) {
  c := cron.New()
  c.AddJob("@every 1s", cron.NewChain(cron.SkipIfStillRunning(cron.DefaultLogger)).Then(&skipJob{}))
  c.Start()

  time.Sleep(10 * time.Second)
}
Copy the code

Output:

$ go run main.go
2020/06/27 14:22:07 1: hello world
2020/06/27 14:22:10 2: hello world
2020/06/27 14:22:11 3: hello world
2020/06/27 14:22:12 4: hello world
2020/06/27 14:22:13 5: hello world
2020/06/27 14:22:14 6: hello world
2020/06/27 14:22:15 7: hello world
2020/06/27 14:22:16 8: hello world
Copy the code

Notice the time difference of 3s between the first output and the second output because two executions are skipped.

Note that DelayIfStillRunning is essentially different from SkipIfStillRunning. As long as the time of the former DelayIfStillRunning is long enough, all tasks will be completed step by step, but the previous task may take too long. The execution time of the latter task was delayed a bit. SkipIfStillRunning skips some execution.

Look at the source code:

func SkipIfStillRunning(logger Logger) JobWrapper {
  return func(j Job) Job {
    var ch = make(chan struct{}, 1)
    ch <- struct{} {}return FuncJob(func(a) {
      select {
      case v := <-ch:
        j.Run()
        ch <- v
      default:
        logger.Info("skip")}})}}Copy the code

Chan struct{} defines a channel whose cache size is 1 shared by this task. If the task is executed successfully, the task is executed. Otherwise, the task is skipped. Once the execution is complete, send a value to the channel to ensure that the next task can be executed. A value is initially sent to the channel to ensure the first task is executed.

conclusion

The cron implementation is small and elegant, with few lines of code, and well worth a look!

If you find a fun and useful Go library, please Go to GitHub and submit issue😄

reference

  1. Cron GitHub:github.com/robfig/cron
  2. Go daily carbon of a library: darjun. Making. IO / 2020/02/14 /…
  3. Go daily gron of library: darjun. Making. IO / 2020/04/20 /…
  4. GitHub: github.com/darjun/go-d…

I

My blog is darjun.github. IO

Welcome to follow my wechat public account [GoUpUp], learn together, progress together ~