From c4327f62e7e058773a028e63aad349e6941341cf Mon Sep 17 00:00:00 2001 From: John Guo Date: Thu, 19 Sep 2024 14:10:16 +0800 Subject: [PATCH] fix(net/ghttp): server shutdown not graceful using admin api `/debug/admin/shutdown` (#3777) --- net/ghttp/ghttp_server.go | 8 +++- net/ghttp/ghttp_server_admin.go | 12 ++++-- net/ghttp/ghttp_server_admin_process.go | 21 ++++++---- net/ghttp/ghttp_server_config.go | 22 ++++++----- net/ghttp/ghttp_server_config_api.go | 22 +++++++++++ net/ghttp/ghttp_server_config_mess.go | 35 ++++++++++++----- net/ghttp/ghttp_server_graceful.go | 1 + net/ghttp/ghttp_z_unit_feature_config_test.go | 39 +++++++++++++++++++ 8 files changed, 131 insertions(+), 29 deletions(-) create mode 100644 net/ghttp/ghttp_server_config_api.go diff --git a/net/ghttp/ghttp_server.go b/net/ghttp/ghttp_server.go index 75944a489..80976ee97 100644 --- a/net/ghttp/ghttp_server.go +++ b/net/ghttp/ghttp_server.go @@ -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) } diff --git a/net/ghttp/ghttp_server_admin.go b/net/ghttp/ghttp_server_admin.go index 2928493a4..109cacb13 100644 --- a/net/ghttp/ghttp_server_admin.go +++ b/net/ghttp/ghttp_server_admin.go @@ -36,8 +36,14 @@ func (p *utilAdmin) Index(r *Request) {

Pid: {{.pid}}

File Path: {{.path}}

-

Restart

-

Shutdown

+

+Restart +please make sure it is running using standalone binary not from IDE or "go run" +

+

+Shutdown +graceful shutdown the server +

`, 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 } diff --git a/net/ghttp/ghttp_server_admin_process.go b/net/ghttp/ghttp_server_admin_process.go index 3c8687824..31313f6aa 100644 --- a/net/ghttp/ghttp_server_admin_process.go +++ b/net/ghttp/ghttp_server_admin_process.go @@ -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 { diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index 22163051e..5daa99d4d 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -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. diff --git a/net/ghttp/ghttp_server_config_api.go b/net/ghttp/ghttp_server_config_api.go new file mode 100644 index 000000000..abbf79b01 --- /dev/null +++ b/net/ghttp/ghttp_server_config_api.go @@ -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 +} diff --git a/net/ghttp/ghttp_server_config_mess.go b/net/ghttp/ghttp_server_config_mess.go index 441138778..c822eb914 100644 --- a/net/ghttp/ghttp_server_config_mess.go +++ b/net/ghttp/ghttp_server_config_mess.go @@ -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 } diff --git a/net/ghttp/ghttp_server_graceful.go b/net/ghttp/ghttp_server_graceful.go index cc3c30157..a86475378 100644 --- a/net/ghttp/ghttp_server_graceful.go +++ b/net/ghttp/ghttp_server_graceful.go @@ -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 diff --git a/net/ghttp/ghttp_z_unit_feature_config_test.go b/net/ghttp/ghttp_z_unit_feature_config_test.go index e408e4f62..c84cf2425 100644 --- a/net/ghttp/ghttp_z_unit_feature_config_test.go +++ b/net/ghttp/ghttp_z_unit_feature_config_test.go @@ -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) + }) +}