gin/graceful_test.go
Rikiya Narita f894f4c7ed feat(engine): add Shutdown method for graceful shutdown support
- Add server and serverLock fields to Engine struct
- Add Shutdown() method that calls http.Server.Shutdown()
- Modify Run/RunTLS/RunUnix/RunListener to store server reference
- Add RunWithShutdown convenience method with signal handling
- Add comprehensive tests for graceful shutdown (8 test cases)
- Fix lint errors (errorlint, testifylint, errcheck)
2026-01-19 11:29:52 +09:00

268 lines
6.1 KiB
Go

// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"context"
"net"
"net/http"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEngineShutdown(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Start server in goroutine
go func() {
err := router.Run(":18080")
assert.ErrorIs(t, err, http.ErrServerClosed)
}()
time.Sleep(100 * time.Millisecond) // Wait for server start
// Verify server is running
resp, err := http.Get("http://localhost:18080/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = router.Shutdown(ctx)
require.NoError(t, err)
// Wait a moment for server to fully stop
time.Sleep(50 * time.Millisecond)
// Verify server is stopped
_, err = http.Get("http://localhost:18080/")
require.Error(t, err)
}
func TestEngineShutdownBeforeStart(t *testing.T) {
router := New()
// Shutdown before starting should not error
err := router.Shutdown(context.Background())
require.NoError(t, err)
}
func TestEngineShutdownTLS(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Start TLS server in goroutine
go func() {
err := router.RunTLS(":18443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")
assert.ErrorIs(t, err, http.ErrServerClosed)
}()
time.Sleep(100 * time.Millisecond) // Wait for server start
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := router.Shutdown(ctx)
require.NoError(t, err)
}
func TestEngineShutdownWithActiveRequest(t *testing.T) {
router := New()
requestStarted := make(chan struct{})
requestDone := make(chan struct{})
router.GET("/slow", func(c *Context) {
close(requestStarted)
time.Sleep(500 * time.Millisecond) // Simulate slow request
c.String(http.StatusOK, "done")
close(requestDone)
})
// Start server
go func() {
_ = router.Run(":18081")
}()
time.Sleep(100 * time.Millisecond)
// Start slow request
go func() {
resp, err := http.Get("http://localhost:18081/slow")
if err == nil {
resp.Body.Close()
}
}()
// Wait for request to start
<-requestStarted
// Initiate shutdown while request is in progress
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
shutdownDone := make(chan error, 1)
go func() {
shutdownDone <- router.Shutdown(ctx)
}()
// Verify request completes before shutdown finishes
select {
case <-requestDone:
// Request completed - this is expected
case err := <-shutdownDone:
t.Errorf("Shutdown completed before request finished: %v", err)
}
// Wait for shutdown to complete
err := <-shutdownDone
require.NoError(t, err)
}
func TestRunWithShutdown(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdown(":18082", 5*time.Second)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18082/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send shutdown signal to self
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGINT)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownConfig(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
config := ShutdownConfig{
Timeout: 5 * time.Second,
Signals: []os.Signal{syscall.SIGUSR1},
}
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdownConfig(":18083", config)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18083/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send custom signal
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGUSR1)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownConfigDefaults(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Test with zero values to check defaults are applied
config := ShutdownConfig{}
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdownConfig(":18084", config)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18084/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send SIGINT (default signal)
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGINT)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(15 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownServerError(t *testing.T) {
router := New()
// Start a server on the same port first
listener, err := net.Listen("tcp", ":18085")
require.NoError(t, err)
defer listener.Close()
// Try to run on the same port - should fail
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdown(":18085", 5*time.Second)
}()
// Should get an error because port is already in use
select {
case err := <-errCh:
require.Error(t, err)
case <-time.After(2 * time.Second):
t.Fatal("Expected error but timed out")
}
}