diff --git a/README.md b/README.md index 8a0d5db..602b9c0 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,15 @@ This includes: - Protocol errors - TLS-specific connection issues +### Graceful Shutdown + +When backer receives a termination signal (SIGTERM or SIGINT), it will: +1. Log a warning message: "Shutdown signal received, shutting down immediately..." +2. Stop accepting new connections and abort any in-progress downloads immediately +3. Clean up resources and exit + +This ensures no partial/corrupted backups are left. Clients will need to restart their backup. For a forced immediate stop, use `SIGKILL`. + ## Special Thanks This project was developed with the assistance of **opencode** (AI co-programmer powered by MIMO). The AI helped with: diff --git a/cmd/backer/main.go b/cmd/backer/main.go index 9d1e2d2..98236b5 100644 --- a/cmd/backer/main.go +++ b/cmd/backer/main.go @@ -41,6 +41,10 @@ func main() { log.Fatalf("Failed to create server: %v", err) } + // Setup signal handling for immediate shutdown. + log.Infof("Installing signal handler") + setupSignalHandler(server) + log.Infof("Starting backer version %s at %s", version, server.Addr) if backer.C.NoHTTPS { @@ -60,6 +64,8 @@ func main() { log.Fatalf("Failed to start server: %v", err) } } + + log.Infof("Server stopped") } // defaultConfigPath returns the default config file path based on the OS. diff --git a/cmd/backer/sigHandler.go b/cmd/backer/sigHandler.go new file mode 100644 index 0000000..3f7eb34 --- /dev/null +++ b/cmd/backer/sigHandler.go @@ -0,0 +1,29 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "backer/internal/backer" + "backer/internal/log" +) + +// setupSignalHandler sets up signal handling for immediate shutdown. +// SIGTERM is commonly sent by systemd, k8s, and other container orchestrators. +// SIGINT is sent on Ctrl+C. +func setupSignalHandler(server *backer.ServerWrapper) { + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + + go func() { + <-signalCh + log.Warnf("Shutdown signal received, shutting down immediately...") + // Close the embedded http.Server directly. + if closeErr := server.Close(); closeErr != nil { + log.Errorf("Error during server shutdown: %v", closeErr) + } + }() +} + +/* vim: setlocal ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/backer/newServer.go b/internal/backer/newServer.go index 37e1d5e..c8669e1 100644 --- a/internal/backer/newServer.go +++ b/internal/backer/newServer.go @@ -54,8 +54,8 @@ func withServerHeader(next http.Handler) http.Handler { } // NewServer starts new server instance with given config file. -// It returns a serverWrapper that intercepts and classifies server errors. -func NewServer(configPath string) (*serverWrapper, error) { //nolint: revive +// It returns a ServerWrapper that intercepts and classifies server errors. +func NewServer(configPath string) (*ServerWrapper, error) { _ = log.Init("info", "") // Initialize logger with defaults for early logging. if err := LoadConfig(configPath); err != nil { @@ -175,7 +175,7 @@ func NewServer(configPath string) (*serverWrapper, error) { //nolint: revive // HTTP mode if nohttps is enabled in config. // HTTPS mode if nohttps is disabled in config. if C.NoHTTPS { - return &serverWrapper{ + return &ServerWrapper{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", C.Address, C.Port), Handler: withServerHeader(mux), @@ -186,7 +186,7 @@ func NewServer(configPath string) (*serverWrapper, error) { //nolint: revive }, nil } - return &serverWrapper{ + return &ServerWrapper{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", C.Address, C.Port), Handler: withServerHeader(mux), @@ -284,15 +284,15 @@ func getCompressionAlgorithm(requestedExt string) string { } type ( - // serverWrapper wraps [http.Server] to intercept and classify errors. - serverWrapper struct { + // ServerWrapper wraps [http.Server] to intercept and classify errors. + ServerWrapper struct { *http.Server } ) // Serve starts the server and classifies any errors returned. // TLS-related errors are logged at debug level, other errors at warn level. -func (sw *serverWrapper) Serve() error { +func (sw *ServerWrapper) Serve() error { err := sw.ListenAndServe() if err != nil { @@ -307,7 +307,7 @@ func (sw *serverWrapper) Serve() error { } // ServeTLS starts the TLS server and classifies any errors returned. -func (sw *serverWrapper) ServeTLS(certFile, keyFile string) error { +func (sw *ServerWrapper) ServeTLS(certFile, keyFile string) error { err := sw.ListenAndServeTLS(certFile, keyFile) if err != nil {