Go Graceful Shutdown with signal.NotifyContext
Drain in-flight HTTP requests on SIGINT or SIGTERM with http.Server.Shutdown, a bounded grace period, and a clean Serve error check.
Code
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
mux := http.NewServeMux()
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(300 * time.Millisecond):
fmt.Fprintln(w, "finished")
case <-r.Context().Done():
log.Printf("client went away: %v", r.Context().Err())
}
})
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, "ok")
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
server := &http.Server{
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
IdleTimeout: 30 * time.Second,
}
serveErr := make(chan error, 1)
go func() {
serveErr <- server.Serve(listener)
}()
clientDone := make(chan struct{})
go func() {
defer close(clientDone)
resp, err := http.Get("http://" + listener.Addr().String() + "/slow")
if err != nil {
log.Printf("request error: %v", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Printf("client got %q", body)
}()
time.Sleep(50 * time.Millisecond)
stop() // Simulates the signal after the request is already in flight.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
} else {
log.Println("stopped accepting new connections and drained in-flight work")
}
<-clientDone
if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("serve error: %v", err)
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.