Graceful Shutdowns with signal.NotifyContext

February 25th, 2021
golang cli

Graceful shutdowns are an important part of any application, especially if that application modifies state. Before you “pull the plug” you should be responding to those HTTP requests, finishing off database interactions and closing off anything that might be left otherwise hanging or orphaned.

With the new signal.NotifyContext function that was released with Go 1.16, graceful shutdowns are easier than ever to add into your application.

Here is a simple web server with a single handler that will sleep for 10 seconds before responding. If you run this web server locally, execute a cURL or Postman request against it, then immediately send the interrupt signal with Ctrl+C. You’ll see the server gracefully shutdown by responding to the existing request before terminating. If the shutdown is taking too long another interrupt signal can be sent to exit immediately. Alternatively the timeout will kick in after 5 seconds.

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"
)

var server http.Server

func main() {
	// Create context that listens for the interrupt signal from the OS.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
	defer stop()

	server = http.Server{
		Addr: ":8080",
	}

	// Perform application startup.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(time.Second * 10)
		fmt.Fprint(w, "Hello world!")
	})

	// Listen on a different Goroutine so the application doesn't stop here.
	go server.ListenAndServe()

	// Listen for the interrupt signal.
	<-ctx.Done()

	// Restore default behavior on the interrupt signal and notify user of shutdown.
	stop()
	log.Println("shutting down gracefully, press Ctrl+C again to force")

	// Perform application shutdown with a maximum timeout of 5 seconds.
	timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	go func() {
		if err := server.Shutdown(timeoutCtx); err != nil {
			log.Fatalln(err)
		}
	}()

	select {
	case <-timeoutCtx.Done():
		if timeoutCtx.Err() == context.DeadlineExceeded {
			log.Fatalln("timeout exceeded, forcing shutdown")
		}

		os.Exit(0)
	}
}

Below you'll find some resources that I would recommend reading if you'd like a better understanding of the signal.NotifyContext function, the context package or the importance of graceful shutdowns.

  • signal.NotifyContext documentation.
  • JustForFunc Episode 9, The Context Package.
  • Wayne Ashley Berry's article on graceful shutdown with Go http servers and Kubernetes rolling updates. This is a great article showing a real world situation where this can be applied. Just note, this article was written pre Go 1.16 so the code snippets will show the older way of listening for signals from the operating system.
  • Some insightful comments and feedback about this post from some kind Gophers over on r/golang.