Introduction to the

Gron is a small and flexible scheduled task library that can execute scheduled and periodic tasks. Gron provides a concise, concurrency-safe interface. We first introduce the use of GRon library, and then a simple analysis of the source code.

Quick to use

First installation:

$ go get github.com/roylee0704/gron
Copy the code

After using:

package main

import (
  "fmt"
  "sync"
  "time"

  "github.com/roylee0704/gron"
)

func main(a) {
  var wg sync.WaitGroup
  wg.Add(1)

  c := gron.New()
  c.AddFunc(gron.Every(5*time.Second), func(a) {
    fmt.Println("runs every 5 seconds.")
  })
  c.Start()

  wg.Wait()
}
Copy the code

Gron is relatively simple to use:

  • First callgron.New()To create amanager, which is a scheduled task manager;
  • Then call the manager’sAddFunc()orAdd()Method to add tasks to it, which is also possible at startup, as analyzed below;
  • The last call to the managerStart()Method to start it.

Gron supports two ways to add tasks, one by using functions with no arguments, and the other by implementing the task interface. The example above uses the former approach, which we will describe later. Gron.every () specifies the interval of periodic tasks when adding tasks. A periodic task of 5s is added above, and a line of text is output Every 5s.

Note that we use sync.waitgroup to ensure that the main goroutine does not exit. Because only one goroutine is started in c.start (), if the main goroutine exits, the whole program stops.

Run the program, output every 5s:

runs every 5 seconds.
runs every 5 seconds.
runs every 5 seconds.
Copy the code

The program needs to be stopped by pressing CTRL + C!

Time format

Gron accepts time intervals of type time.Duration. In addition to the base Second/Minute/Hour defined in the time package, the Xtime subpackage in GRon also provides time in units of Day/Week. It should be noted that grON supports a time precision of 1s, and an interval less than 1s is not supported. In addition to the unit time interval, we can use a time like 4m10s:

func main(a) {
  var wg sync.WaitGroup
  wg.Add(1)

  c := gron.New()
  c.AddFunc(gron.Every(1*time.Second), func(a) {
    fmt.Println("runs every second.")
  })
  c.AddFunc(gron.Every(1*time.Minute), func(a) {
    fmt.Println("runs every minute.")
  })
  c.AddFunc(gron.Every(1*time.Hour), func(a) {
    fmt.Println("runs every hour.")
  })
  c.AddFunc(gron.Every(1*xtime.Day), func(a) {
    fmt.Println("runs every day.")
  })
  c.AddFunc(gron.Every(1*xtime.Week), func(a) {
    fmt.Println("runs every week.")
  })
  t, _ := time.ParseDuration("4m10s")
  c.AddFunc(gron.Every(t), func(a) {
    fmt.Println("runs every 4 minutes 10 seconds.")
  })
  c.Start()

  wg.Wait()
}
Copy the code

Use gron.every () to set how often a task should be executed. For intervals greater than 1 day, we can also specify that it should be executed At a certain point in time using gron.every ().at (). For example, the following program is triggered every other day, starting at 22:00 the next day, i.e. 22:00 each day:

func main(a) {
  var wg sync.WaitGroup
  wg.Add(1)

  c := gron.New()
  c.AddFunc(gron.Every(1*xtime.Day).At("22:00"), func(a) {
    fmt.Println("runs every second.")
  })
  c.Start()

  wg.Wait()
}
Copy the code

Custom Tasks

It is also easy to implement custom tasks by implementing the Gron.job interface:

// src/github.com/roylee0704/gron/cron.go
type Job interface {
  Run()
}
Copy the code

We need to call the scheduler’s Add() method to Add a custom task to the manager:

type GreetingJob struct {
  Name string
}

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

func main(a) {
  var wg sync.WaitGroup
  wg.Add(1)

  g1 := GreetingJob{Name: "dj"}
  g2 := GreetingJob{Name: "dajun"}

  c := gron.New()
  c.Add(gron.Every(5*time.Second), g1)
  c.Add(gron.Every(10*time.Second), g2)
  c.Start()

  wg.Wait()
}
Copy the code

We created a GreetingJob structure that implements the Gron. Job interface, and then created two objects, g1/ G2, one fired once every 5s and one fired once every 10s. Tasks that carry state can be better handled by using a custom task, such as the Name field above.

In fact, the AddFunc() method is also implemented internally by Add() :

// src/github.com/roylee0704/gron/cron.go
func (c *Cron) AddFunc(s Schedule, j func(a)) {
  c.Add(s, JobFunc(j))
}

type JobFunc func(a)

func (j JobFunc) Run(a) {
  j()
}
Copy the code

Inside AddFunc(), the function passed in is converted to type JobFunc, and gron implements the Gron.job interface for JobFunc. Is this similar to HandleFunc and Handle in NET/HTTP packages? If you notice, you can see this pattern in a lot of Go code.

Point source

The source code of Gron is only two files, Cron. go and schedule.go. The two files are only 260 lines, including comments! The tasks we added are represented as Entry structures within grON:

type Entry struct {
  Schedule Schedule
  Job      Job
  Next time.Time
  Prev time.Time
}
Copy the code

Next is the Next execution time, Prev is the last execution time, Job is the task to be executed, Schedule is the gron.Schedule interface type, and Next() can be called to calculate the Next execution time.

The manager uses the gron.cron structure to represent:

