lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 260d429596562d90ef460fbc8c86750e6e2348da
parent e15092ca525b77aeabc9467034d73f2382bdf126
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 04:51:44 +0000

service-host: add cached health handlers

- project liveness from cached supervisor phase
- project readiness from cached readiness state
- freeze bounded status, body, and content type
- verify every phase without probes or callbacks

Diffstat:
Mcrates/service_host/src/lib.rs | 4+++-
Acrates/service_host/src/operations/health.rs | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/service_host/src/operations/mod.rs | 5+++++
Mcrates/service_host/tests/package_boundary.rs | 9++++++++-
4 files changed, 239 insertions(+), 2 deletions(-)

diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs @@ -50,9 +50,11 @@ pub use lifecycle::{ TaskRegistrationError, TaskSupervisor, UnfinishedWork, }; pub use operations::{ - OperationsBindPolicy, OperationsConfigError, OperationsConfigField, OperationsListenAddress, + LIVEZ_PATH, OPERATIONS_HEALTH_CONTENT_TYPE, OperationsBindPolicy, OperationsConfigError, + OperationsConfigField, OperationsHealthResponse, OperationsListenAddress, OperationsListenAddressError, OperationsListenerConfig, OperationsTransportLimitField, OperationsTransportLimitValues, OperationsTransportLimits, OperationsTransportLimitsError, + READYZ_PATH, livez, readyz, }; pub use status::{ CONFIGURATION_SCHEMA_VERSION, CachedServiceState, CachedServiceStatePublisher, diff --git a/crates/service_host/src/operations/health.rs b/crates/service_host/src/operations/health.rs @@ -0,0 +1,223 @@ +//! Constant-time liveness and readiness projections from cached service state. + +use http::StatusCode; + +use crate::{CachedServiceStateReader, ServicePhase}; + +pub const LIVEZ_PATH: &str = "/livez"; +pub const READYZ_PATH: &str = "/readyz"; +pub const OPERATIONS_HEALTH_CONTENT_TYPE: &str = "text/plain; charset=utf-8"; + +const LIVE_BODY: &[u8] = b"live\n"; +const FAILED_BODY: &[u8] = b"failed\n"; +const READY_BODY: &[u8] = b"ready\n"; +const UNREADY_BODY: &[u8] = b"unready\n"; + +/// One fixed, bounded response for a passive health route. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct OperationsHealthResponse { + status: StatusCode, + body: &'static [u8], +} + +impl OperationsHealthResponse { + #[must_use] + pub const fn status(&self) -> StatusCode { + self.status + } + + #[must_use] + pub const fn content_type(&self) -> &'static str { + OPERATIONS_HEALTH_CONTENT_TYPE + } + + #[must_use] + pub const fn body(&self) -> &'static [u8] { + self.body + } +} + +/// Projects `/livez` from the latest supervisor-owned lifecycle snapshot. +/// +/// The failed terminal phase is not live. Every other phase describes a +/// process whose supervisor is still able to advance or complete shutdown. +#[must_use] +pub fn livez<M>(cache: &CachedServiceStateReader<M>) -> OperationsHealthResponse { + if cache.snapshot().operational().phase() == ServicePhase::Failed { + OperationsHealthResponse { + status: StatusCode::SERVICE_UNAVAILABLE, + body: FAILED_BODY, + } + } else { + OperationsHealthResponse { + status: StatusCode::OK, + body: LIVE_BODY, + } + } +} + +/// Projects `/readyz` from the latest service-owned cached readiness bit. +#[must_use] +pub fn readyz<M>(cache: &CachedServiceStateReader<M>) -> OperationsHealthResponse { + if cache.snapshot().operational().readiness().is_ready() { + OperationsHealthResponse { + status: StatusCode::OK, + body: READY_BODY, + } + } else { + OperationsHealthResponse { + status: StatusCode::SERVICE_UNAVAILABLE, + body: UNREADY_BODY, + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use super::*; + use crate::{ + CachedServiceState, Readiness, ReasonCodes, ServiceOperationalState, cached_service_state, + }; + + struct PassiveMetrics { + probe_calls: Arc<AtomicUsize>, + } + + impl PassiveMetrics { + fn probe_calls(&self) -> usize { + self.probe_calls.load(Ordering::SeqCst) + } + } + + fn cache( + phase: ServicePhase, + readiness: Readiness, + ) -> CachedServiceStateReader<PassiveMetrics> { + let operational = + ServiceOperationalState::new(phase, readiness, ReasonCodes::empty()).unwrap(); + let (_, reader) = cached_service_state(CachedServiceState::new( + operational, + PassiveMetrics { + probe_calls: Arc::new(AtomicUsize::new(0)), + }, + )); + reader + } + + #[test] + fn every_phase_has_an_exact_bounded_health_projection() { + let cases = [ + ( + ServicePhase::Starting, + Readiness::NOT_READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::SERVICE_UNAVAILABLE, + UNREADY_BODY, + ), + ( + ServicePhase::Ready, + Readiness::READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::OK, + READY_BODY, + ), + ( + ServicePhase::Degraded, + Readiness::READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::OK, + READY_BODY, + ), + ( + ServicePhase::Degraded, + Readiness::NOT_READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::SERVICE_UNAVAILABLE, + UNREADY_BODY, + ), + ( + ServicePhase::Unready, + Readiness::NOT_READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::SERVICE_UNAVAILABLE, + UNREADY_BODY, + ), + ( + ServicePhase::Stopping, + Readiness::NOT_READY, + StatusCode::OK, + LIVE_BODY, + StatusCode::SERVICE_UNAVAILABLE, + UNREADY_BODY, + ), + ( + ServicePhase::Failed, + Readiness::NOT_READY, + StatusCode::SERVICE_UNAVAILABLE, + FAILED_BODY, + StatusCode::SERVICE_UNAVAILABLE, + UNREADY_BODY, + ), + ]; + + for (phase, readiness, live_status, live_body, ready_status, ready_body) in cases { + let reader = cache(phase, readiness); + let live = livez(&reader); + let ready = readyz(&reader); + + assert_eq!((live.status(), live.body()), (live_status, live_body)); + assert_eq!((ready.status(), ready.body()), (ready_status, ready_body)); + assert_eq!(live.content_type(), OPERATIONS_HEALTH_CONTENT_TYPE); + assert_eq!(ready.content_type(), OPERATIONS_HEALTH_CONTENT_TYPE); + assert!(live.body().len() <= UNREADY_BODY.len()); + assert!(ready.body().len() <= UNREADY_BODY.len()); + assert_eq!(reader.snapshot().metrics().probe_calls(), 0); + } + } + + #[test] + fn handlers_read_the_latest_snapshot_without_waiting_or_probing() { + let probe_calls = Arc::new(AtomicUsize::new(0)); + let starting = ServiceOperationalState::new( + ServicePhase::Starting, + Readiness::NOT_READY, + ReasonCodes::empty(), + ) + .unwrap(); + let (mut publisher, reader) = cached_service_state(CachedServiceState::new( + starting, + PassiveMetrics { + probe_calls: Arc::clone(&probe_calls), + }, + )); + + assert_eq!(readyz(&reader).status(), StatusCode::SERVICE_UNAVAILABLE); + let ready = ServiceOperationalState::new( + ServicePhase::Ready, + Readiness::READY, + ReasonCodes::empty(), + ) + .unwrap(); + publisher + .publish(CachedServiceState::new( + ready, + PassiveMetrics { + probe_calls: Arc::clone(&probe_calls), + }, + )) + .unwrap(); + + assert_eq!(readyz(&reader).status(), StatusCode::OK); + assert_eq!(probe_calls.load(Ordering::SeqCst), 0); + } +} diff --git a/crates/service_host/src/operations/mod.rs b/crates/service_host/src/operations/mod.rs @@ -1,9 +1,14 @@ //! Cached, bounded network-operations mechanics. mod config; +mod health; pub use config::{ OperationsBindPolicy, OperationsConfigError, OperationsConfigField, OperationsListenAddress, OperationsListenAddressError, OperationsListenerConfig, OperationsTransportLimitField, OperationsTransportLimitValues, OperationsTransportLimits, OperationsTransportLimitsError, }; +pub use health::{ + LIVEZ_PATH, OPERATIONS_HEALTH_CONTENT_TYPE, OperationsHealthResponse, READYZ_PATH, livez, + readyz, +}; diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs @@ -28,6 +28,7 @@ const LIFECYCLE_SOURCE: &str = concat!( const OPERATIONS_SOURCE: &str = concat!( include_str!("../src/operations/mod.rs"), include_str!("../src/operations/config.rs"), + include_str!("../src/operations/health.rs"), ); #[test] @@ -80,7 +81,13 @@ fn service_host_is_unpublished_lint_governed_and_dependency_bounded() { for forbidden in ["tokio::signal", "ctrl_c", "signal_hook"] { assert!(!LIFECYCLE_SOURCE.contains(forbidden)); } - for forbidden in ["TcpListener", "TcpStream", "/status", "process::exit"] { + for forbidden in [ + "TcpListener", + "TcpStream", + "/status", + "process::exit", + "tokio::spawn", + ] { assert!(!OPERATIONS_SOURCE.contains(forbidden)); } }