From 278acb670271033c86c45cc4a61d91ce86a8989b Mon Sep 17 00:00:00 2001 From: Farzad Ghanei <644113+farzadghanei@users.noreply.github.com> Date: Sat, 27 Apr 2024 10:51:55 +0200 Subject: [PATCH] feat: Rework shutting down for HTTP server to use a signal string Revised `run_modes.go` to stop the HTTP server if the "X-Server-Shutdown" header value matches the `shutdownSignalHeaderValue`. Now it's possible to shutdown the server based on header set in the request rather than closing after a set number of responses. This provides more efficiency and control over the server's operations. --- cmd/chkok_test.go | 1 + examples/config.yaml | 6 +++++- examples/test-http.yaml | 2 +- internal/conf.go | 24 +++++++++++----------- internal/conf_test.go | 44 ++++++++++++++++++++++++----------------- internal/run_modes.go | 15 +++++++------- 6 files changed, 53 insertions(+), 39 deletions(-) diff --git a/cmd/chkok_test.go b/cmd/chkok_test.go index 4a25f88..dda7de2 100644 --- a/cmd/chkok_test.go +++ b/cmd/chkok_test.go @@ -46,6 +46,7 @@ func TestRunHttp(t *testing.T) { if err != nil { t.Fatalf("Failed to create HTTP request: %v", err) } + req.Header.Set("X-Server-Shutdown", "test-shutdown-signal") // shutdown the server after the request // Send the request multiple times, waiting for the server to // start up and respond diff --git a/examples/config.yaml b/examples/config.yaml index 45c5056..cbcb1c4 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -3,8 +3,12 @@ runners: default: timeout: 5m - shutdown_after_requests: 100 # mainly useful for testing http mode listen_address: "127.0.0.1:51234" + http: + # shutdown_signal_header is mainly useful for testing http mode, do not set it in production + # if set, better be treated like a secret, and a secure transport layer should be used. + # this is the value set on "X-Shutdown-Signal" header in the http request + # shutdown_signal_header: "test-shutdown-signal" check_suites: etc: diff --git a/examples/test-http.yaml b/examples/test-http.yaml index f7cd982..60cce5a 100644 --- a/examples/test-http.yaml +++ b/examples/test-http.yaml @@ -7,7 +7,7 @@ runners: http: timeout: 5s listen_address: "127.0.0.1:51234" - shutdown_after_requests: 1 # stop http server after 1 request, used for testing http mode + shutdown_signal_header: "test-shutdown-signal" # mainly useful for testing http mode request_read_timeout: 2s response_write_timeout: 2s diff --git a/internal/conf.go b/internal/conf.go index 6bae6c3..c2ea9b0 100644 --- a/internal/conf.go +++ b/internal/conf.go @@ -21,11 +21,11 @@ type Conf struct { // ConfRunner is config for the check runners type ConfRunner struct { - Timeout time.Duration - ShutdownAfterRequests uint32 `yaml:"shutdown_after_requests"` - ListenAddress string `yaml:"listen_address"` - RequestReadTimeout time.Duration `yaml:"request_read_timeout"` - ResponseWriteTimeout time.Duration `yaml:"response_write_timeout"` + Timeout time.Duration + ShutdownSignalHeader *string `yaml:"shutdown_signal_header"` + ListenAddress string `yaml:"listen_address"` + RequestReadTimeout time.Duration `yaml:"request_read_timeout"` + ResponseWriteTimeout time.Duration `yaml:"response_write_timeout"` } // ConfCheckSpec is the spec for each check configuration @@ -73,18 +73,18 @@ func GetConfRunner(runners *ConfRunners, name string) (ConfRunner, bool) { // Merge the requested runner with the default runner mergedConf := ConfRunner{ - Timeout: namedConf.Timeout, - ShutdownAfterRequests: namedConf.ShutdownAfterRequests, - ListenAddress: namedConf.ListenAddress, - RequestReadTimeout: namedConf.RequestReadTimeout, - ResponseWriteTimeout: namedConf.ResponseWriteTimeout, + Timeout: namedConf.Timeout, + ShutdownSignalHeader: namedConf.ShutdownSignalHeader, + ListenAddress: namedConf.ListenAddress, + RequestReadTimeout: namedConf.RequestReadTimeout, + ResponseWriteTimeout: namedConf.ResponseWriteTimeout, } if mergedConf.Timeout == 0 { mergedConf.Timeout = defaultConf.Timeout } - if mergedConf.ShutdownAfterRequests == 0 { - mergedConf.ShutdownAfterRequests = defaultConf.ShutdownAfterRequests + if mergedConf.ShutdownSignalHeader == nil { + mergedConf.ShutdownSignalHeader = defaultConf.ShutdownSignalHeader } if mergedConf.ListenAddress == "" { mergedConf.ListenAddress = defaultConf.ListenAddress diff --git a/internal/conf_test.go b/internal/conf_test.go index 6443500..3745d53 100644 --- a/internal/conf_test.go +++ b/internal/conf_test.go @@ -37,11 +37,6 @@ func TestReadConf(t *testing.T) { if runner.Timeout.Minutes() != float64(wantMinutes) { t.Errorf("invalid read conf default runner, want %v timeout got %v", wantMinutes, runner.Timeout.Minutes()) } - var wantShutdownAfterRequests uint32 = 100 - if runner.ShutdownAfterRequests != wantShutdownAfterRequests { - t.Errorf("invalid read conf, want %v shutdown after requests got %v", - wantShutdownAfterRequests, runner.ShutdownAfterRequests) - } etcChecks, ok := conf.CheckSuites["etc"] if !ok { t.Errorf("read conf found no etc checks") @@ -66,18 +61,19 @@ func TestReadConf(t *testing.T) { } func TestGetConfRunner(t *testing.T) { + var shutdownSignalHeader = "Test-Shutdown" runners := ConfRunners{ "default": ConfRunner{ - Timeout: 5 * time.Second, - ShutdownAfterRequests: 10, - ListenAddress: "localhost:8080", + Timeout: 5 * time.Second, + ListenAddress: "localhost:8080", }, + "testMinimalHttpRunner": ConfRunner{}, "testHttpRunner": ConfRunner{ - Timeout: 10 * time.Second, - ShutdownAfterRequests: 20, - ListenAddress: "localhost:9090", - RequestReadTimeout: 5 * time.Second, - ResponseWriteTimeout: 5 * time.Second, + Timeout: 10 * time.Second, + ShutdownSignalHeader: &shutdownSignalHeader, + ListenAddress: "localhost:9090", + RequestReadTimeout: 5 * time.Second, + ResponseWriteTimeout: 5 * time.Second, }, } @@ -91,11 +87,11 @@ func TestGetConfRunner(t *testing.T) { name: "Existing runner", runnerName: "testHttpRunner", expectedRunner: ConfRunner{ - Timeout: 10 * time.Second, - ShutdownAfterRequests: 20, - ListenAddress: "localhost:9090", - RequestReadTimeout: 5 * time.Second, - ResponseWriteTimeout: 5 * time.Second, + Timeout: 10 * time.Second, + ShutdownSignalHeader: &shutdownSignalHeader, + ListenAddress: "localhost:9090", + RequestReadTimeout: 5 * time.Second, + ResponseWriteTimeout: 5 * time.Second, }, expectedExists: true, }, @@ -105,6 +101,18 @@ func TestGetConfRunner(t *testing.T) { expectedRunner: runners["default"], expectedExists: true, }, + { + name: "Minimal http runner", + runnerName: "testMinimalHttpRunner", + expectedRunner: ConfRunner{ + Timeout: 5 * time.Second, + ShutdownSignalHeader: nil, + ListenAddress: "localhost:8080", + RequestReadTimeout: 0, + ResponseWriteTimeout: 0, + }, + expectedExists: true, + }, { name: "Default runner", runnerName: "default", diff --git a/internal/run_modes.go b/internal/run_modes.go index cfe2709..42815b9 100644 --- a/internal/run_modes.go +++ b/internal/run_modes.go @@ -43,7 +43,10 @@ func httpRequestAsString(r *http.Request) string { // RunModeHTTP runs app in http server mode using the provided config, return exit code func RunModeHTTP(checkGroups *CheckSuites, conf *ConfRunner, logger *log.Logger) int { timeout := conf.Timeout - shutdownAfterRequests := conf.ShutdownAfterRequests + shutdownSignalHeaderValue := "" + if conf.ShutdownSignalHeader != nil { + shutdownSignalHeaderValue = *conf.ShutdownSignalHeader + } listenAddress := conf.ListenAddress requestReadTimeout := conf.RequestReadTimeout responseWriteTimeout := conf.ResponseWriteTimeout @@ -101,7 +104,7 @@ func RunModeHTTP(checkGroups *CheckSuites, conf *ConfRunner, logger *log.Logger) for request = range reqHandlerChan { atomic.AddUint32(&count, 1) logger.Printf("request [%v] is processed: %v", count, httpRequestAsString(request)) - if shutdownAfterRequests > 0 && atomic.LoadUint32(&count) >= shutdownAfterRequests { + if shutdownSignalHeaderValue != "" && request.Header.Get("X-Server-Shutdown") == shutdownSignalHeaderValue { if err := server.Shutdown(timeoutCtx); err != nil { logger.Printf("http server shutdown failed: %v", err) } @@ -113,11 +116,9 @@ func RunModeHTTP(checkGroups *CheckSuites, conf *ConfRunner, logger *log.Logger) logger.Printf("starting http server ...") err := server.ListenAndServe() close(reqHandlerChan) - if err != nil { - if atomic.LoadUint32(&count) < 1 { // server didn't handle any requests - logger.Printf("http server failed to start: %v", err) - return ExSoftware - } + if err != nil && err != http.ErrServerClosed { + logger.Printf("http server failed to start: %v", err) + return ExSoftware } logger.Printf("http server shutdown!") return ExOK