Files
health/go-health/readiness.go
2026-01-27 18:13:33 -08:00

222 lines
5.9 KiB
Go

package health
import (
"encoding/json"
"net/http"
"slices"
"time"
)
// Status of the service readiness.
type ReadinessStatus string
// String returns the string representation of the readiness status.
func (s ReadinessStatus) String() string {
return string(s)
}
// HTTPStatus returns the corresponding HTTP status code for the readiness status.
func (s ReadinessStatus) HTTPStatus() int {
switch s {
case ReadinessStatusReady:
return http.StatusOK
case ReadinessStatusDegraded:
return http.StatusOK // 206 could also be suitable, but let's avoid false alarms
case ReadinessStatusFatal:
return http.StatusServiceUnavailable
case ReadinessStatusNotReady:
return http.StatusOK // treat as ok to avoid false alarms
default:
return http.StatusOK
}
}
const (
// Service is ready to handle requests.
ReadinessStatusReady ReadinessStatus = "ready"
// Service is not ready to handle requests.
ReadinessStatusNotReady ReadinessStatus = "not_ready"
// Service experienced a fatal error and cannot handle requests.
ReadinessStatusFatal ReadinessStatus = "fatal"
// Service is degraded but can still handle requests.
ReadinessStatusDegraded ReadinessStatus = "degraded"
)
// Returns the worst readiness status from a list of statuses.
func worstStatus(statuses []ReadinessStatus) ReadinessStatus {
if slices.Contains(statuses, ReadinessStatusFatal) {
return ReadinessStatusFatal
}
if slices.Contains(statuses, ReadinessStatusNotReady) {
return ReadinessStatusNotReady
}
if slices.Contains(statuses, ReadinessStatusDegraded) {
return ReadinessStatusDegraded
}
return ReadinessStatusReady
}
// Readiness check function return type.
type ReadinessReturn struct {
// Status of the readiness check.
Status ReadinessStatus
// Optional message providing additional information about the readiness status.
Message *string
}
// Function that performs a readiness check.
type ReadinessCheckFn func() ReadinessReturn
// Individual readiness check.
type ReadinessCheck struct {
// Name of the readiness check.
Name string
// Function that performs the readiness check.
Fn ReadinessCheckFn
// Timeout for the readiness check (default: 5 seconds)
Timeout time.Duration
}
// System-generated result from an individual readiness check.
type ReadinessCheckResult struct {
// Name of the readiness check.
Name string
// Status of the readiness check.
Status ReadinessStatus
// Optional message providing additional information about the readiness status.
Message *string
// Duration of the readiness check in milliseconds.
Duration int64
}
// System readiness status.
type Readiness struct {
// Status of the service readiness. Aggregated as the worst status from
// all individual readiness checks. If no checks have been performed
// yet, the status is ReadinessStatusNotReady.
Status ReadinessStatus
// Start time of the readiness check in milliseconds since Unix epoch.
Start int64
// Duration of the readiness check in milliseconds.
Duration int64
// Individual readiness check results.
Details map[string]ReadinessCheckResult
}
// Performs a full system readiness check given a list of individual checks.
func CheckReadiness(checks []ReadinessCheck) Readiness {
start := time.Now()
details := make(map[string]ReadinessCheckResult)
var statuses []ReadinessStatus
for _, check := range checks {
checkt0 := time.Now()
result := checkWithTimeout(check)
duration := time.Since(checkt0).Milliseconds()
details[check.Name] = ReadinessCheckResult{
Name: check.Name,
Status: result.Status,
Message: result.Message,
Duration: duration,
}
statuses = append(statuses, result.Status)
}
return Readiness{
Status: worstStatus(statuses),
Start: start.UnixMilli(),
Duration: time.Since(start).Milliseconds(),
Details: details,
}
}
// Creates a handler function for readiness HTTP requests.
func ReadinessHandler(checks []ReadinessCheck) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
respondWithResult(w, CheckReadiness(checks))
}
}
// Runs a readiness check with a timeout.
func checkWithTimeout(check ReadinessCheck) ReadinessReturn {
timeout := check.Timeout
if timeout == 0 {
timeout = 5 * time.Second
}
resultCh := make(chan ReadinessReturn, 1)
go func() {
resultCh <- check.Fn()
}()
select {
case result := <-resultCh:
return result
case <-time.After(timeout):
msg := "readiness check timed out"
return ReadinessReturn{
Status: ReadinessStatusFatal,
Message: &msg,
}
}
}
func respondWithResult(w http.ResponseWriter, result Readiness) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(result.Status.HTTPStatus())
json.NewEncoder(w).Encode(result)
}
// ScheduledReadiness performs periodic readiness checks and stores the latest result.
type ScheduledReadiness struct {
checks []ReadinessCheck
interval time.Duration
currentState Readiness
ticker *time.Ticker
quit chan struct{}
}
// NewScheduledReadiness creates a new ScheduledReadiness instance.
func NewScheduledReadiness(checks []ReadinessCheck, interval time.Duration) *ScheduledReadiness {
return &ScheduledReadiness{
checks: checks,
interval: interval,
quit: make(chan struct{}),
}
}
// Start begins the periodic readiness checks.
func (sr *ScheduledReadiness) Start() {
sr.ticker = time.NewTicker(sr.interval)
go func() {
for {
select {
case <-sr.ticker.C:
sr.currentState = CheckReadiness(sr.checks)
case <-sr.quit:
sr.ticker.Stop()
return
}
}
}()
}
// Stop halts the periodic readiness checks.
func (sr *ScheduledReadiness) Stop() {
close(sr.quit)
}
// GetCurrentState returns the latest readiness state.
func (sr *ScheduledReadiness) GetCurrentState() Readiness {
return sr.currentState
}
// Creates a handler function for scheduled readiness HTTP requests.
func (sr *ScheduledReadiness) ReadinessHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
respondWithResult(w, sr.GetCurrentState())
}
}