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:
- Start the HTTP server in a separate goroutine
- Wait for termination signals
- When a signal is received, call
server.Shutdown()
with a timeout context - 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:
- Encapsulating servers in a common interface
- Starting each server in its own goroutine
- Coordinating shutdown in a specific order
- Using a
WaitGroup
to ensure all servers have fully stopped