5 min read
In a lot of situations, we would like to be able to schedule functions in Go. While there are many current tools for doing scheduling (such as Cron), I would much prefer if the entire working of my program was contained in the same code/binary and I did not have to go to the system crontab to discover what is going on.
Thankfully, the time
standard library already comes with A LOT of tools to schedule events. Let’s look at some of them and when to use them.
I also have a package Kronika, which includes an Every
function that can be used for scheduling.
ctx := context.Background()
// see https://golang.org/pkg/time/#Time.AddDate
start, err := time.Now().AddDate(0, 0, 7)
if err != nil {
panic(err)
}
interval := time.Minute * 10 // 10 minutes
for t := range kronika.Every(ctx, start, interval) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Check out Kronika, a package that adds some nice helpers around the time package.
time.After()
time.After()
allows us to perform an action after a duration. For example:
NOTE: The helper constants time.Second
, time.Minute
and time.Hour
are all of type time.Duration
. So if we have to supply a duration, we can multiply these constants by a number (e.g. time.Second * 5
) and the expression returns a time.Duration
.
package main
import (
"fmt"
"time"
)
func main() {
// This will block for 5 seconds and then return the current time
theTime := <-time.After(time.Second * 5)
fmt.Println(theTime.Format("2006-01-02 15:04:05"))
}
2019-09-22 09:33:05
time.Ticker
time.After()
is great for one-time actions, but the power of cron jobs are in performing repeated actions.
So, to do that we use a time.Ticker
. For most use cases, we can use the helper function time.Tick()
to create a ticker. For example:
package main
import (
"fmt"
"time"
)
func main() {
// This will print the time every 5 seconds
for theTime := range time.Tick(time.Second * 5) {
fmt.Println(theTime.Format("2006-01-02 15:04:05"))
}
}
2019-09-22 10:07:54
2019-09-22 10:07:59
2019-09-22 10:08:04
2019-09-22 10:08:09
2019-09-22 10:08:14
NOTE: When using a Ticker
, the first event will be triggered AFTER the delay.
time.Tick()
When we use the time.Tick()
function, we do not have direct access to the underlying time.Ticker
and so we cannot close it properly.
If we never need to explicitly stop the ticker (for example if the ticker will run all the time), then this may not be an issue. However, if we simply ignore the ticker, the resources will not be freed up and it will not be garbage collected.
time.Tick()
There are several things we can’t easily do with time.Ticker
:
time.Tick()
using a custom functionTo overcome the limitations of time.Tick()
, I’ve created a helper function which I use in my projects.
func cron(ctx context.Context, startTime time.Time, delay time.Duration) <-chan time.Time {
// Create the channel which we will return
stream := make(chan time.Time, 1)
// Calculating the first start time in the future
// Need to check if the time is zero (e.g. if time.Time{} was used)
if !startTime.IsZero() {
diff := time.Until(startTime)
if diff < 0 {
total := diff - delay
times := total / delay * -1
startTime = startTime.Add(times * delay)
}
}
// Run this in a goroutine, or our function will block until the first event
go func() {
// Run the first event after it gets to the start time
t := <-time.After(time.Until(startTime))
stream <- t
// Open a new ticker
ticker := time.NewTicker(delay)
// Make sure to stop the ticker when we're done
defer ticker.Stop()
// Listen on both the ticker and the context done channel to know when to stop
for {
select {
case t2 := <-ticker.C:
stream <- t2
case <-ctx.Done():
close(stream)
return
}
}
}()
return stream
}
This function is included in the Kronika package, you can use it with kronika.Every()
The function receives 3 parameters.
Context
: The ticker will be stopped whenever the context is canceled. So we can create a context with a cancel function, or a timeout, or a deadline, and when that context is canceled, the function will gracefully release its resources.Time
: The start time is used as a reference to know when to start ticking. If the start time is in the future, it will not start ticking until that time. If it is in the past. The function calculates the first event that will be in the future by adding an appropriate multiple of the delay.Duration
: This is the interval between ticks. Calculated from the start time.Run on Tuesdays by 2 pm
ctx := context.Background()
startTime, err := time.Parse(
"2006-01-02 15:04:05",
"2019-09-17 14:00:00",
) // is a tuesday
if err != nil {
panic(err)
}
delay := time.Hour * 24 * 7 // 1 week
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Run every hour, on the hour
ctx := context.Background()
startTime, err := time.Parse(
"2006-01-02 15:04:05",
"2019-09-17 14:00:00",
) // any time in the past works but it should be on the hour
if err != nil {
panic(err)
}
delay := time.Hour // 1 hour
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
Run every 10 minutes, starting in a week
ctx := context.Background()
startTime, err := time.Now().AddDate(0, 0, 7) // see https://golang.org/pkg/time/#Time.AddDate
if err != nil {
panic(err)
}
delay := time.Minute * 10 // 10 minutes
for t := range cron(ctx, startTime, delay) {
// Perform action here
log.Println(t.Format("2006-01-02 15:04:05"))
}
With this function, we have much better control over scheduling in our projects. We can set a specific time to begin and a duration. We also nicely clean up any resources used.
Check out Kronika, a package that adds some nice helpers around the time package.
Let me know what you think.
Here is a one-liner you can use to pause execution until a specific time
2 min read
This article is about an experiment at Africa’s Talking on using Slack to manage our deployment process. Like many companies, we use Kubernetes ...
1 min read
Comments