Skip to content

Commit 67d083f

Browse files
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)
1 parent b2b489d commit 67d083f

File tree

3 files changed

+386
-0
lines changed

3 files changed

+386
-0
lines changed

gin.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package gin
66

77
import (
8+
"context"
89
"fmt"
910
"html/template"
1011
"net"
@@ -186,6 +187,11 @@ type Engine struct {
186187
maxSections uint16
187188
trustedProxies []string
188189
trustedCIDRs []*net.IPNet
190+
191+
// server holds a reference to the HTTP server for graceful shutdown.
192+
// This is set when one of the Run* methods is called.
193+
server *http.Server
194+
serverLock sync.Mutex
189195
}
190196

191197
var _ IRouter = (*Engine)(nil)
@@ -534,6 +540,30 @@ func parseIP(ip string) net.IP {
534540
return parsedIP
535541
}
536542

543+
// Shutdown gracefully shuts down the server without interrupting any active connections.
544+
// Shutdown works by first closing all open listeners, then closing all idle connections,
545+
// and then waiting indefinitely for connections to return to idle and then shut down.
546+
// If the provided context expires before the shutdown is complete, Shutdown returns the
547+
// context's error, otherwise it returns any error returned from closing the Server's
548+
// underlying Listener(s).
549+
//
550+
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately
551+
// return ErrServerClosed. Make sure the program doesn't exit and waits instead for
552+
// Shutdown to return.
553+
//
554+
// This method returns nil if the server has not been started.
555+
func (engine *Engine) Shutdown(ctx context.Context) error {
556+
engine.serverLock.Lock()
557+
srv := engine.server
558+
engine.serverLock.Unlock()
559+
560+
if srv == nil {
561+
return nil
562+
}
563+
564+
return srv.Shutdown(ctx)
565+
}
566+
537567
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
538568
// It is a shortcut for http.ListenAndServe(addr, router)
539569
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@@ -551,6 +581,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
551581
Addr: address,
552582
Handler: engine.Handler(),
553583
}
584+
585+
engine.serverLock.Lock()
586+
engine.server = server
587+
engine.serverLock.Unlock()
588+
554589
err = server.ListenAndServe()
555590
return
556591
}
@@ -571,6 +606,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
571606
Addr: addr,
572607
Handler: engine.Handler(),
573608
}
609+
610+
engine.serverLock.Lock()
611+
engine.server = server
612+
engine.serverLock.Unlock()
613+
574614
err = server.ListenAndServeTLS(certFile, keyFile)
575615
return
576616
}
@@ -597,6 +637,11 @@ func (engine *Engine) RunUnix(file string) (err error) {
597637
server := &http.Server{ // #nosec G112
598638
Handler: engine.Handler(),
599639
}
640+
641+
engine.serverLock.Lock()
642+
engine.server = server
643+
engine.serverLock.Unlock()
644+
600645
err = server.Serve(listener)
601646
return
602647
}
@@ -654,6 +699,11 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
654699
server := &http.Server{ // #nosec G112
655700
Handler: engine.Handler(),
656701
}
702+
703+
engine.serverLock.Lock()
704+
engine.server = server
705+
engine.serverLock.Unlock()
706+
657707
err = server.Serve(listener)
658708
return
659709
}

graceful.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package gin
6+
7+
import (
8+
"context"
9+
"errors"
10+
"net/http"
11+
"os"
12+
"os/signal"
13+
"syscall"
14+
"time"
15+
)
16+
17+
// ShutdownConfig holds configuration for graceful shutdown.
18+
type ShutdownConfig struct {
19+
// Timeout is the maximum duration to wait for active connections to finish.
20+
// Default: 10 seconds
21+
Timeout time.Duration
22+
23+
// Signals are the OS signals that will trigger shutdown.
24+
// Default: SIGINT, SIGTERM
25+
Signals []os.Signal
26+
}
27+
28+
// RunWithShutdown starts the HTTP server and handles graceful shutdown on SIGINT/SIGTERM.
29+
// It blocks until the server is shut down.
30+
// The timeout parameter specifies the maximum duration to wait for active connections to finish.
31+
func (engine *Engine) RunWithShutdown(addr string, timeout time.Duration) error {
32+
return engine.RunWithShutdownConfig(addr, ShutdownConfig{
33+
Timeout: timeout,
34+
Signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
35+
})
36+
}
37+
38+
// RunWithShutdownConfig starts the HTTP server with custom shutdown configuration.
39+
// It blocks until the server is shut down.
40+
func (engine *Engine) RunWithShutdownConfig(addr string, config ShutdownConfig) error {
41+
if config.Timeout == 0 {
42+
config.Timeout = 10 * time.Second
43+
}
44+
if len(config.Signals) == 0 {
45+
config.Signals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
46+
}
47+
48+
ctx, stop := signal.NotifyContext(context.Background(), config.Signals...)
49+
defer stop()
50+
51+
errCh := make(chan error, 1)
52+
go func() {
53+
if err := engine.Run(addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
54+
errCh <- err
55+
}
56+
close(errCh)
57+
}()
58+
59+
select {
60+
case err := <-errCh:
61+
return err
62+
case <-ctx.Done():
63+
}
64+
65+
shutdownCtx, cancel := context.WithTimeout(context.Background(), config.Timeout)
66+
defer cancel()
67+
68+
return engine.Shutdown(shutdownCtx)
69+
}

0 commit comments

Comments
 (0)