1use std::future::Future;
4#[cfg(test)]
5use std::sync::atomic::AtomicUsize;
6use std::{
7 collections::HashSet,
8 sync::{
9 atomic::{AtomicBool, Ordering::Relaxed},
10 Arc, Mutex,
11 },
12 time::Duration,
13};
14
15use futures::StreamExt as _;
16use saluki_error::{generic_error, GenericError};
17use saluki_metrics::{static_metrics, Gauge, Histogram};
18use tokio::{pin, time::Instant};
19use tokio::{
20 select,
21 sync::{
22 mpsc::{self, error::TrySendError},
23 Notify,
24 },
25};
26use tokio_util::time::{delay_queue::Key, DelayQueue};
27use tracing::{debug, info, trace};
28
29use crate::support::SubsystemIdentifier;
30
31mod api;
32pub use self::api::HealthAPIHandler;
33
34mod worker;
35pub use self::worker::HealthRegistryWorker;
36
37const DEFAULT_PROBE_TIMEOUT_DUR: Duration = Duration::from_secs(5);
38const DEFAULT_PROBE_BACKOFF_DUR: Duration = Duration::from_secs(1);
39
40pub struct Health {
42 shared: Arc<SharedComponentState>,
43 request_rx: mpsc::Receiver<LivenessRequest>,
44 response_tx: mpsc::Sender<LivenessResponse>,
45 readiness_notify: Arc<Notify>,
46}
47
48impl Health {
49 pub fn mark_ready(&mut self) {
51 self.update_readiness(true);
52 }
53
54 pub fn mark_not_ready(&mut self) {
56 self.update_readiness(false);
57 }
58
59 fn update_readiness(&self, ready: bool) {
60 self.shared.ready.store(ready, Relaxed);
61 self.shared.telemetry.update_readiness(ready);
62
63 if ready {
65 self.readiness_notify.notify_waiters();
66 }
67 }
68
69 pub async fn live(&mut self) {
74 if let Some(request) = self.request_rx.recv().await {
77 let response = request.into_response();
78 let _ = self.response_tx.send(response).await;
79 }
80 }
81}
82
83#[derive(Clone, Copy, Eq, PartialEq)]
84enum HealthState {
85 Live,
86 Unknown,
87 Dead,
88}
89
90#[static_metrics(prefix = health, labels(component_id))]
91#[derive(Clone)]
92struct Telemetry {
93 component_ready: Gauge,
94 component_live: Gauge,
95 #[metric(level = trace)]
96 component_liveness_latency_seconds: Histogram,
97}
98
99impl Telemetry {
100 fn from_name(name: &SubsystemIdentifier) -> Self {
101 Self::new(name)
102 }
103
104 fn update_readiness(&self, ready: bool) {
105 self.component_ready().set(if ready { 1.0 } else { 0.0 });
106 }
107
108 fn update_liveness(&self, state: HealthState, response_latency: Duration) {
109 let live = match state {
110 HealthState::Live => 1.0,
111 HealthState::Unknown => 0.0,
112 HealthState::Dead => -1.0,
113 };
114
115 self.component_live().set(live);
116 self.component_liveness_latency_seconds()
117 .record(response_latency.as_secs_f64());
118 }
119}
120
121struct SharedComponentState {
122 ready: AtomicBool,
123 telemetry: Telemetry,
124}
125
126struct ComponentState {
127 name: SubsystemIdentifier,
128 health: HealthState,
129 shared: Arc<SharedComponentState>,
130 request_tx: mpsc::Sender<LivenessRequest>,
131 last_response: Instant,
132 last_response_latency: Duration,
133}
134
135impl ComponentState {
136 fn new(
137 name: SubsystemIdentifier, response_tx: mpsc::Sender<LivenessResponse>, readiness_notify: Arc<Notify>,
138 ) -> (Self, Health) {
139 let shared = Arc::new(SharedComponentState {
140 ready: AtomicBool::new(false),
141 telemetry: Telemetry::from_name(&name),
142 });
143 let (request_tx, request_rx) = mpsc::channel(1);
144
145 let state = Self {
146 name,
147 health: HealthState::Unknown,
148 shared: Arc::clone(&shared),
149 request_tx,
150 last_response: Instant::now(),
151 last_response_latency: Duration::from_secs(0),
152 };
153
154 let handle = Health {
155 shared,
156 request_rx,
157 response_tx,
158 readiness_notify,
159 };
160
161 (state, handle)
162 }
163
164 fn is_ready(&self) -> bool {
165 self.shared.ready.load(Relaxed) && self.health != HealthState::Dead
170 }
171
172 fn is_live(&self) -> bool {
173 self.health == HealthState::Live
174 }
175
176 fn mark_live(&mut self, response_sent: Instant, response_latency: Duration) {
177 self.health = HealthState::Live;
178 self.last_response = response_sent;
179 self.last_response_latency = response_latency;
180 self.shared.telemetry.update_liveness(self.health, response_latency);
181 }
182
183 fn mark_not_live(&mut self) {
184 self.health = HealthState::Unknown;
185
186 self.shared
188 .telemetry
189 .update_liveness(self.health, DEFAULT_PROBE_TIMEOUT_DUR);
190 }
191
192 fn mark_dead(&mut self) {
193 self.health = HealthState::Dead;
194
195 self.shared
197 .telemetry
198 .update_liveness(self.health, DEFAULT_PROBE_TIMEOUT_DUR);
199 }
200}
201
202struct LivenessRequest {
203 component_id: usize,
204 timeout_key: Key,
205 request_sent: Instant,
206}
207
208impl LivenessRequest {
209 fn new(component_id: usize, timeout_key: Key) -> Self {
210 Self {
211 component_id,
212 timeout_key,
213 request_sent: Instant::now(),
214 }
215 }
216
217 fn into_response(self) -> LivenessResponse {
218 LivenessResponse {
219 request: self,
220 response_sent: Instant::now(),
221 }
222 }
223}
224
225struct LivenessResponse {
226 request: LivenessRequest,
227 response_sent: Instant,
228}
229
230enum HealthUpdate {
231 Alive {
232 last_response: Instant,
233 last_response_latency: Duration,
234 },
235 Unknown,
236 Dead,
237}
238
239impl HealthUpdate {
240 fn as_str(&self) -> &'static str {
241 match self {
242 HealthUpdate::Alive { .. } => "alive",
243 HealthUpdate::Unknown => "unknown",
244 HealthUpdate::Dead => "dead",
245 }
246 }
247}
248
249struct RegistryState {
250 registered_components: HashSet<SubsystemIdentifier>,
251 component_state: Vec<ComponentState>,
252 responses_tx: mpsc::Sender<LivenessResponse>,
253 responses_rx: Option<mpsc::Receiver<LivenessResponse>>,
254 pending_components: Vec<usize>,
255 pending_components_notify: Arc<Notify>,
256 readiness_notify: Arc<Notify>,
257}
258
259impl RegistryState {
260 fn new() -> Self {
261 let (responses_tx, responses_rx) = mpsc::channel(16);
262
263 Self {
264 registered_components: HashSet::new(),
265 component_state: Vec::new(),
266 responses_tx,
267 responses_rx: Some(responses_rx),
268 pending_components: Vec::new(),
269 pending_components_notify: Arc::new(Notify::new()),
270 readiness_notify: Arc::new(Notify::new()),
271 }
272 }
273}
274
275#[derive(Clone)]
293pub struct HealthRegistry {
294 inner: Arc<Mutex<RegistryState>>,
295}
296
297impl HealthRegistry {
298 pub fn new() -> Self {
300 Self {
301 inner: Arc::new(Mutex::new(RegistryState::new())),
302 }
303 }
304
305 #[cfg(test)]
306 fn state(&self) -> Arc<Mutex<RegistryState>> {
307 Arc::clone(&self.inner)
308 }
309
310 pub fn register_component(&self, id: &SubsystemIdentifier) -> Option<Health> {
316 let mut inner = self.inner.lock().unwrap();
317
318 if !inner.registered_components.insert(id.clone()) {
320 return None;
321 }
322
323 let readiness_notify = Arc::clone(&inner.readiness_notify);
325 let (state, handle) = ComponentState::new(id.clone(), inner.responses_tx.clone(), readiness_notify);
326 let component_id = inner.component_state.len();
327 inner.component_state.push(state);
328
329 debug!(component_id, "Registered component '{}'.", id);
330
331 inner.pending_components.push(component_id);
333 inner.pending_components_notify.notify_one();
334
335 Some(handle)
336 }
337
338 pub fn api_handler(&self) -> HealthAPIHandler {
343 HealthAPIHandler::from_state(Arc::clone(&self.inner))
344 }
345
346 pub async fn all_ready(&self) {
354 self.all_ready_matching(|_| true).await
355 }
356
357 pub async fn all_ready_matching<F>(&self, predicate: F)
367 where
368 F: Fn(&SubsystemIdentifier) -> bool,
369 {
370 let readiness_notify = {
371 let inner = self.inner.lock().unwrap();
372 Arc::clone(&inner.readiness_notify)
373 };
374
375 loop {
376 let notified = readiness_notify.notified();
378
379 if self.check_ready_matching(&predicate) {
380 return;
381 }
382
383 notified.await;
384 }
385 }
386
387 pub async fn all_ready_under(&self, root: SubsystemIdentifier) {
394 self.all_ready_matching(move |id| root.is_ancestor_of(id)).await
395 }
396
397 fn check_ready_matching<F>(&self, predicate: &F) -> bool
398 where
399 F: Fn(&SubsystemIdentifier) -> bool,
400 {
401 let inner = self.inner.lock().unwrap();
402 inner
403 .component_state
404 .iter()
405 .filter(|component| predicate(&component.name))
406 .all(|component| component.is_ready())
407 }
408
409 pub fn snapshot_json(&self) -> String {
416 #[derive(serde::Serialize)]
417 struct ComponentSnapshot {
418 live: bool,
419 ready: bool,
420 }
421
422 let inner = self.inner.lock().unwrap();
423 let mut state: std::collections::HashMap<String, ComponentSnapshot> = std::collections::HashMap::new();
424 for component in &inner.component_state {
425 state.insert(
426 component.name.to_string(),
427 ComponentSnapshot {
428 live: component.is_live(),
429 ready: component.is_ready(),
430 },
431 );
432 }
433 serde_json::to_string_pretty(&state).unwrap_or_else(|e| format!("{{\"error\": \"{e}\"}}"))
434 }
435
436 pub fn worker(&self) -> HealthRegistryWorker {
441 HealthRegistryWorker::new(self.clone())
442 }
443
444 pub(crate) fn into_runner(self) -> Result<Runner, GenericError> {
445 let (responses_rx, pending_components_notify) = {
447 let mut inner = self.inner.lock().unwrap();
448 let responses_rx = match inner.responses_rx.take() {
449 Some(rx) => rx,
450 None => return Err(generic_error!("health registry already spawned")),
451 };
452
453 let pending_components_notify = Arc::clone(&inner.pending_components_notify);
454 (responses_rx, pending_components_notify)
455 };
456
457 Ok(Runner::new(self.inner, responses_rx, pending_components_notify))
458 }
459}
460
461struct RunnerGuard {
467 registry: Arc<Mutex<RegistryState>>,
468 responses_rx: Option<mpsc::Receiver<LivenessResponse>>,
469}
470
471impl Drop for RunnerGuard {
472 fn drop(&mut self) {
473 if let Some(rx) = self.responses_rx.take() {
474 let mut inner = self.registry.lock().expect("registry state poisoned");
475 inner.responses_rx = Some(rx);
476 debug!("Returned response receiver to registry state.");
477 }
478 }
479}
480
481#[cfg(test)]
482struct RunnerState {
483 pending_scheduled_probes: AtomicUsize,
484 pending_probe_timeouts: AtomicUsize,
485}
486
487#[cfg(test)]
488impl RunnerState {
489 fn new() -> Self {
490 Self {
491 pending_scheduled_probes: AtomicUsize::new(0),
492 pending_probe_timeouts: AtomicUsize::new(0),
493 }
494 }
495
496 fn pending_scheduled_probes(&self) -> usize {
497 self.pending_scheduled_probes.load(Relaxed)
498 }
499
500 fn pending_probe_timeouts(&self) -> usize {
501 self.pending_probe_timeouts.load(Relaxed)
502 }
503
504 fn increment_pending_scheduled_probes(&self) {
505 self.pending_scheduled_probes.fetch_add(1, Relaxed);
506 }
507
508 fn increment_pending_probe_timeouts(&self) {
509 self.pending_probe_timeouts.fetch_add(1, Relaxed);
510 }
511
512 fn decrement_pending_scheduled_probes(&self) {
513 self.pending_scheduled_probes.fetch_sub(1, Relaxed);
514 }
515
516 fn decrement_pending_probe_timeouts(&self) {
517 self.pending_probe_timeouts.fetch_sub(1, Relaxed);
518 }
519}
520
521pub(super) struct Runner {
522 registry: Arc<Mutex<RegistryState>>,
523 pending_probes: DelayQueue<usize>,
524 pending_timeouts: DelayQueue<usize>,
525 guard: RunnerGuard,
526 pending_components_notify: Arc<Notify>,
527 #[cfg(test)]
528 state: Arc<RunnerState>,
529}
530
531impl Runner {
532 fn new(
533 registry: Arc<Mutex<RegistryState>>, responses_rx: mpsc::Receiver<LivenessResponse>,
534 pending_components_notify: Arc<Notify>,
535 ) -> Self {
536 #[cfg(test)]
537 let state = Arc::new(RunnerState::new());
538
539 let guard = RunnerGuard {
540 registry: Arc::clone(®istry),
541 responses_rx: Some(responses_rx),
542 };
543
544 Self {
545 registry,
546 pending_probes: DelayQueue::new(),
547 pending_timeouts: DelayQueue::new(),
548 guard,
549 pending_components_notify,
550 #[cfg(test)]
551 state,
552 }
553 }
554
555 #[cfg(test)]
556 fn state(&self) -> Arc<RunnerState> {
557 Arc::clone(&self.state)
558 }
559
560 fn drain_pending_components(&mut self) -> Vec<usize> {
561 let mut registry = self.registry.lock().unwrap();
563 registry.pending_components.drain(..).collect()
564 }
565
566 fn send_component_probe_request(&mut self, component_id: usize) -> Option<HealthUpdate> {
567 let mut registry = self.registry.lock().unwrap();
568 let component_state = &mut registry.component_state[component_id];
569
570 if component_state.request_tx.is_closed() {
572 debug!(component_name = %component_state.name, "Component is dead, skipping liveness probe.");
573 return Some(HealthUpdate::Dead);
574 }
575
576 trace!(component_name = %component_state.name, probe_timeout = ?DEFAULT_PROBE_TIMEOUT_DUR, "Sending liveness probe to component.");
577
578 let timeout_key = self.pending_timeouts.insert(component_id, DEFAULT_PROBE_TIMEOUT_DUR);
583
584 #[cfg(test)]
585 self.state.increment_pending_probe_timeouts();
586
587 let request = LivenessRequest::new(component_id, timeout_key);
588 if let Err(TrySendError::Closed(request)) = component_state.request_tx.try_send(request) {
589 debug!(component_name = %component_state.name, "Component is dead, removing pending timeout.");
590
591 self.pending_timeouts.remove(&request.timeout_key);
597
598 #[cfg(test)]
599 self.state.decrement_pending_probe_timeouts();
600
601 return Some(HealthUpdate::Dead);
602 }
603
604 None
605 }
606
607 fn schedule_probe_for_component(&mut self, component_id: usize, duration: Duration) {
608 #[cfg(test)]
609 self.state.increment_pending_scheduled_probes();
610
611 self.pending_probes.insert(component_id, duration);
612 }
613
614 fn schedule_all_existing_components(&mut self, responses_rx: &mut mpsc::Receiver<LivenessResponse>) {
615 let _pending = self.drain_pending_components();
618
619 while let Ok(response) = responses_rx.try_recv() {
624 self.handle_component_probe_response(response);
625 }
626
627 let (component_count, stale_component_ids) = {
631 let registry = self.registry.lock().unwrap();
632 let now = Instant::now();
633 let stale_ids: Vec<usize> = (0..registry.component_state.len())
634 .filter(|&id| {
635 let last = registry.component_state[id].last_response;
636 saluki_antithesis::always_or_unreachable!(
637 now >= last,
638 "health probe last-response clock did not move backward"
639 );
640 now.saturating_duration_since(last) >= DEFAULT_PROBE_TIMEOUT_DUR
641 })
642 .collect();
643 (registry.component_state.len(), stale_ids)
644 };
645
646 for &component_id in &stale_component_ids {
648 self.process_component_health_update(component_id, HealthUpdate::Unknown);
649 }
650
651 for component_id in 0..component_count {
653 self.schedule_probe_for_component(component_id, Duration::ZERO);
654 }
655
656 if component_count > 0 {
657 let fresh_count = component_count - stale_component_ids.len();
658 debug!(
659 component_count,
660 fresh_count,
661 stale_count = stale_component_ids.len(),
662 "Scheduled probes for all existing components."
663 );
664 }
665 }
666
667 fn handle_component_probe_response(&mut self, response: LivenessResponse) {
668 let component_id = response.request.component_id;
669 let timeout_key = response.request.timeout_key;
670 let request_sent = response.request.request_sent;
671 let response_sent = response.response_sent;
672 let response_latency = response_sent.checked_duration_since(request_sent).unwrap_or_default();
673
674 let timeout_was_pending = self.pending_timeouts.try_remove(&timeout_key).is_some();
676 if !timeout_was_pending {
677 let mut registry = self.registry.lock().unwrap();
678 let component_state = &mut registry.component_state[component_id];
679
680 debug!(component_name = %component_state.name, "Received probe response for component that already timed out.");
681 }
682
683 let update = HealthUpdate::Alive {
685 last_response: response_sent,
686 last_response_latency: response_latency,
687 };
688 self.process_component_health_update(component_id, update);
689
690 if timeout_was_pending {
693 #[cfg(test)]
694 self.state.decrement_pending_probe_timeouts();
695
696 self.schedule_probe_for_component(component_id, DEFAULT_PROBE_BACKOFF_DUR);
697 }
698 }
699
700 fn handle_component_timeout(&mut self, component_id: usize) {
701 self.process_component_health_update(component_id, HealthUpdate::Unknown);
703
704 self.schedule_probe_for_component(component_id, DEFAULT_PROBE_BACKOFF_DUR);
706 }
707
708 fn process_component_health_update(&mut self, component_id: usize, update: HealthUpdate) {
709 let mut registry = self.registry.lock().unwrap();
711 let component_state = &mut registry.component_state[component_id];
712 trace!(component_name = %component_state.name, status = update.as_str(), "Updating component health status.");
713
714 match update {
715 HealthUpdate::Alive {
716 last_response,
717 last_response_latency,
718 } => component_state.mark_live(last_response, last_response_latency),
719 HealthUpdate::Unknown => component_state.mark_not_live(),
720 HealthUpdate::Dead => component_state.mark_dead(),
721 }
722 }
723
724 async fn run<F: Future<Output = ()>>(mut self, shutdown: F) {
725 info!("Health checker running.");
726
727 let mut responses_rx = self
730 .guard
731 .responses_rx
732 .take()
733 .expect("responses_rx should always be Some when Runner is created");
734
735 self.schedule_all_existing_components(&mut responses_rx);
739
740 pin!(shutdown);
742
743 loop {
744 select! {
745 _ = &mut shutdown => {
747 info!("Health checker shutting down.");
748 break;
749 },
750
751 Some(entry) = self.pending_probes.next() => {
753 #[cfg(test)]
754 self.state.decrement_pending_scheduled_probes();
755
756 let component_id = entry.into_inner();
757 if let Some(health_update) = self.send_component_probe_request(component_id) {
758 self.process_component_health_update(component_id, health_update);
761 }
762 },
763
764 Some(entry) = self.pending_timeouts.next() => {
766 #[cfg(test)]
767 self.state.decrement_pending_probe_timeouts();
768
769 let component_id = entry.into_inner();
770 self.handle_component_timeout(component_id);
771 },
772
773 Some(response) = responses_rx.recv() => {
775 self.handle_component_probe_response(response);
776 },
777
778 _ = self.pending_components_notify.notified() => {
780 let pending_component_ids = self.drain_pending_components();
782 for pending_component_id in pending_component_ids {
783 self.process_component_health_update(pending_component_id, HealthUpdate::Unknown);
784 self.schedule_probe_for_component(pending_component_id, Duration::ZERO);
785 }
786 },
787 }
788 }
789
790 self.guard.responses_rx = Some(responses_rx);
792
793 }
796}
797
798#[cfg(test)]
799mod tests {
800 use std::future::Future;
801
802 use futures::FutureExt as _;
803 use saluki_metrics::test::TestRecorder;
804 use tokio::sync::oneshot;
805 use tokio_test::{
806 assert_pending, assert_ready,
807 task::{spawn, Spawn},
808 };
809
810 use super::*;
811
812 const COMPONENT_ID: &str = "test_component";
813
814 #[track_caller]
815 fn initialize_registry_with_component(
816 component_id: &str,
817 ) -> (
818 Health,
819 Spawn<impl Future<Output = ()>>,
820 Arc<Mutex<RegistryState>>,
821 Arc<RunnerState>,
822 ) {
823 let registry = HealthRegistry::new();
824 let registry_state = registry.state();
825
826 let handle = registry
828 .register_component(&SubsystemIdentifier::from_dotted(component_id))
829 .unwrap();
830
831 let runner = registry.into_runner().expect("should not fail to create runner");
835 let runner_state = runner.state();
836
837 let shutdown = std::future::pending();
839 let registry_task = spawn(runner.run(shutdown));
840
841 (handle, registry_task, registry_state, runner_state)
842 }
843
844 #[track_caller]
845 fn drive_until_quiesced<F: Future<Output = ()>>(task: &mut Spawn<F>) {
846 assert_pending!(task.poll());
847 while task.is_woken() {
848 assert_pending!(task.poll());
849 }
850 }
851
852 fn component_live(state: &Mutex<RegistryState>, component_id: &str) -> bool {
853 let state = state.lock().unwrap();
854 let target = SubsystemIdentifier::from_dotted(component_id);
855 state
856 .component_state
857 .iter()
858 .find(|state| state.name == target)
859 .map(|state| state.is_live())
860 .unwrap()
861 }
862
863 #[test]
864 fn basic_registration() {
865 let registry = HealthRegistry::new();
866 assert!(registry
867 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
868 .is_some());
869 }
870
871 #[test]
872 fn duplicate_component_registration_fails() {
873 let registry = HealthRegistry::new();
874
875 assert!(registry
877 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
878 .is_some());
879 assert!(registry
880 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
881 .is_none());
882 }
883
884 #[test]
885 fn duplicate_runner_creation_fails_while_running() {
886 let registry = HealthRegistry::new();
887 let registry2 = registry.clone();
888
889 let _runner = registry.into_runner().expect("first runner creation should succeed");
892
893 assert!(registry2.into_runner().is_err());
895 }
896
897 #[tokio::test]
898 async fn registry_can_be_respawned_after_shutdown() {
899 let registry = HealthRegistry::new();
900 let registry2 = registry.clone();
901 let registry3 = registry.clone();
902
903 let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
905 let runner = registry.into_runner().expect("first runner creation should succeed");
906
907 let join_handle = tokio::spawn(runner.run(shutdown_rx.map(|_| ())));
909
910 let _ = shutdown_tx.send(());
912
913 join_handle.await.expect("runner should complete without panic");
915
916 let _runner2 = registry2
918 .into_runner()
919 .expect("should be able to create runner after shutdown");
920
921 assert!(
923 registry3.into_runner().is_err(),
924 "should not be able to create runner while one exists"
925 );
926 }
927
928 #[test]
929 fn readiness() {
930 let registry = HealthRegistry::new();
931
932 let mut all_ready_fut = spawn(registry.all_ready());
934 assert_ready!(all_ready_fut.poll());
935
936 let mut handle = registry
938 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
939 .unwrap();
940
941 let mut all_ready_fut = spawn(registry.all_ready());
942 assert_pending!(all_ready_fut.poll());
943
944 handle.mark_ready();
946
947 assert!(all_ready_fut.is_woken());
948 assert_ready!(all_ready_fut.poll());
949
950 let mut all_ready_fut = spawn(registry.all_ready());
952 assert_ready!(all_ready_fut.poll());
953
954 handle.mark_not_ready();
956
957 let mut all_ready_fut = spawn(registry.all_ready());
958 assert_pending!(all_ready_fut.poll());
959 }
960
961 #[test]
962 fn readiness_telemetry_tracks_component_ready_gauge() {
963 let recorder = TestRecorder::default();
966 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
967
968 let registry = HealthRegistry::new();
970 let mut handle = registry
971 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
972 .unwrap();
973
974 let ready_gauge = || recorder.gauge((Telemetry::component_ready_name(), &[("component_id", COMPONENT_ID)]));
975
976 assert_eq!(ready_gauge(), Some(0.0));
978
979 handle.mark_ready();
980 assert_eq!(ready_gauge(), Some(1.0));
981
982 handle.mark_not_ready();
983 assert_eq!(ready_gauge(), Some(0.0));
984 }
985
986 #[tokio::test(start_paused = true)]
987 async fn liveness_telemetry_records_live_gauge_and_latency_histogram() {
988 let recorder = TestRecorder::default();
991 let _recorder_guard = metrics::set_default_local_recorder(&recorder);
992
993 let registry = HealthRegistry::new();
995 let registry_state = registry.state();
996 let mut handle = registry
997 .register_component(&SubsystemIdentifier::from_dotted(COMPONENT_ID))
998 .unwrap();
999 let runner = registry.into_runner().expect("should not fail to create runner");
1000 let mut registry_task = spawn(runner.run(std::future::pending::<()>()));
1001
1002 let live_gauge = || recorder.gauge((Telemetry::component_live_name(), &[("component_id", COMPONENT_ID)]));
1003 let latency_samples = || {
1004 recorder.histogram((
1005 Telemetry::component_liveness_latency_seconds_name(),
1006 &[("component_id", COMPONENT_ID)],
1007 ))
1008 };
1009
1010 let mut live_future = spawn(handle.live());
1012 assert_pending!(live_future.poll());
1013 drive_until_quiesced(&mut registry_task);
1014
1015 assert!(!component_live(®istry_state, COMPONENT_ID));
1017 assert_eq!(live_gauge(), Some(0.0));
1018 assert_eq!(latency_samples(), Some(Vec::new()));
1019
1020 assert!(live_future.is_woken());
1022 assert_ready!(live_future.poll());
1023 assert!(registry_task.is_woken());
1024 drive_until_quiesced(&mut registry_task);
1025
1026 assert!(component_live(®istry_state, COMPONENT_ID));
1027 assert_eq!(live_gauge(), Some(1.0));
1028
1029 let samples = latency_samples().expect("liveness latency histogram should be registered");
1031 assert_eq!(
1032 samples.len(),
1033 1,
1034 "handling one probe response must record exactly one liveness latency sample"
1035 );
1036 assert!(samples[0] >= 0.0, "recorded liveness latency must be non-negative");
1037 }
1038
1039 #[tokio::test(start_paused = true)]
1040 async fn component_responds_before_timeout() {
1041 let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
1043
1044 let mut live_future = spawn(handle.live());
1047 assert_pending!(live_future.poll());
1048 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1049 assert_eq!(runner_state.pending_scheduled_probes(), 0);
1050
1051 drive_until_quiesced(&mut registry);
1053 assert_eq!(runner_state.pending_probe_timeouts(), 1);
1054 assert_eq!(runner_state.pending_scheduled_probes(), 0);
1055
1056 assert!(!component_live(®istry_state, COMPONENT_ID));
1058
1059 assert!(live_future.is_woken());
1063 assert_ready!(live_future.poll());
1064
1065 assert!(registry.is_woken());
1069 drive_until_quiesced(&mut registry);
1070
1071 assert!(component_live(®istry_state, COMPONENT_ID));
1072
1073 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1076 assert_eq!(runner_state.pending_scheduled_probes(), 1);
1077 }
1078
1079 #[tokio::test(start_paused = true)]
1080 async fn component_responds_after_timeout() {
1081 let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
1083
1084 let mut live_future = spawn(handle.live());
1087 assert_pending!(live_future.poll());
1088 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1089 assert_eq!(runner_state.pending_scheduled_probes(), 0);
1090
1091 drive_until_quiesced(&mut registry);
1093 assert_eq!(runner_state.pending_probe_timeouts(), 1);
1094 assert_eq!(runner_state.pending_scheduled_probes(), 0);
1095
1096 assert!(!component_live(®istry_state, COMPONENT_ID));
1098
1099 assert!(live_future.is_woken());
1103 assert!(!registry.is_woken());
1104
1105 tokio::time::advance(DEFAULT_PROBE_TIMEOUT_DUR + Duration::from_secs(1)).await;
1106
1107 assert!(registry.is_woken());
1111 drive_until_quiesced(&mut registry);
1112
1113 assert!(!component_live(®istry_state, COMPONENT_ID));
1114
1115 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1118 assert_eq!(runner_state.pending_scheduled_probes(), 1);
1119
1120 assert_ready!(live_future.poll());
1122
1123 assert!(registry.is_woken());
1124 drive_until_quiesced(&mut registry);
1125
1126 assert!(component_live(®istry_state, COMPONENT_ID));
1127
1128 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1131 assert_eq!(runner_state.pending_scheduled_probes(), 1);
1132 }
1133
1134 #[track_caller]
1135 #[allow(clippy::type_complexity)]
1136 fn initialize_registry_with_component_and_shutdown(
1137 component_id: &str,
1138 ) -> (
1139 Health,
1140 Spawn<impl Future<Output = ()>>,
1141 Arc<Mutex<RegistryState>>,
1142 Arc<RunnerState>,
1143 oneshot::Sender<()>,
1144 ) {
1145 let registry = HealthRegistry::new();
1146 let registry_state = registry.state();
1147 let handle = registry
1148 .register_component(&SubsystemIdentifier::from_dotted(component_id))
1149 .unwrap();
1150 let runner = registry.into_runner().expect("should not fail to create runner");
1151 let runner_state = runner.state();
1152
1153 let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
1154 let registry_task = spawn(runner.run(shutdown_rx.map(|_| ())));
1155
1156 (handle, registry_task, registry_state, runner_state, shutdown_tx)
1157 }
1158
1159 #[tokio::test(start_paused = true)]
1160 async fn respawn_preserves_fresh_component_health() {
1161 let (mut handle, mut registry, registry_state, _runner_state, shutdown_tx) =
1163 initialize_registry_with_component_and_shutdown(COMPONENT_ID);
1164 drive_until_quiesced(&mut registry);
1165
1166 let mut live_future = spawn(handle.live());
1168 assert_ready!(live_future.poll());
1169 drive_until_quiesced(&mut registry);
1170 assert!(component_live(®istry_state, COMPONENT_ID));
1171
1172 let _ = shutdown_tx.send(());
1174 assert_ready!(registry.poll());
1175
1176 let registry = HealthRegistry {
1178 inner: Arc::clone(®istry_state),
1179 };
1180 let runner = registry
1181 .into_runner()
1182 .expect("should be able to respawn after shutdown");
1183 let _runner_state = runner.state();
1184 let mut registry = spawn(runner.run(std::future::pending()));
1185 drive_until_quiesced(&mut registry);
1186
1187 assert!(component_live(®istry_state, COMPONENT_ID));
1189 }
1190
1191 #[tokio::test(start_paused = true)]
1192 async fn respawn_resets_stale_component_health() {
1193 let (mut handle, mut registry, registry_state, _runner_state, shutdown_tx) =
1195 initialize_registry_with_component_and_shutdown(COMPONENT_ID);
1196 drive_until_quiesced(&mut registry);
1197
1198 let mut live_future = spawn(handle.live());
1200 assert_ready!(live_future.poll());
1201 drive_until_quiesced(&mut registry);
1202 assert!(component_live(®istry_state, COMPONENT_ID));
1203
1204 let _ = shutdown_tx.send(());
1206 assert_ready!(registry.poll());
1207
1208 tokio::time::advance(DEFAULT_PROBE_TIMEOUT_DUR + Duration::from_secs(1)).await;
1210
1211 let registry = HealthRegistry {
1213 inner: Arc::clone(®istry_state),
1214 };
1215 let runner = registry
1216 .into_runner()
1217 .expect("should be able to respawn after shutdown");
1218 let _runner_state = runner.state();
1219 let mut registry = spawn(runner.run(std::future::pending()));
1220 drive_until_quiesced(&mut registry);
1221
1222 assert!(!component_live(®istry_state, COMPONENT_ID));
1224 }
1225}