Files
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

90 lines
2.4 KiB
Go

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", ""
}