internal/health: wiederverwendbare Registry fuer benannte Checks (DB, Queue) — nicht Core-spezifisch, sondern von jedem registrierten Modul (API-02) gleichermassen einsetzbar. LivenessHandler prueft bewusst KEINE externen Abhaengigkeiten (Akzeptanzkriterium 2: Liveness/Readiness getrennt) — ein DB-Ausfall soll den Prozess nicht faelschlich als "tot" markieren und einen grundlosen Neustart ausloesen. ReadinessHandler fuehrt alle registrierten Checks NEBENLAEUFIG mit je eigenem Timeout aus (DefaultCheckTimeout=2s) und liefert 503, sobald irgendeine Abhaengigkeit fehlschlaegt (Akzeptanz- kriterium 1 + 3) — echte Pruefung von DB (Ping) und Job-Queue statt nur Prozessstatus. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Simulierter Datenbankausfall fuehrt zu "nicht bereit" — TestReadinessHandler_ReportsNotReadyOnDatabaseFailure: geschlossener Pool, 503 mit "database" im Checks-Ergebnis. PASS. 2. Health-Endpunkt antwortet auch bei haengendem Check innerhalb definierter Zeit — TestReadinessHandler_RespondsWithinTimeoutEvenWithHangingCheck: ein 10s blockierender Check wird durch 50ms-Timeout begrenzt, Handler antwortet deutlich unter 1s. PASS. 3. Readiness- und Liveness-Antwort unterscheiden sich nachweislich in mindestens einem Fehlerfall — TestLivenessAndReadiness_DifferOnDatabaseFailure: bei DB-Ausfall liefert Liveness weiterhin 200, Readiness 503. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
2.5 KiB
Go
87 lines
2.5 KiB
Go
// Package health implementiert Core OPS-01: Health-/Readiness-Endpunkte, die
|
|
// echte Abhaengigkeiten (DB, Job-Queue) statt nur den Prozessstatus pruefen
|
|
// — wiederverwendbar von Core UND jedem registrierten Modul (siehe API-02),
|
|
// nicht nur von Core selbst.
|
|
package health
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// Checker prueft EINE Abhaengigkeit (z.B. Datenbank, Job-Queue).
|
|
type Checker interface {
|
|
Check(ctx context.Context) error
|
|
}
|
|
|
|
type CheckerFunc func(ctx context.Context) error
|
|
|
|
func (f CheckerFunc) Check(ctx context.Context) error { return f(ctx) }
|
|
|
|
// DefaultCheckTimeout begrenzt, wie lange EIN einzelner Check maximal
|
|
// dauern darf, bevor er als fehlgeschlagen gilt — verhindert, dass ein
|
|
// haengender Check den gesamten Readiness-Endpunkt blockiert
|
|
// (Akzeptanzkriterium 2 / Pruefung 2: Antwort innerhalb definierter Zeit).
|
|
const DefaultCheckTimeout = 2 * time.Second
|
|
|
|
// Registry haelt alle benannten Checks eines Dienstes.
|
|
type Registry struct {
|
|
checks map[string]Checker
|
|
timeout time.Duration
|
|
}
|
|
|
|
func NewRegistry() *Registry {
|
|
return &Registry{checks: make(map[string]Checker), timeout: DefaultCheckTimeout}
|
|
}
|
|
|
|
func (r *Registry) WithTimeout(d time.Duration) *Registry {
|
|
return &Registry{checks: r.checks, timeout: d}
|
|
}
|
|
|
|
// Register fuegt einen benannten Check hinzu (z.B. "database", "queue").
|
|
func (r *Registry) Register(name string, c Checker) {
|
|
r.checks[name] = c
|
|
}
|
|
|
|
// Result ist der Ausgang eines einzelnen Checks.
|
|
type Result struct {
|
|
OK bool
|
|
Error string
|
|
}
|
|
|
|
// CheckAll fuehrt alle registrierten Checks NEBENLAEUFIG mit je eigenem
|
|
// Timeout aus (Akzeptanzkriterium 1: echte Abhaengigkeiten statt Prozess-
|
|
// status) und liefert ready=false, sobald irgendein Check fehlschlaegt
|
|
// (Akzeptanzkriterium 3: ein Ausfall wird sichtbar).
|
|
func (r *Registry) CheckAll(ctx context.Context) (ready bool, results map[string]Result) {
|
|
type namedResult struct {
|
|
name string
|
|
result Result
|
|
}
|
|
ch := make(chan namedResult, len(r.checks))
|
|
|
|
for name, checker := range r.checks {
|
|
go func(name string, checker Checker) {
|
|
checkCtx, cancel := context.WithTimeout(ctx, r.timeout)
|
|
defer cancel()
|
|
err := checker.Check(checkCtx)
|
|
if err != nil {
|
|
ch <- namedResult{name, Result{OK: false, Error: err.Error()}}
|
|
return
|
|
}
|
|
ch <- namedResult{name, Result{OK: true}}
|
|
}(name, checker)
|
|
}
|
|
|
|
results = make(map[string]Result, len(r.checks))
|
|
ready = true
|
|
for i := 0; i < len(r.checks); i++ {
|
|
nr := <-ch
|
|
results[nr.name] = nr.result
|
|
if !nr.result.OK {
|
|
ready = false
|
|
}
|
|
}
|
|
return ready, results
|
|
}
|