Graceful Shutdown in Go: Safeguarding Data Integrity and User Experience
Introduction
Enabling graceful shutdown is a crucial step in any production service deployment checklist. In the context of an HTTP server, graceful shutdown ensures that all pending requests are given the opportunity to complete and stopping acceptance of new requests. In this post, we’ll explore the drawbacks of an HTTP server that lacks graceful shutdown and then dive into an updated implementation that employs graceful shutdown to safeguard data integrity and enhance user experience.
The Problem with Non-Graceful Shutdown
Let’s start by examining an HTTP server that lacks graceful shutdown. The following code demonstrates a simple HTTP server exposing an /upload
endpoint responsible for saving the request body contents into a file.
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("👋"))
})
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
f, err := os.Create(fmt.Sprintf("%d", time.Now().UnixNano()))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer f.Close()
_, err = io.Copy(f, r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write([]byte(f.Name()))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
})
server := &http.Server{
Addr: ":1337",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Without graceful shutdown, this server is prone to data corruption. If an operating system process termination signal is received during a file upload, the file contents might become incomplete. Furthermore, abrupt request interruptions negatively impact the user experience. To address these issues, we need to implement graceful shutdown.
Implementing Graceful Shutdown
To enable graceful shutdown, we’ll need to hook into the exit signals sent to the process executing our HTTP server. The Go standard library provides the necessary functionality through the signal
package and its Notify
function. This function allows us to receive operating system signals via a channel.
Let’s now explore the updated code that implements graceful shutdown:
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
const (
shutdownTimeoutDuration = time.Second * 15
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("👋"))
})
mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
f, err := os.Create(fmt.Sprintf("%d", time.Now().UnixNano()))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer f.Close()
_, err = io.Copy(f, r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write([]byte(f.Name()))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
})
server := &http.Server{
Addr: ":1337",
Handler: mux,
}
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
wait := make(chan os.Signal, 1)
signal.Notify(wait, syscall.SIGINT, syscall.SIGTERM)
<-wait
shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), shutdownTimeoutDuration)
defer shutdownCtxCancel()
err := server.Shutdown(shutdownCtx)
if err != nil {
log.Fatal(err)
}
}
In this updated implementation, we have introduced the following changes:
- A goroutine is started using the
go
keyword, which runs an anonymous function. Inside this function, the server starts listening and serving incoming requests usingserver.ListenAndServe()
. The server runs in the background, allowing the main goroutine to continue execution. - A channel called
wait
is created to receive operating system signals. Thesignal.Notify()
function is used to forward theSIGINT
(Ctrl+C) andSIGTERM
signals to thewait
channel. - The code blocks at
<-wait
, waiting for a signal to be received on thewait
channel. - When a signal is received (either
SIGINT
orSIGTERM
), the code proceeds to execute the following steps:- A context is created with a timeout duration using
context.WithTimeout()
. This context will be used to gracefully shut down the server within the specified timeout (as defined byshutdownTimeoutDuration
). - The
server.Shutdown()
method is called with the shutdown context as the argument. This initiates the graceful shutdown process.
- A context is created with a timeout duration using
Conclusion
Incorporating graceful shutdown into your Go HTTP server is a critical step in ensuring data integrity and providing a smoother user experience. By utilizing the signal
package and server.Shutdown()
method, you can gracefully handle exit signals, allowing pending requests to complete and new requests to be stopped. This straightforward addition greatly improves the robustness of your server, making it better prepared to handle real-world production scenarios.
For a more comprehensive example of graceful shutdown, feel free to explore the additional example that demonstrates graceful shutdown with in-process background goroutines cleanup via sync.WaitGroup
and context cancellation.
Remember, investing time in enabling graceful shutdown will pay dividends in the form of a more reliable and user-friendly web service. Happy coding!