Dlog

Graceful Shutdown in Go: Safeguarding Data Integrity and User Experience

📖 4 min read

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 using server.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. The signal.Notify() function is used to forward the SIGINT (Ctrl+C) and SIGTERM signals to the wait channel.
  • The code blocks at <-wait, waiting for a signal to be received on the wait channel.
  • When a signal is received (either SIGINT or SIGTERM), 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 by shutdownTimeoutDuration).
    • The server.Shutdown() method is called with the shutdown context as the argument. This initiates the graceful shutdown process.

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!