- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go - backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline44161d2areproduced DATA RACE; now green under -race) - goose 00071 duplicate (dialogue merge1da17a28brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration - task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
105 lines
2.9 KiB
Go
105 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
resp, code := s.buildHealthResponse(r.Context(), false)
|
|
writeJSONResponse(w, code, resp)
|
|
}
|
|
|
|
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
|
resp, code := s.buildHealthResponse(r.Context(), true)
|
|
writeJSONResponse(w, code, resp)
|
|
}
|
|
|
|
type healthSnapshot struct {
|
|
at time.Time
|
|
strict bool
|
|
resp HealthResponse
|
|
code int
|
|
ok bool
|
|
}
|
|
|
|
func (s *Server) buildHealthResponse(ctx context.Context, strict bool) (HealthResponse, int) {
|
|
const healthCacheTTL = 2 * time.Second
|
|
s.healthMu.Lock()
|
|
if s.healthCache.ok && s.healthCache.strict == strict && time.Since(s.healthCache.at) < healthCacheTTL {
|
|
resp, code := s.healthCache.resp, s.healthCache.code
|
|
s.healthMu.Unlock()
|
|
return resp, code
|
|
}
|
|
s.healthMu.Unlock()
|
|
|
|
dbCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
|
|
dbReady := s.svc() != nil && s.svc().DatabaseReady()
|
|
schemaVersion := int64(0)
|
|
if s.svc() != nil {
|
|
if dbReady {
|
|
schemaVersion = s.svc().LiveSchemaVersion(dbCtx)
|
|
} else {
|
|
schemaVersion = s.svc().SchemaVersion()
|
|
}
|
|
}
|
|
schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory)
|
|
taskStoreReady := s.rt != nil && s.rt.TaskStoreReady(dbCtx)
|
|
|
|
status := "ok"
|
|
code := http.StatusOK
|
|
if s.cfg != nil && s.cfg.ContextDbHost != "" && !s.cfg.AllowInMemory {
|
|
if !dbReady {
|
|
status = "degraded"
|
|
code = http.StatusServiceUnavailable
|
|
} else if strict && (!schemaReady || !taskStoreReady) {
|
|
status = "not_ready"
|
|
code = http.StatusServiceUnavailable
|
|
}
|
|
}
|
|
|
|
vaultIdentityOK, vaultConnected, vaultLagOK := s.vaultReady()
|
|
vaultConfigured := s.cfg != nil && s.cfg.VaultStateRoot != "" && s.cfg.VaultMasterKey != ""
|
|
if s.cfg != nil && s.cfg.ContextDbHost != "" && !s.cfg.AllowInMemory && dbReady && strict {
|
|
if vaultConfigured && (!vaultIdentityOK || !vaultConnected || !vaultLagOK) {
|
|
status = "not_ready"
|
|
code = http.StatusServiceUnavailable
|
|
}
|
|
}
|
|
|
|
kbReady := s.kb() != nil && s.kb().Online()
|
|
|
|
eventsReady := true
|
|
if s.eventsAlive != nil {
|
|
eventsReady = s.eventsAlive()
|
|
if strict && !eventsReady && s.cfg != nil && s.cfg.ContextDbHost != "" && !s.cfg.AllowInMemory && dbReady {
|
|
status = "not_ready"
|
|
code = http.StatusServiceUnavailable
|
|
}
|
|
}
|
|
|
|
resp := HealthResponse{
|
|
Status: status,
|
|
Env: s.cfg.Env,
|
|
AgentBackend: s.cfg.AgentBackend,
|
|
Version: "v1.0",
|
|
SharedKBPort: s.cfg.KbDbPort,
|
|
KBReady: kbReady,
|
|
DatabaseReady: dbReady,
|
|
SchemaVersion: schemaVersion,
|
|
SchemaReady: schemaReady,
|
|
TaskStoreReady: taskStoreReady,
|
|
VaultIdentityOK: vaultIdentityOK,
|
|
VaultConnected: vaultConnected,
|
|
VaultLagOK: vaultLagOK,
|
|
EventsReady: eventsReady,
|
|
}
|
|
s.healthMu.Lock()
|
|
s.healthCache = healthSnapshot{at: time.Now(), strict: strict, resp: resp, code: code, ok: true}
|
|
s.healthMu.Unlock()
|
|
return resp, code
|
|
}
|