I'm working on a Go microservice where I am using robfig/cron
package to schedule jobs, initially thought of using time.Ticker() but package made it easier for me.
My question is how to ensure cron job is registered only once until the running cron completes and prevent multiple goroutines from running simultaneously.
This is different from singleton approach, the block of code inside
RunProcess()
must not run until the previous RunProcess function completes.
I am new to golang and its convention. :(
Here is a simplified version of my current implementation:
package main
import (
"fmt"
"sync"
"time"
"github.com/robfig/cron/v3"
)
var isProcessRunning = false
var mu sync.Mutex
func RunProcess() {
mu.Lock()
defer mu.Unlock()
if isProcessRunning {
fmt.Println("Already Running...")
return
}
isProcessRunning = true
fmt.Println("Running...")
// Simulate work
time.Sleep(15 * time.Second)
isProcessRunning = false
}
func InitCron() {
// Create a new cron scheduler
c := cron.New(cron.WithSeconds())
// Add the task to run every 10 seconds
_, err := c.AddFunc("*/10 * * * * *", RunProcess)
if err != nil {
fmt.Println("Error adding cron job:", err)
return
}
// Start the cron scheduler
c.Start()
// Block indefinitely to keep the cron jobs running
select {}
}
func main() {
InitCron()
}
However, I noticed that when InitCron is called multiple times, it can potentially create multiple cron jobs, leading to concurrency issues and unexpected behavior for an lightweight microservice.
Any advice or examples on how to manage this properly would be greatly appreciated.
Running cron in golang and tryin to prevent the cron until it first cron is finished
Your code is already preventing "multiple goroutines from running simultaneously" but probably not in the way you expect. There are two ways you could handle this:
time.Sleep
).Your code currently takes the first approach; but the fmt.Println("Already Running...")
indicates that you expect it to take the second.
So why is "Already Running..." never output? This is due to the way you are using the Mutex
. When you call Lock
:
Lock locks m. If the lock is already in use, the calling goroutine blocks until the mutex is available.
This means that when RunProcess
is called concurrently, execution will block at mu.Lock()
until mu.Unlock()
is called (in a seperate goroutine). You defer
the mu.Unlock()
call which means the Mutex
is unlocked "the moment the surrounding function returns". This may be easier to see if taken step by step:
RunProcess()
begins, calls mu.Lock()
and proceeds to time.Sleep(15 * time.Second)
.RunProcess()
begins, blocks at mu.Lock()
.RunProcess()
completes, this means the deferred mu.Unlock()
runs.RunProcess()
acquires Mutex and proceeds to time.Sleep(15 * time.Second)
.I believe that whay you intend to happen is for the second RunProcess()
to output "Already Running..." and terminate; if that is the case your code would need to be more like playground:
func RunProcess() {
mu.Lock() // Mutex protects isProcessRunning variable
if isProcessRunning {
fmt.Println("Already Running...")
mu.Unlock()
return
}
isProcessRunning = true
mu.Unlock() // isProcessRunning has been checked/set so release the Mutex (allowing other checks to proceed)
fmt.Println("Running...")
// Simulate work
time.Sleep(2 * time.Second)
fmt.Println("Done")
mu.Lock() // Need to access isProcessRunning so acquire Mutex
isProcessRunning = false
mu.Unlock()
}
Hopefully this answers the first part of your question. I believe the second part ref "InitCron is called multiple times" is a duplicate (i.e. use sync.Once
). However it could be argued that documenting that InitCron
must only be called once may be sufficient.
Moving on to how your solution could be improved; I'm going to assume that you have chosen robfig/cron
for valid reasons (I use it for scheduling reports etc). Preventing concurrent execution of tasks is a common requirement so robfig/cron
provides SkipIfStillRunning
; there is an example in the docs.