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 }