package httpapi import ( "context" "net/http" "time" "github.com/descrybe/descrybe-v2/apps/api/internal/jobs" ) const healthServiceName = "api" // dbPinger is satisfied by *pgxpool.Pool; kept narrow for unit tests. type dbPinger interface { Ping(ctx context.Context) error } func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { JSON(w, http.StatusOK, map[string]any{ "status": "ok", "service": healthServiceName, "maintenance": s.Config.MaintenanceMode, "read_only": s.Config.ReadOnlyMode, "hypercare": s.Config.HypercareMode, }) } func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) { var pinger dbPinger var prober jobs.HeartbeatQuerier if s.Pool != nil { pinger = s.Pool prober = s.Pool } s.writeReadyz(w, r, pinger, prober) } func (s *Server) writeReadyz(w http.ResponseWriter, r *http.Request, pinger dbPinger, prober jobs.HeartbeatQuerier) { ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() ok, check, errMsg := databaseReady(ctx, pinger) checks := map[string]string{"database": check} body := map[string]any{ "status": "ready", "service": healthServiceName, "maintenance": s.Config.MaintenanceMode, "read_only": s.Config.ReadOnlyMode, "hypercare": s.Config.HypercareMode, "checks": checks, } if !ok { body["status"] = "not_ready" body["error"] = errMsg JSON(w, http.StatusServiceUnavailable, body) return } probe := jobs.ProbeWorkerReadiness(ctx, prober, jobs.DefaultHeartbeatStaleAfter) checks["worker"] = probe.WorkerCheck checks["queue"] = probe.QueueCheck body["queue_pending"] = probe.PendingJobs if probe.LastSeenAgeS >= 0 { body["worker_last_seen_age_s"] = probe.LastSeenAgeS } if !probe.OK { body["status"] = "not_ready" body["error"] = probe.ErrMsg if probe.Reason != "" { body["reason"] = probe.Reason } JSON(w, http.StatusServiceUnavailable, body) return } JSON(w, http.StatusOK, body) } // databaseReady pings Postgres for readiness. check is "ok", "unavailable", or "fail". // errMsg is empty when ok; never includes driver detail (safe for public probes). func databaseReady(ctx context.Context, p dbPinger) (ok bool, check string, errMsg string) { if p == nil { return false, "unavailable", "database pool unavailable" } if err := p.Ping(ctx); err != nil { return false, "fail", "database ping failed" } return true, "ok", "" }