Ping database in health check

This commit is contained in:
Jonathon Leight committed 2026-07-05 19:27:34 -04:00
1 parent 60e13cc788
commit 54c4ccbe05
2 files changed
+40 -3

No files matched your search

+23
View File
@@ -28,6 +28,29 @@ func TestHealthzEndpoint(t *testing.T) {
}
}
// TestHealthzReportsDBDown: /healthz is a readiness probe — when the database is
// unreachable it must fail (503) rather than report healthy. (A cookieless
// request touches no session DB rows, so the ping is what fails.)
func TestHealthzReportsDBDown(t *testing.T) {
t.Parallel()
st, _, ts, h := splitServer(t)
// Healthy: 200.
resp := do(t, ts, h.app, "/healthz")
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("healthy /healthz = %d, want 200", resp.StatusCode)
}
// Take the database down; the readiness ping must now fail closed.
st.Close()
down := do(t, ts, h.app, "/healthz")
down.Body.Close()
if down.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("db-down /healthz = %d, want 503", down.StatusCode)
}
}
// #2 /static/* — serves an embedded asset.
func TestStaticAssetEndpoint(t *testing.T) {
t.Parallel()
+17 -3
View File
@@ -16,6 +16,7 @@ import (
"net/url"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -304,13 +305,26 @@ func limitBody(next http.Handler) http.Handler {
// SharedRoutes registers endpoints every surface needs (health, static assets).
func (e *Env) SharedRoutes(r chi.Router) {
r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
r.Get("/healthz", e.healthz)
staticSub, _ := fs.Sub(staticFS, "static")
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub))))
}
// healthz is a readiness probe: it pings the database (briefly) so a broken pool
// or unreachable DB fails the check — letting a load balancer route away or an
// orchestrator restart the instance — rather than reporting healthy while every
// request 500s.
func (e *Env) healthz(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := e.Store.Pool().Ping(ctx); err != nil {
LogError(r, "healthz: db ping failed", err)
http.Error(w, "database unavailable", http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte("ok"))
}
// HostWithoutPort strips a trailing :port from a request Host, if present.
func HostWithoutPort(host string) string {
if h, _, err := net.SplitHostPort(host); err == nil {