fix(net/ghttp): server shutdown not graceful using admin api /debug/admin/shutdown (#3777)

This commit is contained in:
John Guo 2024-09-19 14:10:16 +08:00 committed by GitHub
parent fd33dcb97b
commit c4327f62e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 131 additions and 29 deletions

View File

@ -247,7 +247,13 @@ func (s *Server) Start() error {
// If this is a child process, it then notifies its parent exit.
if gproc.IsChild() {
gtimer.SetTimeout(ctx, time.Duration(s.config.GracefulTimeout)*time.Second, func(ctx context.Context) {
var gracefulTimeout = time.Duration(s.config.GracefulTimeout) * time.Second
gtimer.SetTimeout(ctx, gracefulTimeout, func(ctx context.Context) {
intlog.Printf(
ctx,
`pid[%d]: notice parent server graceful shuttingdown, ppid: %d`,
gproc.Pid(), gproc.PPid(),
)
if err := gproc.Send(gproc.PPid(), []byte("exit"), adminGProcCommGroup); err != nil {
intlog.Errorf(ctx, `server error in process communication: %+v`, err)
}

View File

@ -36,8 +36,14 @@ func (p *utilAdmin) Index(r *Request) {
<body>
<p>Pid: {{.pid}}</p>
<p>File Path: {{.path}}</p>
<p><a href="{{$.uri}}/restart">Restart</a></p>
<p><a href="{{$.uri}}/shutdown">Shutdown</a></p>
<p>
<a href="{{$.uri}}/restart">Restart</a>
please make sure it is running using standalone binary not from IDE or "go run"
</p>
<p>
<a href="{{$.uri}}/shutdown">Shutdown</a>
graceful shutdown the server
</p>
</body>
</html>
`, data)
@ -89,7 +95,7 @@ func (s *Server) Shutdown() error {
// Only shut down current servers.
// It may have multiple underlying http servers.
for _, v := range s.servers {
v.close(ctx)
v.shutdown(ctx)
}
return nil
}

View File

@ -21,6 +21,7 @@ import (
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/gproc"
"github.com/gogf/gf/v2/os/gtime"
@ -55,7 +56,10 @@ var (
// The optional parameter `newExeFilePath` specifies the new binary file for creating process.
func RestartAllServer(ctx context.Context, newExeFilePath string) error {
if !gracefulEnabled {
return gerror.NewCode(gcode.CodeInvalidOperation, "graceful reload feature is disabled")
return gerror.NewCode(
gcode.CodeInvalidOperation,
"graceful reload feature is disabled",
)
}
serverActionLocker.Lock()
defer serverActionLocker.Unlock()
@ -115,13 +119,16 @@ func checkActionFrequency() error {
// forkReloadProcess creates a new child process and copies the fd to child process.
func forkReloadProcess(ctx context.Context, newExeFilePath ...string) error {
var (
path = os.Args[0]
binaryPath = os.Args[0]
)
if len(newExeFilePath) > 0 && newExeFilePath[0] != "" {
path = newExeFilePath[0]
binaryPath = newExeFilePath[0]
}
if !gfile.Exists(binaryPath) {
return gerror.Newf(`binary file path "%s" does not exist`, binaryPath)
}
var (
p = gproc.NewProcess(path, os.Args[1:], os.Environ())
p = gproc.NewProcess(binaryPath, os.Args[1:], os.Environ())
sfm = getServerFdMap()
)
for name, m := range sfm {
@ -145,9 +152,9 @@ func forkReloadProcess(ctx context.Context, newExeFilePath ...string) error {
buffer, _ := gjson.Encode(sfm)
p.Env = append(p.Env, adminActionReloadEnvKey+"="+string(buffer))
if _, err := p.Start(ctx); err != nil {
glog.Errorf(
intlog.Errorf(
ctx,
"%d: fork process failed, error:%s, %s",
"%d: fork process failed, error: %s, %s",
gproc.Pid(), err.Error(), string(buffer),
)
return err
@ -254,7 +261,7 @@ func shutdownWebServersGracefully(ctx context.Context, signal os.Signal) {
gproc.Pid(), signal.String(),
)
} else {
glog.Printf(ctx, "%d: server gracefully shutting down by api", gproc.Pid())
glog.Printf(ctx, "pid[%d]: server gracefully shutting down by api", gproc.Pid())
}
serverMapping.RLockFunc(func(m map[string]interface{}) {
for _, v := range m {

View File

@ -230,6 +230,19 @@ type ServerConfig struct {
SwaggerPath string `json:"swaggerPath"` // SwaggerPath specifies the swagger UI path for route registering.
SwaggerUITemplate string `json:"swaggerUITemplate"` // SwaggerUITemplate specifies the swagger UI custom template
// ======================================================================================================
// Graceful reload & shutdown.
// ======================================================================================================
// Graceful enables graceful reload feature for all servers of the process.
Graceful bool `json:"graceful"`
// GracefulTimeout set the maximum survival time (seconds) of the parent process.
GracefulTimeout int `json:"gracefulTimeout"`
// GracefulShutdownTimeout set the maximum survival time (seconds) before stopping the server.
GracefulShutdownTimeout int `json:"gracefulShutdownTimeout"`
// ======================================================================================================
// Other.
// ======================================================================================================
@ -254,15 +267,6 @@ type ServerConfig struct {
// DumpRouterMap specifies whether automatically dumps router map when server starts.
DumpRouterMap bool `json:"dumpRouterMap"`
// Graceful enables graceful reload feature for all servers of the process.
Graceful bool `json:"graceful"`
// GracefulTimeout set the maximum survival time (seconds) of the parent process.
GracefulTimeout uint8 `json:"gracefulTimeout"`
// GracefulShutdownTimeout set the maximum survival time (seconds) before stopping the server.
GracefulShutdownTimeout uint8 `json:"gracefulShutdownTimeout"`
}
// NewConfig creates and returns a ServerConfig object with default configurations.

View File

@ -0,0 +1,22 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package ghttp
// SetSwaggerPath sets the SwaggerPath for server.
func (s *Server) SetSwaggerPath(path string) {
s.config.SwaggerPath = path
}
// SetSwaggerUITemplate sets the Swagger template for server.
func (s *Server) SetSwaggerUITemplate(swaggerUITemplate string) {
s.config.SwaggerUITemplate = swaggerUITemplate
}
// SetOpenApiPath sets the OpenApiPath for server.
func (s *Server) SetOpenApiPath(path string) {
s.config.OpenApiPath = path
}

View File

@ -27,17 +27,34 @@ func (s *Server) SetFormParsingMemory(maxMemory int64) {
s.config.FormParsingMemory = maxMemory
}
// SetSwaggerPath sets the SwaggerPath for server.
func (s *Server) SetSwaggerPath(path string) {
s.config.SwaggerPath = path
// SetGraceful sets the Graceful for server.
func (s *Server) SetGraceful(graceful bool) {
s.config.Graceful = graceful
// note: global setting.
gracefulEnabled = graceful
}
// SetSwaggerUITemplate sets the Swagger template for server.
func (s *Server) SetSwaggerUITemplate(swaggerUITemplate string) {
s.config.SwaggerUITemplate = swaggerUITemplate
// GetGraceful returns the Graceful for server.
func (s *Server) GetGraceful() bool {
return s.config.Graceful
}
// SetOpenApiPath sets the OpenApiPath for server.
func (s *Server) SetOpenApiPath(path string) {
s.config.OpenApiPath = path
// SetGracefulTimeout sets the GracefulTimeout for server.
func (s *Server) SetGracefulTimeout(gracefulTimeout int) {
s.config.GracefulTimeout = gracefulTimeout
}
// GetGracefulTimeout returns the GracefulTimeout for server.
func (s *Server) GetGracefulTimeout() int {
return s.config.GracefulTimeout
}
// SetGracefulShutdownTimeout sets the GracefulShutdownTimeout for server.
func (s *Server) SetGracefulShutdownTimeout(gracefulShutdownTimeout int) {
s.config.GracefulShutdownTimeout = gracefulShutdownTimeout
}
// GetGracefulShutdownTimeout returns the GracefulShutdownTimeout for server.
func (s *Server) GetGracefulShutdownTimeout() int {
return s.config.GracefulShutdownTimeout
}

View File

@ -259,6 +259,7 @@ func (s *gracefulServer) getRawListener() net.Listener {
}
// close shuts down the server forcibly.
// for graceful shutdown, please use gracefulServer.shutdown.
func (s *gracefulServer) close(ctx context.Context) {
if s.status.Val() == ServerStatusStopped {
return

View File

@ -163,3 +163,42 @@ func Test_ClientMaxBodySize_File(t *testing.T) {
)
})
}
func Test_Config_Graceful(t *testing.T) {
var (
defaultConfig = ghttp.NewConfig()
expect = true
)
gtest.C(t, func(t *gtest.T) {
s := g.Server(guid.S())
t.Assert(s.GetGraceful(), defaultConfig.Graceful)
s.SetGraceful(expect)
t.Assert(s.GetGraceful(), expect)
})
}
func Test_Config_GracefulTimeout(t *testing.T) {
var (
defaultConfig = ghttp.NewConfig()
expect = 3
)
gtest.C(t, func(t *gtest.T) {
s := g.Server(guid.S())
t.Assert(s.GetGracefulTimeout(), defaultConfig.GracefulTimeout)
s.SetGracefulTimeout(expect)
t.Assert(s.GetGracefulTimeout(), expect)
})
}
func Test_Config_GracefulShutdownTimeout(t *testing.T) {
var (
defaultConfig = ghttp.NewConfig()
expect = 10
)
gtest.C(t, func(t *gtest.T) {
s := g.Server(guid.S())
t.Assert(s.GetGracefulShutdownTimeout(), defaultConfig.GracefulShutdownTimeout)
s.SetGracefulShutdownTimeout(expect)
t.Assert(s.GetGracefulShutdownTimeout(), expect)
})
}