Using the graceful shutdown approach to dispose of applications
Graceful shutdown is a process that is well stated in twelve factors; in addition to keeping applications with 🏁 fast and furious launch, we need be concerned with how we dispose of every application component. We're not talking about classes and garbage collector. This topic is about the interruption, which could be caused by a user stopping a program or a container receiving a signal to stop for a scaling operation, swapping from another node, or other things that happen on a regular basis while working with containers.
Imagine an application receiving requests for transaction payments and an interruption occurs; this transaction becomes lost or incomplete, and if retry processing or reconciliation is not implemented, someone will need to push a button to recover this transaction...
We should agree that manual processing works at first, but every developer knows the end...
How does graceful shutdown work?
When your application begins to dispose, you can stop receiving more demands; these demands could be a message from a queue or topic; if we're dealing with workers, this message should return to the queue or topic; Rabbit provides a message confirmation (ACK) that performs a delete message from the queue that is successfully processed by the worker. In container contexts, this action should be quick to avoid a forced interruption caused by a long waiting time.
Show me the code!
You may get the source code from my Github repository.
The following code shows a basic application that uses signals to display Dragon Ball 🐲 character information every second. When interruption signal is received the timer responsible to print messages per second is stopped. In this example, we're using simple timers, but it could also be a web server or a worker connected into a queue, as previously said. Many frameworks and components include behaviors for closing and waiting for incoming demands.
- app.go
package main
import (
"encoding/csv"
"fmt"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
)
const blackColor string = "\033[1;30m%s\033[0m"
var colors = []string{
"\033[1;31m%s\033[0m",
"\033[1;32m%s\033[0m",
"\033[1;33m%s\033[0m",
"\033[1;34m%s\033[0m",
"\033[1;35m%s\033[0m",
"\033[1;36m%s\033[0m",
}
type Character struct {
Name string
Description string
}
func main() {
printHello()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("Starting random Dragon Ball characters service...")
shutdown := make(chan bool, 1)
go func() {
sig := <-sigs
fmt.Println()
fmt.Println(sig)
shutdown <- true
}()
characterSize, characterList := readFile()
quit := make(chan struct{})
go func() {
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-ticker.C:
printMessage(characterSize, characterList)
case <-quit:
ticker.Stop()
return
}
}
}()
<-shutdown
close(quit)
fmt.Println("Process gracefully stopped.")
}
func printHello() {
dat, err := os.ReadFile("ascii_art.txt")
if err != nil {
panic(err)
}
fmt.Println(string(dat))
}
func readFile() (int, []Character) {
file, err := os.Open("dragon_ball.csv")
if err != nil {
panic(err)
}
csvReader := csv.NewReader(file)
data, err := csvReader.ReadAll()
if err != nil {
panic(err)
}
characterList := buildCharacterList(data)
file.Close()
return len(characterList), characterList
}
func buildCharacterList(data [][]string) []Character {
var characterList []Character
for row, line := range data {
if row == 0 {
continue
}
var character Character
for col, field := range line {
if col == 0 {
character.Name = field
} else if col == 1 {
character.Description = field
}
}
characterList = append(characterList, character)
}
return characterList
}
func printMessage(characterSize int, characterList []Character) {
color := colors[rand.Intn(len(colors))]
characterIndex := rand.Intn(characterSize)
character := characterList[characterIndex]
fmt.Printf(color, fmt.Sprintf("%s %s", "🐉", character.Name))
fmt.Printf(blackColor, fmt.Sprintf(" %s\n", character.Description))
}
- go.mod
module app
go 1.20
Code Highlights
- This code block prepares the application to support signals; shutdown is a channel that, when modified, triggers an execution block for disposal.
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
shutdown := make(chan bool, 1)
go func() {
sig := <-sigs
fmt.Println()
fmt.Println(sig)
shutdown <- true
}()
- The ticker is in charge of printing messages every 5 seconds; when it receives a signal from the quit channel, it stops.
quit := make(chan struct{})
go func() {
ticker := time.NewTicker(5 * time.Second)
for {
select {
case <-ticker.C:
printMessage(characterSize, characterList)
case <-quit:
ticker.Stop()
return
}
}
}()
- The ticker is closed by "quit channel" after receiving a signal halting the application's execution.
<-shutdown
close(quit)
fmt.Println("Process gracefully stopped.")
Graceful Shutdown working
When CTRL+C is pressed, the application receives the signal SIGINT, and disposal occurs, the following command will launch the application.
go run app.go
Containers
It's time to look at graceful shutdown in the container context; in the following file, we have a container image:
- Containerfile
FROM docker.io/golang:alpine3.17
MAINTAINER [email protected]
WORKDIR /app
COPY ./graceful_shutdown go.mod /app
RUN go build -o /app/graceful-shutdown
EXPOSE 3000
CMD [ "/app/graceful-shutdown" ]
Let's build a container image:
docker buildx build -t graceful-shutdown -f graceful_shutdown/Containerfile .
# without buildx
docker build -t graceful-shutdown -f graceful_shutdown/Containerfile .
# for podmans
podman build -t graceful-shutdown -f graceful_shutdown/Containerfile .
The following command will test the execution, logs, and stop that is in charge of sending signals to the application; if no signals are received, Docker will wait a few seconds and force an interruption:
docker run --name graceful-shutdown -d -it --rm graceful-shutdown
docker logs -f graceful-shutdown
# sent signal to application stop
docker stop graceful-shutdown
# Using
# Podman
podman run --name graceful-shutdown -d -it --rm graceful-shutdown
podman logs -f graceful-shutdown
# sent signal to application stop
podman stop graceful-shutdown
That's all folks
In this article, I described how graceful shutdown works and how you may apply it in your applications. Implementing graceful shutdown is part of a robust process; we should also examine how to reconcile a processing when a server, node, or network fails, so we should stop thinking just on the happy path.
I hope this information is useful to you on a daily basis as a developer.
I'll see you next time, and please keep your kernel 🧠 updated.
Time for feedback!