lib

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

health.rs (6799B)


      1 //! Constant-time liveness and readiness projections from cached service state.
      2 
      3 use http::StatusCode;
      4 
      5 use crate::{CachedServiceStateReader, ServicePhase};
      6 
      7 pub const LIVEZ_PATH: &str = "/livez";
      8 pub const READYZ_PATH: &str = "/readyz";
      9 pub const OPERATIONS_HEALTH_CONTENT_TYPE: &str = "text/plain; charset=utf-8";
     10 
     11 const LIVE_BODY: &[u8] = b"live\n";
     12 const FAILED_BODY: &[u8] = b"failed\n";
     13 const READY_BODY: &[u8] = b"ready\n";
     14 const UNREADY_BODY: &[u8] = b"unready\n";
     15 
     16 /// One fixed, bounded response for a passive health route.
     17 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     18 pub struct OperationsHealthResponse {
     19     status: StatusCode,
     20     body: &'static [u8],
     21 }
     22 
     23 impl OperationsHealthResponse {
     24     #[must_use]
     25     pub const fn status(&self) -> StatusCode {
     26         self.status
     27     }
     28 
     29     #[must_use]
     30     pub const fn content_type(&self) -> &'static str {
     31         OPERATIONS_HEALTH_CONTENT_TYPE
     32     }
     33 
     34     #[must_use]
     35     pub const fn body(&self) -> &'static [u8] {
     36         self.body
     37     }
     38 }
     39 
     40 /// Projects `/livez` from the latest supervisor-owned lifecycle snapshot.
     41 ///
     42 /// The failed terminal phase is not live. Every other phase describes a
     43 /// process whose supervisor is still able to advance or complete shutdown.
     44 #[must_use]
     45 pub fn livez<M>(cache: &CachedServiceStateReader<M>) -> OperationsHealthResponse {
     46     if cache.snapshot().operational().phase() == ServicePhase::Failed {
     47         OperationsHealthResponse {
     48             status: StatusCode::SERVICE_UNAVAILABLE,
     49             body: FAILED_BODY,
     50         }
     51     } else {
     52         OperationsHealthResponse {
     53             status: StatusCode::OK,
     54             body: LIVE_BODY,
     55         }
     56     }
     57 }
     58 
     59 /// Projects `/readyz` from the latest service-owned cached readiness bit.
     60 #[must_use]
     61 pub fn readyz<M>(cache: &CachedServiceStateReader<M>) -> OperationsHealthResponse {
     62     if cache.snapshot().operational().readiness().is_ready() {
     63         OperationsHealthResponse {
     64             status: StatusCode::OK,
     65             body: READY_BODY,
     66         }
     67     } else {
     68         OperationsHealthResponse {
     69             status: StatusCode::SERVICE_UNAVAILABLE,
     70             body: UNREADY_BODY,
     71         }
     72     }
     73 }
     74 
     75 #[cfg(test)]
     76 mod tests {
     77     use std::sync::{
     78         Arc,
     79         atomic::{AtomicUsize, Ordering},
     80     };
     81 
     82     use super::*;
     83     use crate::{
     84         CachedServiceState, Readiness, ReasonCodes, ServiceOperationalState, cached_service_state,
     85     };
     86 
     87     struct PassiveMetrics {
     88         probe_calls: Arc<AtomicUsize>,
     89     }
     90 
     91     impl PassiveMetrics {
     92         fn probe_calls(&self) -> usize {
     93             self.probe_calls.load(Ordering::SeqCst)
     94         }
     95     }
     96 
     97     fn cache(
     98         phase: ServicePhase,
     99         readiness: Readiness,
    100     ) -> CachedServiceStateReader<PassiveMetrics> {
    101         let operational =
    102             ServiceOperationalState::new(phase, readiness, ReasonCodes::empty()).unwrap();
    103         let (_, reader) = cached_service_state(CachedServiceState::new(
    104             operational,
    105             PassiveMetrics {
    106                 probe_calls: Arc::new(AtomicUsize::new(0)),
    107             },
    108         ));
    109         reader
    110     }
    111 
    112     #[test]
    113     fn every_phase_has_an_exact_bounded_health_projection() {
    114         let cases = [
    115             (
    116                 ServicePhase::Starting,
    117                 Readiness::NOT_READY,
    118                 StatusCode::OK,
    119                 LIVE_BODY,
    120                 StatusCode::SERVICE_UNAVAILABLE,
    121                 UNREADY_BODY,
    122             ),
    123             (
    124                 ServicePhase::Ready,
    125                 Readiness::READY,
    126                 StatusCode::OK,
    127                 LIVE_BODY,
    128                 StatusCode::OK,
    129                 READY_BODY,
    130             ),
    131             (
    132                 ServicePhase::Degraded,
    133                 Readiness::READY,
    134                 StatusCode::OK,
    135                 LIVE_BODY,
    136                 StatusCode::OK,
    137                 READY_BODY,
    138             ),
    139             (
    140                 ServicePhase::Degraded,
    141                 Readiness::NOT_READY,
    142                 StatusCode::OK,
    143                 LIVE_BODY,
    144                 StatusCode::SERVICE_UNAVAILABLE,
    145                 UNREADY_BODY,
    146             ),
    147             (
    148                 ServicePhase::Unready,
    149                 Readiness::NOT_READY,
    150                 StatusCode::OK,
    151                 LIVE_BODY,
    152                 StatusCode::SERVICE_UNAVAILABLE,
    153                 UNREADY_BODY,
    154             ),
    155             (
    156                 ServicePhase::Stopping,
    157                 Readiness::NOT_READY,
    158                 StatusCode::OK,
    159                 LIVE_BODY,
    160                 StatusCode::SERVICE_UNAVAILABLE,
    161                 UNREADY_BODY,
    162             ),
    163             (
    164                 ServicePhase::Failed,
    165                 Readiness::NOT_READY,
    166                 StatusCode::SERVICE_UNAVAILABLE,
    167                 FAILED_BODY,
    168                 StatusCode::SERVICE_UNAVAILABLE,
    169                 UNREADY_BODY,
    170             ),
    171         ];
    172 
    173         for (phase, readiness, live_status, live_body, ready_status, ready_body) in cases {
    174             let reader = cache(phase, readiness);
    175             let live = livez(&reader);
    176             let ready = readyz(&reader);
    177 
    178             assert_eq!((live.status(), live.body()), (live_status, live_body));
    179             assert_eq!((ready.status(), ready.body()), (ready_status, ready_body));
    180             assert_eq!(live.content_type(), OPERATIONS_HEALTH_CONTENT_TYPE);
    181             assert_eq!(ready.content_type(), OPERATIONS_HEALTH_CONTENT_TYPE);
    182             assert!(live.body().len() <= UNREADY_BODY.len());
    183             assert!(ready.body().len() <= UNREADY_BODY.len());
    184             assert_eq!(reader.snapshot().metrics().probe_calls(), 0);
    185         }
    186     }
    187 
    188     #[test]
    189     fn handlers_read_the_latest_snapshot_without_waiting_or_probing() {
    190         let probe_calls = Arc::new(AtomicUsize::new(0));
    191         let starting = ServiceOperationalState::new(
    192             ServicePhase::Starting,
    193             Readiness::NOT_READY,
    194             ReasonCodes::empty(),
    195         )
    196         .unwrap();
    197         let (mut publisher, reader) = cached_service_state(CachedServiceState::new(
    198             starting,
    199             PassiveMetrics {
    200                 probe_calls: Arc::clone(&probe_calls),
    201             },
    202         ));
    203 
    204         assert_eq!(readyz(&reader).status(), StatusCode::SERVICE_UNAVAILABLE);
    205         let ready = ServiceOperationalState::new(
    206             ServicePhase::Ready,
    207             Readiness::READY,
    208             ReasonCodes::empty(),
    209         )
    210         .unwrap();
    211         publisher
    212             .publish(CachedServiceState::new(
    213                 ready,
    214                 PassiveMetrics {
    215                     probe_calls: Arc::clone(&probe_calls),
    216                 },
    217             ))
    218             .unwrap();
    219 
    220         assert_eq!(readyz(&reader).status(), StatusCode::OK);
    221         assert_eq!(probe_calls.load(Ordering::SeqCst), 0);
    222     }
    223 }