When developing HTTP services in Go, you will start and stop your server hundreds of times, think of the whole go run main.go
ctrl + c
loop!
When you hit ctrl + c
, (on UNIX systems) your program is passed the SIGINT
Unix signal. The go runtime handles this and will stop your program relatively safely, but it’s not great practice!
What if you have database connection pools open or forgot to close a file etc.?
This is where graceful shutdown comes in 🎉
This is going to be a super short post just showing a good example of implementing graceful shutdown of a Go HTTP server.
The Bad Way
A simple web server that connects to a SQL database without graceful shutdown might look something like this:
package main
import (
"context"
"database/sql"
"flag"
"net/http"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/sirupsen/logrus"
)
const (
defaultPort = ":8000"
defaultDSN = ""
)
type application struct {
logger *logrus.Logger
}
func main() {
// Accept command line flags for configuration and secrets
port := flag.String("port", defaultPort, "HTTP network address")
dsn := flag.String("dsn", defaultDSN, "DB Connection string")
flag.Parse()
// Set up logger
log := logrus.New()
log.Out = os.Stdout
if *dsn == "" {
log.Fatalln("dsn must not be empty")
}
log.Infoln("Establishing db connection")
db, err := openDB(*dsn)
if err != nil {
log.WithError(err).Fatalln("Error connecting to DB")
}
defer func() {
log.Infoln("Closing DB connection")
db.Close()
}()
app := &application{
logger: log,
}
srv := &http.Server{
Addr: *port,
Handler: app.routes(),
ReadTimeout: 5 * time.Second, // max time to read request from the client
WriteTimeout: 10 * time.Second, // max time to write response to the client
IdleTimeout: 60 * time.Second, // max time for connections using TCP Keep-Alive
}
log.WithField("port", *port).Infoln("Starting server on port")
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.WithError(err).Errorln("Error starting server")
}
}
func openDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}
Notice how we just start the server and log the error here:
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.WithError(err).Errorln("Error starting server")
}
Notice also how we close our database connection in a defer
function:
db, err := openDB(*dsn)
if err != nil {
log.WithError(err).Fatalln("Error connecting to DB")
}
defer func() {
log.Infoln("Closing DB connection")
db.Close()
}()
In go, when your program is interrupted with SIGINT
deferred functions aren’t run, so we’re relying on the garbage collector to close our database connection. The Go garbage collector is a work of art and is very good, but relying on it when we could handle this better is just bad practice!
A Better Way
Let’s handle the SIGINT
and gracefully close down the server!
All we need to do is add the following block to our main
function:
// start the server in a goroutine so it runs off doing it's own thing
go func() {
log.WithField("port", *port).Infoln("Starting server on port")
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.WithError(err).Errorln("Error starting server")
}
}()
// trap sigterm or interrupt and gracefully shutdown the server
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, os.Kill)
// Block the rest of the code until a signal is received.
sig := <-c
log.WithField("sig", sig).Infoln("Got signal")
log.Infoln("Shutting everything down gracefully")
// gracefully shutdown the server, waiting max 30 seconds for current operations to complete
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.WithError(err).Fatalln("Graceful shutdown failed")
}
log.Infoln("Server shutdown successfully")
So here we:
- Replace the call to
ListenAndServe
with one running inside an anonymous goroutine so it goes off and runs without blocking ourmain
- Make a channel on which to pass
SIGINT
andSIGTERM
- Register the signals using
signal.Notify
- Then we try and pull the signal off the channel. Receiving from a channel in go is a blocking operation, meaning the code will not progress past this point unless something (in this case either
SIGINT
orSIGTERM
) is sent on the channel. Remember, the whole time ourmain
is blocked here, our server is off in it’s own goroutine happily serving HTTP! - If we do get a signal,
main
unblocks and we then usecontext
to gracefully shutdown the server. - Now because we’ve “trapped” the signal sent by
ctrl + c
, our main will exit normally, meaning all thedefer
functions will run, and our DB connection pool will be cleaned up nicely!