Graceful Shutdown Patterns

With the fundamentals established, let’s explore more sophisticated patterns for implementing graceful shutdown in different types of Go applications.

HTTP Server Graceful Shutdown

Go’s standard library provides built-in support for graceful shutdown of HTTP servers since Go 1.8. This allows existing connections to complete their requests before the server shuts down:

package main

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

func main() {
	// Create a new server
	server := &http.Server{
		Addr: ":8080",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Simulate a long-running request
			time.Sleep(5 * time.Second)
			fmt.Fprintf(w, "Hello, World!")
		}),
	}
	
	// Channel to listen for errors coming from the listener
	serverErrors := make(chan error, 1)
	
	// Start the server in a goroutine
	go func() {
		log.Printf("Server listening on %s", server.Addr)
		serverErrors <- server.ListenAndServe()
	}()
	
	// Channel to listen for interrupt signals
	shutdown := make(chan os.Signal, 1)
	signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
	
	// Block until we receive a signal or an error
	select {
	case err := <-serverErrors:
		log.Fatalf("Error starting server: %v", err)
		
	case sig := <-shutdown:
		log.Printf("Received signal: %v", sig)
		
		// Create a deadline for graceful shutdown
		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		
		// Gracefully shutdown the server
		log.Printf("Shutting down server gracefully with timeout: %s", ctx.Deadline())
		
		if err := server.Shutdown(ctx); err != nil {
			log.Printf("Server shutdown error: %v", err)
			// Force close if graceful shutdown fails
			if err := server.Close(); err != nil {
				log.Printf("Server close error: %v", err)
			}
		}
		
		log.Println("Server shutdown complete")
	}
}

Key aspects of this pattern:

  1. Start the HTTP server in a separate goroutine
  2. Wait for termination signals
  3. When a signal is received, call server.Shutdown() with a timeout context
  4. If graceful shutdown fails within the timeout, force close the server

Multiple Server Coordination

In real-world applications, you might need to coordinate the shutdown of multiple servers or services:

package main

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

type Server struct {
	name       string
	httpServer *http.Server
}

func NewServer(name string, addr string, handler http.Handler) *Server {
	return &Server{
		name: name,
		httpServer: &http.Server{
			Addr:    addr,
			Handler: handler,
		},
	}
}

func (s *Server) Start(wg *sync.WaitGroup) {
	defer wg.Done()
	
	log.Printf("%s server starting on %s", s.name, s.httpServer.Addr)
	
	if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed {
		log.Printf("%s server error: %v", s.name, err)
	}
	
	log.Printf("%s server stopped", s.name)
}

func (s *Server) Shutdown(ctx context.Context) error {
	log.Printf("Shutting down %s server...", s.name)
	return s.httpServer.Shutdown(ctx)
}

func main() {
	// Create API and metrics servers
	apiServer := NewServer("API", ":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second) // Simulate work
		fmt.Fprintf(w, "API response")
	}))
	
	metricsServer := NewServer("Metrics", ":9090", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Metrics data")
	}))
	
	// WaitGroup for tracking running servers
	var wg sync.WaitGroup
	
	// Start servers
	wg.Add(2)
	go apiServer.Start(&wg)
	go metricsServer.Start(&wg)
	
	// Channel to listen for interrupt signals
	shutdown := make(chan os.Signal, 1)
	signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM)
	
	// Wait for shutdown signal
	sig := <-shutdown
	log.Printf("Received signal: %v", sig)
	
	// Create a deadline for graceful shutdown
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	
	// Shutdown servers in order (metrics first, then API)
	if err := metricsServer.Shutdown(ctx); err != nil {
		log.Printf("Metrics server shutdown error: %v", err)
	}
	
	if err := apiServer.Shutdown(ctx); err != nil {
		log.Printf("API server shutdown error: %v", err)
	}
	
	// Wait for servers to finish
	log.Println("Waiting for servers to complete shutdown...")
	wg.Wait()
	log.Println("All servers shutdown complete")
}

This pattern demonstrates:

  1. Encapsulating servers in a common interface
  2. Starting each server in its own goroutine
  3. Coordinating shutdown in a specific order
  4. Using a WaitGroup to ensure all servers have fully stopped