Files
weichen a9c9a9182e refactor(server): remove 24 mirrored Runtime fields via nil-safe accessors; fix Shutdown-vs-background-loop DATA RACE and goose 00071 duplicate version
- 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 (baseline 44161d2a reproduced DATA RACE; now green under -race)
- goose 00071 duplicate (dialogue merge 1da17a28 brought 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)
2026-09-25 13:17:22 +00:00

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
}