type Cron struct {
  entries []*Entry
  running bool
  add     chan *Entry
  stop    chan struct{}}Copy the code

The task is scheduled in a separate Goroutine. If scheduling has not started, you can append tasks directly to entries slices. If scheduling has started (the Start() method has been called), you need to send the tasks to be added to channel Add. The core logic of task scheduling is in the Run() method:

func (c *Cron) run(a) {
  var effective time.Time
  now := time.Now().Local()

  // to figure next trig time for entries, referenced from now
  for _, e := range c.entries {
    e.Next = e.Schedule.Next(now)
  }

  for {
    sort.Sort(byTime(c.entries))
    if len(c.entries) > 0 {
      effective = c.entries[0].Next
    } else {
      effective = now.AddDate(15.0.0) // to prevent phantom jobs.
    }

    select {
    case now = <-after(effective.Sub(now)):
      // entries with same time gets run.
      for _, entry := range c.entries {
        ifentry.Next ! = effective {break
        }
        entry.Prev = now
        entry.Next = entry.Schedule.Next(now)
        go entry.Job.Run()
      }
    case e := <-c.add:
      e.Next = e.Schedule.Next(time.Now())
      c.entries = append(c.entries, e)
    case <-c.stop:
      return // terminate go-routine.}}}Copy the code

The execution process is as follows:

  1. When the scheduler starts, it calculates the next execution time of all tasks.
  2. And then in aforIn the loop, sort tasks according to the execution time from morning to night, and get the latest time points to execute tasks;
  3. inselectThe statement waits until this point in time to start a new Goroutine to execute the expired task, one new Goroutine for each task;
  4. If, while waiting, a new task is added (through the channelc.add) to calculate the first execution time of this new task. Skip to Step 2, because the newly added task may be executed earliest.

There are a few details to note:

  1. Local time is used to determine task expiration:time.Now().Local();
  2. If there is no task, the wait time is set tonow.AddDate(15, 0, 0), that is, 15 years to prevent CPU idling;
  3. Tasks are performed in a separate Goroutine;
  4. By implementingsort.InterfaceInterface can implement custom sorting (in codebyTime).

Finally, let’s look at the time policy code. We know that an object of type Gron.schedule is stored in the Entry structure, and calling the Next() method on this object returns the Next execution point:

// src/github.com/roylee0704/gron/schedule.go
type Schedule interface {
  Next(t time.Time) time.Time
}
Copy the code

Gron implements two types of schedules. The first is Periodic-triggered Schedule, which is returned by the gron.every () function:

// src/github.com/roylee0704/gron/schedule.go
type periodicSchedule struct {
  period time.Duration
}
Copy the code

One is a periodic trigger at a fixed time, which is actually a periodic trigger at a fixed point in time:

type atSchedule struct {
  period time.Duration
  hh     int
  mm     int
}
Copy the code

Their core logic in the Next() method is that periodicSchedule simply needs the current time plus the period to get the Next trigger time. Here the Truncate() method truncates the portion of the current time less than 1s:

func (ps periodicSchedule) Next(t time.Time) time.Time {
  return t.Truncate(time.Second).Add(ps.period)
}
Copy the code

The Next() method of atSchedule calculates the time point of the day and adds the period to the Next trigger time:

func (as atSchedule) reset(t time.Time) time.Time {
  return time.Date(t.Year(), t.Month(), t.Day(), as.hh, as.mm, 0.0, time.UTC)
}

func (as atSchedule) Next(t time.Time) time.Time {
  next := as.reset(t)
  if t.After(next) {
    return next.Add(as.period)
  }
  return next
}
Copy the code

PeriodicSchedule provides the At() method to convert to atSchedule:

func (ps periodicSchedule) At(t string) Schedule {
  if ps.period < xtime.Day {
    panic("period must be at least in days")}// parse t naively
  h, m, err := parse(t)

  iferr ! =nil {
    panic(err.Error())
  }

  return &atSchedule{
    period: ps.period,
    hh:     h,
    mm:     m,
  }
}
Copy the code

User-defined time policies

We can easily implement a custom time policy. For example, we want to implement an “exponential retreat” time series, waiting 1s, then 2s, 4s…

type ExponentialBackOffSchedule struct {
	last int
}

func (e *ExponentialBackOffSchedule) Next(t time.Time) time.Time {
	interval := time.Duration(math.Pow(2.0.float64(e.last))) * time.Second
	e.last += 1
	return t.Truncate(time.Second).Add(interval)
}

func main(a) {
	var wg sync.WaitGroup
	wg.Add(1)

	c := gron.New()
	c.AddFunc(&ExponentialBackOffSchedule{}, func(a) {
		fmt.Println(time.Now().Local().Format("The 2006-01-02 15:04:05"), "hello")
	})
	c.Start()

	wg.Wait()
}
Copy the code

The running results are as follows:

2020- 04- 20 23:47:11 hello
2020- 04- 20 23:47:13 hello
2020- 04- 20 23:47:17 hello
2020- 04- 20 23:47:25 hello
Copy the code

The difference between the second output and the first output is 2s, the third output and the second output is 4s, the fourth output and the third output is 8s, perfect!

conclusion

This article introduces gron this small timing task library, how to use, how to customize the task and time strategy, along with the analysis of the source code. Gron source code implementation is very simple, very recommended reading!

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

reference

  1. Gron GitHub:github.com/roylee0704/…
  2. 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 ~