package allocator import ( "net/http" "sync" "time" ) // Health records only complete successful cycles. Readiness ages out when // provider or durable-store work repeatedly fails or stalls, while liveness // remains independent so Kubernetes does not restart a healthy process for a // dependency outage. type Health struct { mu sync.RWMutex lastSuccessfulCycle time.Time } func (h *Health) ObserveSuccessfulCycle(at time.Time) { if h == nil || at.IsZero() { return } h.mu.Lock() h.lastSuccessfulCycle = at h.mu.Unlock() } func (h *Health) Ready(now time.Time, maxStale time.Duration) bool { if h == nil || now.IsZero() || maxStale <= 0 { return false } h.mu.RLock() lastSuccess := h.lastSuccessfulCycle h.mu.RUnlock() return !lastSuccess.IsZero() && !now.Before(lastSuccess) && now.Sub(lastSuccess) <= maxStale } // RoleHandler exposes metrics plus distinct process-liveness and dependency- // progress readiness endpoints on the allocator's private listener. func RoleHandler(metrics *Metrics, health *Health, maxStale time.Duration, now func() time.Time) http.Handler { mux := http.NewServeMux() mux.Handle("/metrics", MetricsHandler(metrics)) mux.HandleFunc("/healthz", methodGet(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok\n")) })) mux.HandleFunc("/readyz", methodGet(func(w http.ResponseWriter, _ *http.Request) { if now == nil || !health.Ready(now(), maxStale) { http.Error(w, "allocator has no recent successful cycle", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ready\n")) })) return mux } func methodGet(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } next(w, r) } }