From 605ea3849f44047781a8d780886106ee403866f1 Mon Sep 17 00:00:00 2001 From: Patrick Scheid Date: Mon, 23 Feb 2026 00:07:45 +0100 Subject: [PATCH] Make graceful shutdown timeout configurable via SHUTDOWN_TIMEOUT The shutdown timeout was hardcoded to 10s. Long-running webhook processing or slow database drains could get cut off. Add a SHUTDOWN_TIMEOUT env var (default: 10s) following the existing SESSION_MAX_AGE pattern. --- .env.example | 1 + cmd/server/main.go | 7 +++---- internal/platform/config/config.go | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 644166d..a44db87 100644 --- a/.env.example +++ b/.env.example @@ -43,3 +43,4 @@ LOG_FORMAT=text # WebSocket / Session MAX_WEBSOCKET_CONNECTIONS=10000 # Used to check file descriptor limit at startup SESSION_MAX_AGE=168h # HTTP session cookie expiry (7 days) +SHUTDOWN_TIMEOUT=10s # Graceful shutdown deadline (default: 10s) diff --git a/cmd/server/main.go b/cmd/server/main.go index 8b4048a..f9d1c2b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -35,7 +35,6 @@ const ( dbConnectTimeout = 30 * time.Second migrationTimeout = 60 * time.Second eventsubSetupTimeout = 30 * time.Second - shutdownTimeout = 10 * time.Second configCacheTTL = 10 * time.Second configEvictInterval = 1 * time.Minute ) @@ -168,7 +167,7 @@ func initWebhookHandler(cfg *config.Config, ovl *app.Overlay, presence twitch.Vi return twitch.NewWebhookHandler(cfg.WebhookSecret, ovl, presence, publisher, onVoteApplied, voteMetrics) } -func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node) <-chan struct{} { +func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node, timeout time.Duration) <-chan struct{} { done := make(chan struct{}) sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -177,7 +176,7 @@ func runGracefulShutdown(srv *httpserver.Server, node *centrifuge.Node) <-chan s <-sigChan slog.Info("Shutdown signal received, cleaning up...") - ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if err := srv.Shutdown(ctx); err != nil { @@ -273,7 +272,7 @@ func main() { os.Exit(1) } - done := runGracefulShutdown(srv, ws.node) + done := runGracefulShutdown(srv, ws.node, cfg.ShutdownTimeout) if err := srv.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("Server error", "error", err) diff --git a/internal/platform/config/config.go b/internal/platform/config/config.go index 06fbcbb..f5d86d0 100644 --- a/internal/platform/config/config.go +++ b/internal/platform/config/config.go @@ -31,7 +31,8 @@ type Config struct { MaxWebSocketConnections int `env:"MAX_WEBSOCKET_CONNECTIONS" default:"10000"` - SessionMaxAge time.Duration `env:"SESSION_MAX_AGE" default:"168h"` // 7 days + SessionMaxAge time.Duration `env:"SESSION_MAX_AGE" default:"168h"` // 7 days + ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" default:"10s"` // Graceful shutdown deadline } func Load() (*Config, error) {