Shut the API down gracefully on deploys

Go · Go · intermediate · modification

Deploys currently cut off in-flight requests: on SIGTERM the process dies mid-response and the load balancer sees 502s. `run` now owns the lifecycle. It pings the database before listening, serves in a goroutine, waits for a signal or a serve error, and then calls `Shutdown` with a 25-second budget. SIGTERM arrives after the 5-second `preStop` sleep, which counts against the 35-second grace period, so the budget leaves 5 seconds to log and exit before SIGKILL. After the first signal, signal handling is reset, so a second Ctrl-C still kills a stuck process. The server also gets real timeouts instead of none. Tested locally: SIGTERM during a slow request lets it finish and exits 0; a taken port and an unreachable database both exit 1.

Go 1.22 JSON API in a Kubernetes Deployment. The container runs the binary under `tini` as init, and the pod spec has a `preStop` hook (`sleep 5`), which delays SIGTERM until the pod's removal from the Service endpoints has propagated; the grace period is 35 seconds. No streaming, WebSocket or long-poll endpoints; request bodies are small JSON (files go straight to object storage through presigned URLs). Every handler finishes within 20 seconds (the database has a 15-second statement timeout) and uses the request context. `config.Load` (existing) reads and validates the environment; `api.NewRouter(db)` (existing) builds the handler tree. The pgx stdlib driver registers itself as `pgx` through the blank import, as before.

Requirements

Files touched

--- cmd/api/main.go
 
 import (
+	"context"
 	"database/sql"
-	"log"
+	"errors"
+	"fmt"
+	"log/slog"
 	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
 
 	_ "github.com/jackc/pgx/v5/stdlib"
 )
 
+const (
+	startupPingTimeout = 5 * time.Second
+	shutdownTimeout    = 25 * time.Second
+)
+
 func main() {
+	if err := run(); err != nil {
+		slog.Error("api exited", "err", err)
+		os.Exit(1)
+	}
+	slog.Info("api stopped")
+}

Review this PR

Go practice