Golang: Implementing a cron / executing tasks at a specific time
Asked Answered
M

6

47

I have been looking around for examples on how to implement a function that allows you to execute tasks at a certain time in Go, but I couldn't find anything.

I implemented one myself and I am sharing it in the answers, so other people can have a reference for their own implementation.

Marcenemarcescent answered 23/10, 2013 at 18:7 Comment(1)
shameless plug: perhaps you could check this out: minimalist cron go-package: github.com/roylee0704/gronFrausto
M
41

This is a general implementation, which lets you set:

  • interval period
  • hour to tick
  • minute to tick
  • second to tick

UPDATED: (the memory leak was fixed)

import (
"fmt"
"time"
)

const INTERVAL_PERIOD time.Duration = 24 * time.Hour

const HOUR_TO_TICK int = 23
const MINUTE_TO_TICK int = 00
const SECOND_TO_TICK int = 03

type jobTicker struct {
    timer *time.Timer
}

func runningRoutine() {
    jobTicker := &jobTicker{}
    jobTicker.updateTimer()
    for {
        <-jobTicker.timer.C
        fmt.Println(time.Now(), "- just ticked")
        jobTicker.updateTimer()
    }
}

func (t *jobTicker) updateTimer() {
    nextTick := time.Date(time.Now().Year(), time.Now().Month(), 
    time.Now().Day(), HOUR_TO_TICK, MINUTE_TO_TICK, SECOND_TO_TICK, 0, time.Local)
    if !nextTick.After(time.Now()) {
        nextTick = nextTick.Add(INTERVAL_PERIOD)
    }
    fmt.Println(nextTick, "- next tick")
    diff := nextTick.Sub(time.Now())
    if t.timer == nil {
        t.timer = time.NewTimer(diff)
    } else {
        t.timer.Reset(diff)
    }
}
Marcenemarcescent answered 23/10, 2013 at 18:22 Comment(5)
This implementation leaks memory. > Stop the ticker to release associated resources.Antwanantwerp
@Antwanantwerp is right, each time we create a new ticker, the old one will never be released. a better solution is here: https://mcmap.net/q/360541/-golang-implementing-a-cron-executing-tasks-at-a-specific-timePreceptor
@Caleb, thanks! feel free to update the code on the answer, to fix the leakMarcenemarcescent
isn't it got garbage collected?Pumpkinseed
This doesn't handle NTP clock adjustment well... a daily ticker fires twice sometimes.Granulose
L
31

In case someone drops in on this question searching for a quick solution. I found a neat library that makes it really easy to schedule jobs.

Link: https://github.com/jasonlvhit/gocron

The API is pretty straightforward:

import (
    "fmt"
    "github.com/jasonlvhit/gocron"
)

func task() {
    fmt.Println("Task is being performed.")
}

func main() {
    s := gocron.NewScheduler()
    s.Every(2).Hours().Do(task)
    <- s.Start()
}
Lent answered 11/2, 2016 at 13:22 Comment(0)
P
24

the answer provided by @Daniele B is not good enough, as @Caleb says, that implementation leaks memory, because each time we create a new ticker, the old one will never be released.

so I wrap the time.timer, and reset it everytime, a example here:

package main

import (
    "fmt"
    "time"
)

const INTERVAL_PERIOD time.Duration = 24 * time.Hour

const HOUR_TO_TICK int = 23
const MINUTE_TO_TICK int = 21
const SECOND_TO_TICK int = 03

type jobTicker struct {
    t *time.Timer
}

func getNextTickDuration() time.Duration {
    now := time.Now()
    nextTick := time.Date(now.Year(), now.Month(), now.Day(), HOUR_TO_TICK, MINUTE_TO_TICK, SECOND_TO_TICK, 0, time.Local)
    if nextTick.Before(now) {
        nextTick = nextTick.Add(INTERVAL_PERIOD)
    }
    return nextTick.Sub(time.Now())
}

func NewJobTicker() jobTicker {
    fmt.Println("new tick here")
    return jobTicker{time.NewTimer(getNextTickDuration())}
}

func (jt jobTicker) updateJobTicker() {
    fmt.Println("next tick here")
    jt.t.Reset(getNextTickDuration())
}

func main() {
    jt := NewJobTicker()
    for {
        <-jt.t.C
        fmt.Println(time.Now(), "- just ticked")
        jt.updateJobTicker()
    }
}
Preceptor answered 2/9, 2016 at 15:29 Comment(0)
B
10

I have created a package that actually supports crontab syntax if you are familiar with it, for example:

ctab := crontab.New()
ctab.AddJob("*/5 * * * *", myFunc)
ctab.AddJob("0 0 * * *", myFunc2)

Package link: https://github.com/mileusna/crontab

Backsight answered 10/8, 2017 at 1:45 Comment(1)
This is actually really nice! Thx for creating this package :)Balloon
S
8

This is another general implementation without need for a third party library.

Disclaimer: This implementation works with UTC. For managing timezones it has to be modified.

Run a func once a day at noon.

  • Period: time.Hour * 24
  • Offset: time.Hour * 12

Run a func twice a day at 03:40 (00:00 + 03:40) and 15:40 (12:00 + 03:40).

  • Period: time.Hour * 12
  • Offset: time.Hour * 3 + time.Minute * 40

Updated (2020-01-28):

Changes:

  • context.Context can be used for cancellation, makes it testable.
  • time.Ticker removes the need for calculating the time of the next execution.
package main

import (
    "context"
    "time"
)

// Schedule calls function `f` with a period `p` offsetted by `o`.
func Schedule(ctx context.Context, p time.Duration, o time.Duration, f func(time.Time)) {
    // Position the first execution
    first := time.Now().Truncate(p).Add(o)
    if first.Before(time.Now()) {
        first = first.Add(p)
    }
    firstC := time.After(first.Sub(time.Now()))

    // Receiving from a nil channel blocks forever
    t := &time.Ticker{C: nil}

    for {
        select {
        case v := <-firstC:
            // The ticker has to be started before f as it can take some time to finish
            t = time.NewTicker(p)
            f(v)
        case v := <-t.C:
            f(v)
        case <-ctx.Done():
            t.Stop()
            return
        }
    }

}

Original:

package main

import (
    "time"
)

// Repeat calls function `f` with a period `d` offsetted by `o`.
func Repeat(d time.Duration, o time.Duration, f func(time.Time)) {
    next := time.Now().Truncate(d).Add(o)
    if next.Before(time.Now()) {
        next = next.Add(d)
    }

    t := time.NewTimer(next.Sub(time.Now()))

    for {
        v := <-t.C
        next = next.Add(d)
        t.Reset(next.Sub(time.Now()))
        f(v)
    }
}
Sansen answered 15/5, 2019 at 20:6 Comment(0)
T
0

I'm using https://github.com/ehsaniara/gointerlock. It's also supported in distributed systems and has a builtin distributer lock (Redis)

import (
    "context"
    "fmt"
    "github.com/ehsaniara/gointerlock"
    "log"
    "time"
)

var job = gointerlock.GoInterval{
    Interval: 2 * time.Second,
    Arg:      myJob,
}

err := job.Run(ctx)
if err != nil {
    log.Fatalf("Error: %s", err)
}

func myJob() {
    fmt.Println(time.Now(), " - called")
}
Terbecki answered 31/7, 2021 at 6:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.