Synchronising Periodic Tasks and Graceful Shutdown with Goroutines and Tickers | Golang

Sidharthan Chandrasekaran Kamaraj
The Bug Shots
Published in
3 min readSep 12, 2023

Goroutines and channels are very useful and powerful primitives provided in the Go programming language for concurrently handling events, signals, and other asynchronous operations. The ticker can be used to create a repeatedly firing timer that will send signals on a channel at regular intervals. This channel can be passed to a goroutine that blocks waiting to receive from it. The goroutine provides concurrency safety while the ticker allows periodic checking and responding to external async events like signals. In this blog post, we will explore a specific example of utilizing a goroutine together with a ticker from the time package to gracefully handle POSIX operating system signals like SIGTERM and SIGINT.

The Scenario

Imagine we have a long-running Go program that needs to execute some logic periodically. For example, it could be polling a server for updates or performing other scheduled tasks. In such a scenario, we want the program to:

  1. Execute the logic every minute.
  2. Gracefully shut down when it receives a SIGTERM or SIGINT signal.

A Ticker Goroutine

Go’s time package provides a convenient Ticker object that can fire events on a channel at regular intervals. Here’s how we can create a ticker that fires every minute:

ticker := time.NewTicker(time.Minute)

Additionally, we need a channel to receive OS signals:

signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGTERM, syscall.SIGINT)

With the ticker and signal channel set up, we can proceed to launch a goroutine that uses them:

go func(ticker *time.Ticker) {
for {
select {
case <-ticker.C:
// Run required logic every minute
fmt.Println("Ticker was fired!")
ExampleLogic()
case <-signalChannel:
// remove tempfiles, close database connections etc.,
// Shutdown goroutine
os.Exit(0)
}
}
}(ticker)

In the code above, we define an anonymous function that accepts the ticker as a parameter. Inside this goroutine:

  • The select block concurrently waits on the ticker and signal channel.
  • When the ticker fires, it triggers the execution of the recurring logic.
  • If a signal (SIGTERM or SIGINT) is received, the goroutine proceeds to handle the graceful shutdown logic.

Putting it All Together

Our main function simply needs to start the ticker goroutine:

package main

import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)

func main() {
ticker := time.NewTicker(time.Minute)
TriggerGoroutine(ticker)
time.Sleep(10 * time.Minute)
}

func TriggerGoroutine(ticker *time.Ticker) {

signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGTERM, syscall.SIGINT)

go func(ticker *time.Ticker) {
for {
select {
case <-ticker.C:
// Run required logic every minute
fmt.Println("Ticker was fired!")
ExampleLogic()
case <-signalChannel:
// remove tempfiles, close database connections etc.,
// Shutdown goroutine
fmt.Println("Got signal, exiting...")
os.Exit(0)
}
}
}(ticker)
}

func ExampleLogic() {
fmt.Println("ExampleLogic was called!")
}

With this setup, the goroutine will execute every minute until the program is terminated with CTRL-C or a terminate signal. When a shutdown signal comes in, the goroutine will handle the graceful shutdown logic before the program exits.

This pattern provides a clean and efficient way to build periodically executing processes that respond to common signals, ensuring the program behaves predictably and gracefully.

Feel free to reach out if you have any questions or need further clarification!

--

--