Gin Graceful Shutdown
When developing production-grade web applications with Gin, one crucial aspect that's often overlooked is implementing a proper shutdown mechanism. A graceful shutdown ensures that your application can terminate without abruptly cutting off ongoing requests, properly close database connections, and release other resources. In this tutorial, we'll explore how to implement graceful shutdown in Gin applications.
Why Graceful Shutdown Matters
Imagine a scenario where your web server is handling numerous requests when it receives a termination signal (e.g., when deploying a new version or when the server is being restarted). Without proper handling:
- In-progress requests might be abruptly terminated
- Database connections might not close properly
- File operations might remain incomplete
- Resources might not be released correctly
These issues can lead to data corruption, incomplete operations, and resource leaks. A graceful shutdown mechanism addresses these problems by:
- Stopping new requests from coming in
- Allowing in-progress requests to complete
- Closing database connections and cleaning up resources
- Exiting only when all operations are safely completed
Basic Implementation of Graceful Shutdown
Let's start with a basic implementation of graceful shutdown in a Gin application:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
// Set up Gin router
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
// Create an HTTP server with the Gin router
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start the server in a goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal to gracefully shut down the server
quit := make(chan os.Signal, 1)
// Kill (no param) default sends syscall.SIGTERM
// Kill -2 is syscall.SIGINT
// Kill -9 is syscall.SIGKILL but can't be caught, so don't need to add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create a deadline for the shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown the server
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exiting")
}
How It Works
Let's break down this implementation:
-
Start the server in a goroutine: This allows our main goroutine to continue and handle the shutdown signal.
-
Set up signal handling: We create a channel to receive operating system signals and wait for either SIGINT (Ctrl+C) or SIGTERM (termination signal).
-
Wait for shutdown signal: The
<-quit
statement blocks until a signal is received. -
Create a context with timeout: We use a context with a timeout to give our server a grace period to finish existing requests (5 seconds in this example).
-
Call server.Shutdown(): This stops the server from accepting new connections and gracefully closes existing ones.
Testing Graceful Shutdown
To test the graceful shutdown, you can make a few long-running requests to your server and then trigger a shutdown. Let's create an endpoint that simulates a long-running process:
router.GET("/long-process", func(c *gin.Context) {
// Simulate a long-running process
time.Sleep(10 * time.Second)
c.String(http.StatusOK, "Long process completed")
})
When you run the server and make a request to /long-process
, you have a 10-second window to trigger a shutdown (press Ctrl+C). You should observe:
- The server logs "Shutting down server..."
- Your request continues to process until completion
- After the response is sent, the server logs "Server exiting"
Advanced Graceful Shutdown Patterns
Custom Resource Cleanup
In real-world applications, you often need to close more than just the HTTP server. You might have database connections, cache clients, or other resources that need proper cleanup:
func main() {
// Initialize resources
router := gin.Default()
db := initDatabase() // Imagine this returns some database connection
cacheClient := initCacheClient() // Imagine this returns a cache client
// Set up routes
// ...
// Create server
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start the server in a goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create a deadline for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown server
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
// Close other resources
if err := db.Close(); err != nil {
log.Println("Error closing database:", err)
}
if err := cacheClient.Close(); err != nil {
log.Println("Error closing cache client:", err)
}
log.Println("Server exiting")
}
Using Multiple Servers
Sometimes, you might need to run multiple servers, such as an HTTP server and a gRPC server:
func main() {
// Initialize Gin router
router := gin.Default()
// Set up routes...
// HTTP server
httpServer := &http.Server{
Addr: ":8080",
Handler: router,
}
// Start HTTP server
go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server failed: %v", err)
}
}()
// Imagine we have a gRPC server too
grpcServer := initGRPCServer()
go func() {
if err := grpcServer.Serve(listen); err != nil {
log.Fatalf("gRPC server failed: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down servers...")
// Create a deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Shutdown HTTP server
if err := httpServer.Shutdown(ctx); err != nil {
log.Printf("HTTP server forced to shutdown: %v", err)
}
// Stop gRPC server
grpcServer.GracefulStop()
log.Println("Servers exited")
}
Real-World Example: REST API with Database
Let's create a more complete example that shows a REST API with a database connection:
package main
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
type App struct {
Router *gin.Engine
DB *sql.DB
}
func (a *App) Initialize() error {
// Initialize database
var err error
a.DB, err = sql.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
if err != nil {
return err
}
// Test database connection
if err = a.DB.Ping(); err != nil {
return err
}
// Configure database connection pool
a.DB.SetMaxIdleConns(5)
a.DB.SetMaxOpenConns(20)
a.DB.SetConnMaxLifetime(time.Hour)
// Initialize router
a.Router = gin.Default()
// Register routes
a.registerRoutes()
return nil
}
func (a *App) registerRoutes() {
a.Router.GET("/users", a.getUsers)
a.Router.GET("/users/:id", a.getUser)
// More routes...
}
func (a *App) getUsers(c *gin.Context) {
// Simulate database query
rows, err := a.DB.Query("SELECT id, name, email FROM users LIMIT 100")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
// Process results
users := []map[string]interface{}{}
for rows.Next() {
var id int
var name, email string
if err := rows.Scan(&id, &name, &email); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
users = append(users, map[string]interface{}{
"id": id,
"name": name,
"email": email,
})
}
c.JSON(http.StatusOK, users)
}
func (a *App) getUser(c *gin.Context) {
id := c.Param("id")
// Simulate a database query
var name, email string
err := a.DB.QueryRow("SELECT name, email FROM users WHERE id = ?", id).Scan(&name, &email)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"id": id,
"name": name,
"email": email,
})
}
func (a *App) Run(addr string) error {
srv := &http.Server{
Addr: addr,
Handler: a.Router,
}
// Start server in a goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown server
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
// Close database connection
if err := a.DB.Close(); err != nil {
log.Println("Error closing database:", err)
}
log.Println("Server exited properly")
return nil
}
func main() {
app := App{}
if err := app.Initialize(); err != nil {
log.Fatal("Failed to initialize app:", err)
}
log.Fatal(app.Run(":8080"))
}
In this example:
- We create an
App
struct that manages both the Gin router and the database connection - The
Initialize()
method sets up the database and router - The
Run()
method starts the server and implements graceful shutdown - When the server receives a shutdown signal, it completes ongoing requests and properly closes the database connection
Best Practices for Graceful Shutdown
-
Set appropriate timeouts: The timeout for shutdown should be long enough to allow legitimate requests to complete, but not so long that a misbehaving client can hold up your server.
-
Log the shutdown process: Make sure to log each step of the shutdown process to aid in debugging.
-
Close resources in the correct order: Generally, close the server first (to stop accepting new requests) and then close other resources like database connections.
-
Implement health checks: During shutdown, update your health checks to report that the server is shutting down, so load balancers can direct traffic elsewhere.
-
Test your shutdown process: Simulate shutdowns in testing and staging environments to ensure everything works as expected.
Handling Long-Running Operations
For very long operations, you might need additional strategies:
func main() {
router := gin.Default()
// Track active requests
activeRequests := make(map[string]context.CancelFunc)
var mu sync.Mutex
// Middleware to track requests
router.Use(func(c *gin.Context) {
requestID := c.Request.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String() // Generate a UUID
c.Request.Header.Set("X-Request-ID", requestID)
}
ctx, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(ctx)
mu.Lock()
activeRequests[requestID] = cancel
mu.Unlock()
defer func() {
mu.Lock()
delete(activeRequests, requestID)
mu.Unlock()
cancel()
}()
c.Next()
})
// Routes...
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Cancel all active requests if they exceed shutdown timeout
go func() {
<-time.After(25 * time.Second)
log.Println("Canceling remaining requests...")
mu.Lock()
for _, cancelFunc := range activeRequests {
cancelFunc()
}
mu.Unlock()
}()
// Shutdown server
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited properly")
}
This advanced example tracks all active requests and can forcefully cancel them if they're taking too long during shutdown.
Summary
Implementing graceful shutdown in your Gin application is crucial for production-ready services. It ensures that in-progress requests complete, resources are properly released, and your application can be deployed or restarted without disrupting users.
Key points to remember:
- Start your server in a goroutine so you can handle shutdown signals
- Use the built-in
Shutdown()
method of the HTTP server - Set appropriate timeouts for the shutdown process
- Close all resources properly (database connections, file handles, etc.)
- Log each step of the shutdown process for debugging
By following these practices, you'll create more robust web applications that behave properly during deployment cycles and system restarts.
Additional Resources
- Gin Framework Documentation
- HTTP Server Shutdown Documentation (Go)
- Context Package Documentation (Go)
- Signal Handling in Go
Exercises
- Implement graceful shutdown in an existing Gin application.
- Create a middleware that logs the duration of each request, including those that complete during shutdown.
- Extend the example to include other resources like Redis connections or file operations.
- Implement a feature that reports the shutdown status to health check endpoints so load balancers can stop sending traffic.
- Create a test that simulates a shutdown during high load and verifies that all requests are properly handled.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)