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:
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));
}
}