1use std::{
19 collections::{HashMap, HashSet},
20 sync::{Arc, OnceLock},
21 time::Instant,
22};
23use tokio::sync::mpsc;
24
25use crate::component;
26use crate::config::HealthStatus;
27use crate::metrics::{MetricsHierarchy, prometheus_names::distributed_runtime};
28
29pub struct ReadinessHold {
44 system_health: Arc<parking_lot::Mutex<SystemHealth>>,
45 endpoint: String,
46}
47
48impl ReadinessHold {
49 pub fn take(system_health: Arc<parking_lot::Mutex<SystemHealth>>, endpoint: &str) -> Self {
50 system_health.lock().hold_endpoint_readiness(endpoint);
51 Self {
52 system_health,
53 endpoint: endpoint.to_string(),
54 }
55 }
56}
57
58impl Drop for ReadinessHold {
59 fn drop(&mut self) {
60 self.system_health
61 .lock()
62 .release_endpoint_readiness(&self.endpoint);
63 }
64}
65
66#[derive(Clone, Debug)]
68pub struct HealthCheckTarget {
69 pub instance: component::Instance,
70 pub payload: serde_json::Value,
71}
72
73#[derive(Clone)]
78pub struct SystemHealth {
79 system_health: HealthStatus,
80 endpoint_health: Arc<std::sync::RwLock<HashMap<String, HealthStatus>>>,
81 health_check_targets: Arc<std::sync::RwLock<HashMap<String, HealthCheckTarget>>>,
83 health_check_notifiers: Arc<std::sync::RwLock<HashMap<String, Arc<tokio::sync::Notify>>>>,
85 readiness_holds: Arc<std::sync::RwLock<HashSet<String>>>,
88 new_endpoint_tx: mpsc::UnboundedSender<String>,
92 new_endpoint_rx: Arc<parking_lot::Mutex<Option<mpsc::UnboundedReceiver<String>>>>,
93 use_endpoint_health_status: Vec<String>,
94 health_check_enabled: bool,
95 health_path: String,
96 live_path: String,
97 start_time: Instant,
98 uptime_gauge: OnceLock<prometheus::Gauge>,
99}
100
101impl SystemHealth {
102 pub fn new(
103 starting_health_status: HealthStatus,
104 use_endpoint_health_status: Vec<String>,
105 health_check_enabled: bool,
106 health_path: String,
107 live_path: String,
108 ) -> Self {
109 let initial_endpoint_status = if health_check_enabled {
111 HealthStatus::NotReady
112 } else {
113 starting_health_status.clone()
114 };
115 let mut endpoint_health = HashMap::new();
116 for endpoint in &use_endpoint_health_status {
117 endpoint_health.insert(endpoint.clone(), initial_endpoint_status.clone());
118 }
119
120 let (tx, rx) = mpsc::unbounded_channel();
122
123 SystemHealth {
124 system_health: starting_health_status,
125 endpoint_health: Arc::new(std::sync::RwLock::new(endpoint_health)),
126 health_check_targets: Arc::new(std::sync::RwLock::new(HashMap::new())),
127 health_check_notifiers: Arc::new(std::sync::RwLock::new(HashMap::new())),
128 readiness_holds: Arc::new(std::sync::RwLock::new(HashSet::new())),
129 new_endpoint_tx: tx,
130 new_endpoint_rx: Arc::new(parking_lot::Mutex::new(Some(rx))),
131 use_endpoint_health_status,
132 health_check_enabled,
133 health_path,
134 live_path,
135 start_time: Instant::now(),
136 uptime_gauge: OnceLock::new(),
137 }
138 }
139
140 pub fn health_check_enabled(&self) -> bool {
141 self.health_check_enabled
142 }
143
144 pub fn set_endpoint_registered(&self, endpoint: &str) {
148 if self.readiness_holds.read().unwrap().contains(endpoint) {
149 return;
150 }
151 let has_health_check_target = self
152 .health_check_targets
153 .read()
154 .unwrap()
155 .contains_key(endpoint);
156 if !self.health_check_enabled || !has_health_check_target {
157 self.set_endpoint_health_status(endpoint, HealthStatus::Ready);
158 }
159 }
160
161 pub fn hold_endpoint_readiness(&self, endpoint: &str) {
178 self.readiness_holds
179 .write()
180 .unwrap()
181 .insert(endpoint.to_string());
182 }
183
184 pub fn release_endpoint_readiness(&self, endpoint: &str) {
189 self.readiness_holds.write().unwrap().remove(endpoint);
190 }
191
192 pub fn set_health_status(&mut self, status: HealthStatus) {
193 self.system_health = status;
194 }
195
196 pub fn set_endpoint_health_status(&self, endpoint: &str, status: HealthStatus) {
197 let mut endpoint_health = self.endpoint_health.write().unwrap();
198 endpoint_health.insert(endpoint.to_string(), status);
199 }
200
201 pub fn get_health_status(&self) -> (bool, HashMap<String, String>) {
204 let health_check_targets = self.health_check_targets.read().unwrap();
205 let endpoint_health = self.endpoint_health.read().unwrap();
206 let mut endpoints: HashMap<String, String> = HashMap::new();
207
208 for (endpoint, status) in endpoint_health.iter() {
209 endpoints.insert(
210 endpoint.clone(),
211 if *status == HealthStatus::Ready {
212 "ready".to_string()
213 } else {
214 "notready".to_string()
215 },
216 );
217 }
218
219 if !self.readiness_holds.read().unwrap().is_empty() {
223 return (false, endpoints);
224 }
225
226 let healthy = if !self.use_endpoint_health_status.is_empty() {
227 self.use_endpoint_health_status.iter().all(|endpoint| {
228 endpoint_health
229 .get(endpoint)
230 .is_some_and(|status| *status == HealthStatus::Ready)
231 })
232 } else {
233 if !health_check_targets.is_empty() {
235 health_check_targets
236 .iter()
237 .all(|(endpoint_subject, _target)| {
238 endpoint_health
239 .get(endpoint_subject)
240 .is_some_and(|status| *status == HealthStatus::Ready)
241 })
242 } else if self.health_check_enabled && !endpoint_health.is_empty() {
243 endpoint_health
247 .values()
248 .all(|status| *status == HealthStatus::Ready)
249 } else {
250 self.system_health == HealthStatus::Ready
252 }
253 };
254
255 (healthy, endpoints)
256 }
257
258 pub fn register_health_check_target(
260 &self,
261 endpoint_subject: &str,
262 instance: component::Instance,
263 payload: serde_json::Value,
264 ) {
265 let key = endpoint_subject.to_owned();
266
267 let inserted = {
269 let mut targets = self.health_check_targets.write().unwrap();
270 match targets.entry(key.clone()) {
271 std::collections::hash_map::Entry::Occupied(_) => false,
272 std::collections::hash_map::Entry::Vacant(v) => {
273 v.insert(HealthCheckTarget { instance, payload });
274 true
275 }
276 }
277 };
278
279 if !inserted {
280 tracing::warn!(
281 "Attempted to re-register health check for endpoint '{}'; ignoring.",
282 key
283 );
284 return;
285 }
286
287 {
289 let mut notifiers = self.health_check_notifiers.write().unwrap();
290 notifiers
291 .entry(key.clone())
292 .or_insert_with(|| Arc::new(tokio::sync::Notify::new()));
293 }
294
295 {
297 let mut endpoint_health = self.endpoint_health.write().unwrap();
298 endpoint_health
299 .entry(key.clone())
300 .or_insert(HealthStatus::NotReady);
301 }
302
303 if let Err(e) = self.new_endpoint_tx.send(key.clone()) {
304 tracing::error!(
305 "Failed to send endpoint '{}' registration to health check manager: {}. \
306 Health checks will not be performed for this endpoint.",
307 key,
308 e
309 );
310 }
311 }
312
313 pub fn get_health_check_targets(&self) -> Vec<(String, HealthCheckTarget)> {
315 let targets = self.health_check_targets.read().unwrap();
316 targets
317 .iter()
318 .map(|(k, v)| (k.clone(), v.clone()))
319 .collect()
320 }
321
322 pub fn has_health_check_targets(&self) -> bool {
324 let targets = self.health_check_targets.read().unwrap();
325 !targets.is_empty()
326 }
327
328 pub fn get_health_check_endpoints(&self) -> Vec<String> {
330 let targets = self.health_check_targets.read().unwrap();
331 targets.keys().cloned().collect()
332 }
333
334 pub fn get_health_check_target(&self, endpoint: &str) -> Option<HealthCheckTarget> {
336 let targets = self.health_check_targets.read().unwrap();
337 targets.get(endpoint).cloned()
338 }
339
340 pub fn get_endpoint_health_status(&self, endpoint: &str) -> Option<HealthStatus> {
342 let endpoint_health = self.endpoint_health.read().unwrap();
343 endpoint_health.get(endpoint).cloned()
344 }
345
346 pub fn get_endpoint_health_check_notifier(
348 &self,
349 endpoint_subject: &str,
350 ) -> Option<Arc<tokio::sync::Notify>> {
351 let notifiers = self.health_check_notifiers.read().unwrap();
352 notifiers.get(endpoint_subject).cloned()
353 }
354
355 pub fn take_new_endpoint_receiver(&self) -> Option<mpsc::UnboundedReceiver<String>> {
358 self.new_endpoint_rx.lock().take()
359 }
360
361 pub fn initialize_uptime_gauge<T: MetricsHierarchy>(&self, registry: &T) -> anyhow::Result<()> {
363 let gauge = registry.metrics().create_gauge(
364 distributed_runtime::UPTIME_SECONDS,
365 "Total uptime of the DistributedRuntime in seconds",
366 &[],
367 )?;
368 self.uptime_gauge
369 .set(gauge)
370 .map_err(|_| anyhow::anyhow!("uptime_gauge already initialized"))?;
371 Ok(())
372 }
373
374 pub fn uptime(&self) -> std::time::Duration {
376 self.start_time.elapsed()
377 }
378
379 pub fn update_uptime_gauge(&self) {
381 if let Some(gauge) = self.uptime_gauge.get() {
382 gauge.set(self.uptime().as_secs_f64());
383 }
384 }
385
386 pub fn health_path(&self) -> &str {
388 &self.health_path
389 }
390
391 pub fn live_path(&self) -> &str {
393 &self.live_path
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::component::{Instance, TransportType};
401
402 const ENDPOINT: &str = "generate";
403
404 fn system_health(health_check_enabled: bool) -> SystemHealth {
405 SystemHealth::new(
406 HealthStatus::NotReady,
407 Vec::new(),
410 health_check_enabled,
411 "/health".to_string(),
412 "/live".to_string(),
413 )
414 }
415
416 fn instance() -> Instance {
417 Instance {
418 component: "backend".to_string(),
419 endpoint: ENDPOINT.to_string(),
420 namespace: "dynamo".to_string(),
421 instance_id: 1,
422 transport: TransportType::Tcp("127.0.0.1:0".to_string()),
423 device_type: None,
424 request_plane_codec: None,
425 }
426 }
427
428 #[test]
431 fn registered_target_makes_the_worker_ready_with_canary_off() {
432 let health = system_health(false);
433 health.register_health_check_target(ENDPOINT, instance(), serde_json::json!({}));
434 health.set_endpoint_registered(ENDPOINT);
435
436 let (healthy, endpoints) = health.get_health_status();
437 assert!(healthy, "a registered, ready endpoint must report healthy");
438 assert_eq!(endpoints.get(ENDPOINT).map(String::as_str), Some("ready"));
439 }
440
441 #[test]
453 fn ready_endpoint_without_a_registered_target_still_reports_unhealthy() {
454 let health = system_health(false);
455 health.set_endpoint_registered(ENDPOINT);
457
458 let (healthy, endpoints) = health.get_health_status();
459 assert_eq!(
460 endpoints.get(ENDPOINT).map(String::as_str),
461 Some("ready"),
462 "the endpoint itself is ready"
463 );
464 assert!(
465 !healthy,
466 "with no health-check target the endpoint's readiness is ignored and \
467 the worker falls back to system_health (NotReady) — this is the 503"
468 );
469 }
470
471 #[test]
474 fn held_endpoint_is_not_ready_on_transport_registration() {
475 let health = system_health(false);
476 health.hold_endpoint_readiness(ENDPOINT);
477 health.set_endpoint_registered(ENDPOINT);
478 assert_ne!(
479 health.get_endpoint_health_status(ENDPOINT),
480 Some(HealthStatus::Ready),
481 "a held endpoint must not be marked ready by transport registration"
482 );
483
484 health.release_endpoint_readiness(ENDPOINT);
485 health.set_endpoint_registered(ENDPOINT);
486 assert_eq!(
487 health.get_endpoint_health_status(ENDPOINT),
488 Some(HealthStatus::Ready),
489 "after release the owner's registration signal publishes readiness"
490 );
491 }
492
493 #[test]
499 fn an_unheld_sibling_endpoint_cannot_report_the_worker_ready() {
500 let health = system_health(true);
501 health.hold_endpoint_readiness(ENDPOINT);
502 health.set_endpoint_registered(ENDPOINT);
503 health.set_endpoint_registered("rl_system");
504
505 assert!(
506 !health.get_health_status().0,
507 "a held endpoint must keep the worker not-ready however many siblings register"
508 );
509
510 health.release_endpoint_readiness(ENDPOINT);
511 health.set_endpoint_registered(ENDPOINT);
512 assert!(health.get_health_status().0);
513 }
514
515 #[test]
519 fn a_hold_does_not_suppress_a_siblings_endpoint_flag() {
520 let health = system_health(false);
521 health.hold_endpoint_readiness(ENDPOINT);
522 health.set_endpoint_registered("other");
523 assert_eq!(
524 health.get_endpoint_health_status("other"),
525 Some(HealthStatus::Ready)
526 );
527 }
528
529 #[test]
532 fn without_targets_health_tracks_system_health_only() {
533 let mut health = system_health(false);
534 health.set_endpoint_registered(ENDPOINT);
535 assert!(!health.get_health_status().0);
536
537 health.set_health_status(HealthStatus::Ready);
538 assert!(
539 health.get_health_status().0,
540 "with no targets, system_health alone decides"
541 );
542 }
543
544 #[test]
547 fn payloadless_endpoint_is_ready_with_canary_on() {
548 let health = system_health(true);
549 health.set_endpoint_registered(ENDPOINT);
550
551 let (healthy, endpoints) = health.get_health_status();
552 assert!(
553 healthy,
554 "a registered payload-less endpoint must report healthy"
555 );
556 assert_eq!(endpoints.get(ENDPOINT).map(String::as_str), Some("ready"));
557 }
558
559 #[test]
565 fn canary_enabled_withholds_ready_until_verified() {
566 let health = system_health(true);
567 health.register_health_check_target(ENDPOINT, instance(), serde_json::json!({}));
568 health.set_endpoint_registered(ENDPOINT);
569
570 assert!(
571 !health.get_health_status().0,
572 "canary must verify before the worker reports ready"
573 );
574
575 health.set_endpoint_health_status(ENDPOINT, HealthStatus::Ready);
576 assert!(
577 health.get_health_status().0,
578 "after the canary marks it ready the worker is healthy"
579 );
580 }
581}