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;
18use stringtheory::MetaString;
19use tokio::{pin, time::Instant};
20use tokio::{
21 select,
22 sync::{
23 mpsc::{self, error::TrySendError},
24 Notify,
25 },
26};
27use tokio_util::time::{delay_queue::Key, DelayQueue};
28use tracing::{debug, info, trace};
29
30mod api;
31pub use self::api::HealthAPIHandler;
32
33mod worker;
34pub use self::worker::HealthRegistryWorker;
35
36const DEFAULT_PROBE_TIMEOUT_DUR: Duration = Duration::from_secs(5);
37const DEFAULT_PROBE_BACKOFF_DUR: Duration = Duration::from_secs(1);
38
39pub struct Health {
41 shared: Arc<SharedComponentState>,
42 request_rx: mpsc::Receiver<LivenessRequest>,
43 response_tx: mpsc::Sender<LivenessResponse>,
44 readiness_notify: Arc<Notify>,
45}
46
47impl Health {
48 pub fn mark_ready(&mut self) {
50 self.update_readiness(true);
51 }
52
53 pub fn mark_not_ready(&mut self) {
55 self.update_readiness(false);
56 }
57
58 fn update_readiness(&self, ready: bool) {
59 self.shared.ready.store(ready, Relaxed);
60 self.shared.telemetry.update_readiness(ready);
61
62 if ready {
64 self.readiness_notify.notify_waiters();
65 }
66 }
67
68 pub async fn live(&mut self) {
73 if let Some(request) = self.request_rx.recv().await {
76 let response = request.into_response();
77 let _ = self.response_tx.send(response).await;
78 }
79 }
80}
81
82#[derive(Clone, Copy, Eq, PartialEq)]
83enum HealthState {
84 Live,
85 Unknown,
86 Dead,
87}
88
89static_metrics!(
90 name => Telemetry,
91 prefix => health,
92 labels => [component_id: Arc<str>],
93 metrics => [
94 gauge(component_ready),
95 gauge(component_live),
96 trace_histogram(component_liveness_latency_seconds),
97 ]
98);
99
100impl Telemetry {
101 fn from_name(name: &str) -> Self {
102 Self::new(Arc::from(name))
103 }
104
105 fn update_readiness(&self, ready: bool) {
106 self.component_ready().set(if ready { 1.0 } else { 0.0 });
107 }
108
109 fn update_liveness(&self, state: HealthState, response_latency: Duration) {
110 let live = match state {
111 HealthState::Live => 1.0,
112 HealthState::Unknown => 0.0,
113 HealthState::Dead => -1.0,
114 };
115
116 self.component_live().set(live);
117 self.component_liveness_latency_seconds()
118 .record(response_latency.as_secs_f64());
119 }
120}
121
122struct SharedComponentState {
123 ready: AtomicBool,
124 telemetry: Telemetry,
125}
126
127struct ComponentState {
128 name: MetaString,
129 health: HealthState,
130 shared: Arc<SharedComponentState>,
131 request_tx: mpsc::Sender<LivenessRequest>,
132 last_response: Instant,
133 last_response_latency: Duration,
134}
135
136impl ComponentState {
137 fn new(
138 name: MetaString, response_tx: mpsc::Sender<LivenessResponse>, readiness_notify: Arc<Notify>,
139 ) -> (Self, Health) {
140 let shared = Arc::new(SharedComponentState {
141 ready: AtomicBool::new(false),
142 telemetry: Telemetry::from_name(&name),
143 });
144 let (request_tx, request_rx) = mpsc::channel(1);
145
146 let state = Self {
147 name,
148 health: HealthState::Unknown,
149 shared: Arc::clone(&shared),
150 request_tx,
151 last_response: Instant::now(),
152 last_response_latency: Duration::from_secs(0),
153 };
154
155 let handle = Health {
156 shared,
157 request_rx,
158 response_tx,
159 readiness_notify,
160 };
161
162 (state, handle)
163 }
164
165 fn is_ready(&self) -> bool {
166 self.shared.ready.load(Relaxed) && self.health != HealthState::Dead
171 }
172
173 fn is_live(&self) -> bool {
174 self.health == HealthState::Live
175 }
176
177 fn mark_live(&mut self, response_sent: Instant, response_latency: Duration) {
178 self.health = HealthState::Live;
179 self.last_response = response_sent;
180 self.last_response_latency = response_latency;
181 self.shared.telemetry.update_liveness(self.health, response_latency);
182 }
183
184 fn mark_not_live(&mut self) {
185 self.health = HealthState::Unknown;
186
187 self.shared
189 .telemetry
190 .update_liveness(self.health, DEFAULT_PROBE_TIMEOUT_DUR);
191 }
192
193 fn mark_dead(&mut self) {
194 self.health = HealthState::Dead;
195
196 self.shared
198 .telemetry
199 .update_liveness(self.health, DEFAULT_PROBE_TIMEOUT_DUR);
200 }
201}
202
203struct LivenessRequest {
204 component_id: usize,
205 timeout_key: Key,
206 request_sent: Instant,
207}
208
209impl LivenessRequest {
210 fn new(component_id: usize, timeout_key: Key) -> Self {
211 Self {
212 component_id,
213 timeout_key,
214 request_sent: Instant::now(),
215 }
216 }
217
218 fn into_response(self) -> LivenessResponse {
219 LivenessResponse {
220 request: self,
221 response_sent: Instant::now(),
222 }
223 }
224}
225
226struct LivenessResponse {
227 request: LivenessRequest,
228 response_sent: Instant,
229}
230
231enum HealthUpdate {
232 Alive {
233 last_response: Instant,
234 last_response_latency: Duration,
235 },
236 Unknown,
237 Dead,
238}
239
240impl HealthUpdate {
241 fn as_str(&self) -> &'static str {
242 match self {
243 HealthUpdate::Alive { .. } => "alive",
244 HealthUpdate::Unknown => "unknown",
245 HealthUpdate::Dead => "dead",
246 }
247 }
248}
249
250struct RegistryState {
251 registered_components: HashSet<MetaString>,
252 component_state: Vec<ComponentState>,
253 responses_tx: mpsc::Sender<LivenessResponse>,
254 responses_rx: Option<mpsc::Receiver<LivenessResponse>>,
255 pending_components: Vec<usize>,
256 pending_components_notify: Arc<Notify>,
257 readiness_notify: Arc<Notify>,
258}
259
260impl RegistryState {
261 fn new() -> Self {
262 let (responses_tx, responses_rx) = mpsc::channel(16);
263
264 Self {
265 registered_components: HashSet::new(),
266 component_state: Vec::new(),
267 responses_tx,
268 responses_rx: Some(responses_rx),
269 pending_components: Vec::new(),
270 pending_components_notify: Arc::new(Notify::new()),
271 readiness_notify: Arc::new(Notify::new()),
272 }
273 }
274}
275
276#[derive(Clone)]
294pub struct HealthRegistry {
295 inner: Arc<Mutex<RegistryState>>,
296}
297
298impl HealthRegistry {
299 pub fn new() -> Self {
301 Self {
302 inner: Arc::new(Mutex::new(RegistryState::new())),
303 }
304 }
305
306 #[cfg(test)]
307 fn state(&self) -> Arc<Mutex<RegistryState>> {
308 Arc::clone(&self.inner)
309 }
310
311 pub fn register_component<S: Into<MetaString>>(&self, name: S) -> Option<Health> {
316 let mut inner = self.inner.lock().unwrap();
317
318 let name = name.into();
320 if !inner.registered_components.insert(name.clone()) {
321 return None;
322 }
323
324 let readiness_notify = Arc::clone(&inner.readiness_notify);
326 let (state, handle) = ComponentState::new(name.clone(), inner.responses_tx.clone(), readiness_notify);
327 let component_id = inner.component_state.len();
328 inner.component_state.push(state);
329
330 debug!(component_id, "Registered component '{}'.", name);
331
332 inner.pending_components.push(component_id);
334 inner.pending_components_notify.notify_one();
335
336 Some(handle)
337 }
338
339 pub fn api_handler(&self) -> HealthAPIHandler {
344 HealthAPIHandler::from_state(Arc::clone(&self.inner))
345 }
346
347 pub async fn all_ready(&self) {
355 self.all_ready_matching(|_| true).await
356 }
357
358 pub async fn all_ready_matching<F>(&self, predicate: F)
368 where
369 F: Fn(&str) -> bool,
370 {
371 let readiness_notify = {
372 let inner = self.inner.lock().unwrap();
373 Arc::clone(&inner.readiness_notify)
374 };
375
376 loop {
377 let notified = readiness_notify.notified();
379
380 if self.check_ready_matching(&predicate) {
381 return;
382 }
383
384 notified.await;
385 }
386 }
387
388 fn check_ready_matching<F>(&self, predicate: &F) -> bool
389 where
390 F: Fn(&str) -> bool,
391 {
392 let inner = self.inner.lock().unwrap();
393 inner
394 .component_state
395 .iter()
396 .filter(|component| predicate(&component.name))
397 .all(|component| component.is_ready())
398 }
399
400 pub fn snapshot_json(&self) -> String {
407 #[derive(serde::Serialize)]
408 struct ComponentSnapshot {
409 live: bool,
410 ready: bool,
411 }
412
413 let inner = self.inner.lock().unwrap();
414 let mut state: std::collections::HashMap<String, ComponentSnapshot> = std::collections::HashMap::new();
415 for component in &inner.component_state {
416 state.insert(
417 component.name.to_string(),
418 ComponentSnapshot {
419 live: component.is_live(),
420 ready: component.is_ready(),
421 },
422 );
423 }
424 serde_json::to_string_pretty(&state).unwrap_or_else(|e| format!("{{\"error\": \"{e}\"}}"))
425 }
426
427 pub fn worker(&self) -> HealthRegistryWorker {
432 HealthRegistryWorker::new(self.clone())
433 }
434
435 pub(crate) fn into_runner(self) -> Result<Runner, GenericError> {
436 let (responses_rx, pending_components_notify) = {
438 let mut inner = self.inner.lock().unwrap();
439 let responses_rx = match inner.responses_rx.take() {
440 Some(rx) => rx,
441 None => return Err(generic_error!("health registry already spawned")),
442 };
443
444 let pending_components_notify = Arc::clone(&inner.pending_components_notify);
445 (responses_rx, pending_components_notify)
446 };
447
448 Ok(Runner::new(self.inner, responses_rx, pending_components_notify))
449 }
450}
451
452struct RunnerGuard {
458 registry: Arc<Mutex<RegistryState>>,
459 responses_rx: Option<mpsc::Receiver<LivenessResponse>>,
460}
461
462impl Drop for RunnerGuard {
463 fn drop(&mut self) {
464 if let Some(rx) = self.responses_rx.take() {
465 let mut inner = self.registry.lock().expect("registry state poisoned");
466 inner.responses_rx = Some(rx);
467 debug!("Returned response receiver to registry state.");
468 }
469 }
470}
471
472#[cfg(test)]
473struct RunnerState {
474 pending_scheduled_probes: AtomicUsize,
475 pending_probe_timeouts: AtomicUsize,
476}
477
478#[cfg(test)]
479impl RunnerState {
480 fn new() -> Self {
481 Self {
482 pending_scheduled_probes: AtomicUsize::new(0),
483 pending_probe_timeouts: AtomicUsize::new(0),
484 }
485 }
486
487 fn pending_scheduled_probes(&self) -> usize {
488 self.pending_scheduled_probes.load(Relaxed)
489 }
490
491 fn pending_probe_timeouts(&self) -> usize {
492 self.pending_probe_timeouts.load(Relaxed)
493 }
494
495 fn increment_pending_scheduled_probes(&self) {
496 self.pending_scheduled_probes.fetch_add(1, Relaxed);
497 }
498
499 fn increment_pending_probe_timeouts(&self) {
500 self.pending_probe_timeouts.fetch_add(1, Relaxed);
501 }
502
503 fn decrement_pending_scheduled_probes(&self) {
504 self.pending_scheduled_probes.fetch_sub(1, Relaxed);
505 }
506
507 fn decrement_pending_probe_timeouts(&self) {
508 self.pending_probe_timeouts.fetch_sub(1, Relaxed);
509 }
510}
511
512pub(super) struct Runner {
513 registry: Arc<Mutex<RegistryState>>,
514 pending_probes: DelayQueue<usize>,
515 pending_timeouts: DelayQueue<usize>,
516 guard: RunnerGuard,
517 pending_components_notify: Arc<Notify>,
518 #[cfg(test)]
519 state: Arc<RunnerState>,
520}
521
522impl Runner {
523 fn new(
524 registry: Arc<Mutex<RegistryState>>, responses_rx: mpsc::Receiver<LivenessResponse>,
525 pending_components_notify: Arc<Notify>,
526 ) -> Self {
527 #[cfg(test)]
528 let state = Arc::new(RunnerState::new());
529
530 let guard = RunnerGuard {
531 registry: Arc::clone(®istry),
532 responses_rx: Some(responses_rx),
533 };
534
535 Self {
536 registry,
537 pending_probes: DelayQueue::new(),
538 pending_timeouts: DelayQueue::new(),
539 guard,
540 pending_components_notify,
541 #[cfg(test)]
542 state,
543 }
544 }
545
546 #[cfg(test)]
547 fn state(&self) -> Arc<RunnerState> {
548 Arc::clone(&self.state)
549 }
550
551 fn drain_pending_components(&mut self) -> Vec<usize> {
552 let mut registry = self.registry.lock().unwrap();
554 registry.pending_components.drain(..).collect()
555 }
556
557 fn send_component_probe_request(&mut self, component_id: usize) -> Option<HealthUpdate> {
558 let mut registry = self.registry.lock().unwrap();
559 let component_state = &mut registry.component_state[component_id];
560
561 if component_state.request_tx.is_closed() {
563 debug!(component_name = %component_state.name, "Component is dead, skipping liveness probe.");
564 return Some(HealthUpdate::Dead);
565 }
566
567 trace!(component_name = %component_state.name, probe_timeout = ?DEFAULT_PROBE_TIMEOUT_DUR, "Sending liveness probe to component.");
568
569 let timeout_key = self.pending_timeouts.insert(component_id, DEFAULT_PROBE_TIMEOUT_DUR);
574
575 #[cfg(test)]
576 self.state.increment_pending_probe_timeouts();
577
578 let request = LivenessRequest::new(component_id, timeout_key);
579 if let Err(TrySendError::Closed(request)) = component_state.request_tx.try_send(request) {
580 debug!(component_name = %component_state.name, "Component is dead, removing pending timeout.");
581
582 self.pending_timeouts.remove(&request.timeout_key);
588
589 #[cfg(test)]
590 self.state.decrement_pending_probe_timeouts();
591
592 return Some(HealthUpdate::Dead);
593 }
594
595 None
596 }
597
598 fn schedule_probe_for_component(&mut self, component_id: usize, duration: Duration) {
599 #[cfg(test)]
600 self.state.increment_pending_scheduled_probes();
601
602 self.pending_probes.insert(component_id, duration);
603 }
604
605 fn schedule_all_existing_components(&mut self, responses_rx: &mut mpsc::Receiver<LivenessResponse>) {
606 let _pending = self.drain_pending_components();
609
610 while let Ok(response) = responses_rx.try_recv() {
615 self.handle_component_probe_response(response);
616 }
617
618 let (component_count, stale_component_ids) = {
622 let registry = self.registry.lock().unwrap();
623 let now = Instant::now();
624 let stale_ids: Vec<usize> = (0..registry.component_state.len())
625 .filter(|&id| {
626 now.duration_since(registry.component_state[id].last_response) >= DEFAULT_PROBE_TIMEOUT_DUR
627 })
628 .collect();
629 (registry.component_state.len(), stale_ids)
630 };
631
632 for &component_id in &stale_component_ids {
634 self.process_component_health_update(component_id, HealthUpdate::Unknown);
635 }
636
637 for component_id in 0..component_count {
639 self.schedule_probe_for_component(component_id, Duration::ZERO);
640 }
641
642 if component_count > 0 {
643 let fresh_count = component_count - stale_component_ids.len();
644 debug!(
645 component_count,
646 fresh_count,
647 stale_count = stale_component_ids.len(),
648 "Scheduled probes for all existing components."
649 );
650 }
651 }
652
653 fn handle_component_probe_response(&mut self, response: LivenessResponse) {
654 let component_id = response.request.component_id;
655 let timeout_key = response.request.timeout_key;
656 let request_sent = response.request.request_sent;
657 let response_sent = response.response_sent;
658 let response_latency = response_sent.checked_duration_since(request_sent).unwrap_or_default();
659
660 let timeout_was_pending = self.pending_timeouts.try_remove(&timeout_key).is_some();
662 if !timeout_was_pending {
663 let mut registry = self.registry.lock().unwrap();
664 let component_state = &mut registry.component_state[component_id];
665
666 debug!(component_name = %component_state.name, "Received probe response for component that already timed out.");
667 }
668
669 let update = HealthUpdate::Alive {
671 last_response: response_sent,
672 last_response_latency: response_latency,
673 };
674 self.process_component_health_update(component_id, update);
675
676 if timeout_was_pending {
679 #[cfg(test)]
680 self.state.decrement_pending_probe_timeouts();
681
682 self.schedule_probe_for_component(component_id, DEFAULT_PROBE_BACKOFF_DUR);
683 }
684 }
685
686 fn handle_component_timeout(&mut self, component_id: usize) {
687 self.process_component_health_update(component_id, HealthUpdate::Unknown);
689
690 self.schedule_probe_for_component(component_id, DEFAULT_PROBE_BACKOFF_DUR);
692 }
693
694 fn process_component_health_update(&mut self, component_id: usize, update: HealthUpdate) {
695 let mut registry = self.registry.lock().unwrap();
697 let component_state = &mut registry.component_state[component_id];
698 trace!(component_name = %component_state.name, status = update.as_str(), "Updating component health status.");
699
700 match update {
701 HealthUpdate::Alive {
702 last_response,
703 last_response_latency,
704 } => component_state.mark_live(last_response, last_response_latency),
705 HealthUpdate::Unknown => component_state.mark_not_live(),
706 HealthUpdate::Dead => component_state.mark_dead(),
707 }
708 }
709
710 async fn run<F: Future<Output = ()>>(mut self, shutdown: F) {
711 info!("Health checker running.");
712
713 let mut responses_rx = self
716 .guard
717 .responses_rx
718 .take()
719 .expect("responses_rx should always be Some when Runner is created");
720
721 self.schedule_all_existing_components(&mut responses_rx);
725
726 pin!(shutdown);
728
729 loop {
730 select! {
731 _ = &mut shutdown => {
733 info!("Health checker shutting down.");
734 break;
735 },
736
737 Some(entry) = self.pending_probes.next() => {
739 #[cfg(test)]
740 self.state.decrement_pending_scheduled_probes();
741
742 let component_id = entry.into_inner();
743 if let Some(health_update) = self.send_component_probe_request(component_id) {
744 self.process_component_health_update(component_id, health_update);
747 }
748 },
749
750 Some(entry) = self.pending_timeouts.next() => {
752 #[cfg(test)]
753 self.state.decrement_pending_probe_timeouts();
754
755 let component_id = entry.into_inner();
756 self.handle_component_timeout(component_id);
757 },
758
759 Some(response) = responses_rx.recv() => {
761 self.handle_component_probe_response(response);
762 },
763
764 _ = self.pending_components_notify.notified() => {
766 let pending_component_ids = self.drain_pending_components();
768 for pending_component_id in pending_component_ids {
769 self.process_component_health_update(pending_component_id, HealthUpdate::Unknown);
770 self.schedule_probe_for_component(pending_component_id, Duration::ZERO);
771 }
772 },
773 }
774 }
775
776 self.guard.responses_rx = Some(responses_rx);
778
779 }
782}
783
784#[cfg(test)]
785mod tests {
786 use std::future::Future;
787
788 use futures::FutureExt as _;
789 use tokio::sync::oneshot;
790 use tokio_test::{
791 assert_pending, assert_ready,
792 task::{spawn, Spawn},
793 };
794
795 use super::*;
796
797 const COMPONENT_ID: &str = "test_component";
798
799 #[track_caller]
800 fn initialize_registry_with_component(
801 component_id: &str,
802 ) -> (
803 Health,
804 Spawn<impl Future<Output = ()>>,
805 Arc<Mutex<RegistryState>>,
806 Arc<RunnerState>,
807 ) {
808 let registry = HealthRegistry::new();
809 let registry_state = registry.state();
810
811 let handle = registry.register_component(component_id).unwrap();
813
814 let runner = registry.into_runner().expect("should not fail to create runner");
818 let runner_state = runner.state();
819
820 let shutdown = std::future::pending();
822 let registry_task = spawn(runner.run(shutdown));
823
824 (handle, registry_task, registry_state, runner_state)
825 }
826
827 #[track_caller]
828 fn drive_until_quiesced<F: Future<Output = ()>>(task: &mut Spawn<F>) {
829 assert_pending!(task.poll());
830 while task.is_woken() {
831 assert_pending!(task.poll());
832 }
833 }
834
835 fn component_live(state: &Mutex<RegistryState>, component_id: &str) -> bool {
836 let state = state.lock().unwrap();
837 state
838 .component_state
839 .iter()
840 .find(|state| state.name == component_id)
841 .map(|state| state.is_live())
842 .unwrap()
843 }
844
845 #[test]
846 fn basic_registration() {
847 let registry = HealthRegistry::new();
848 assert!(registry.register_component(COMPONENT_ID).is_some());
849 }
850
851 #[test]
852 fn duplicate_component_registration_fails() {
853 let registry = HealthRegistry::new();
854
855 assert!(registry.register_component(COMPONENT_ID).is_some());
857 assert!(registry.register_component(COMPONENT_ID).is_none());
858 }
859
860 #[test]
861 fn duplicate_runner_creation_fails_while_running() {
862 let registry = HealthRegistry::new();
863 let registry2 = registry.clone();
864
865 let _runner = registry.into_runner().expect("first runner creation should succeed");
868
869 assert!(registry2.into_runner().is_err());
871 }
872
873 #[tokio::test]
874 async fn registry_can_be_respawned_after_shutdown() {
875 let registry = HealthRegistry::new();
876 let registry2 = registry.clone();
877 let registry3 = registry.clone();
878
879 let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
881 let runner = registry.into_runner().expect("first runner creation should succeed");
882
883 let join_handle = tokio::spawn(runner.run(shutdown_rx.map(|_| ())));
885
886 let _ = shutdown_tx.send(());
888
889 join_handle.await.expect("runner should complete without panic");
891
892 let _runner2 = registry2
894 .into_runner()
895 .expect("should be able to create runner after shutdown");
896
897 assert!(
899 registry3.into_runner().is_err(),
900 "should not be able to create runner while one exists"
901 );
902 }
903
904 #[test]
905 fn readiness() {
906 let registry = HealthRegistry::new();
907
908 let mut all_ready_fut = spawn(registry.all_ready());
910 assert_ready!(all_ready_fut.poll());
911
912 let mut handle = registry.register_component(COMPONENT_ID).unwrap();
914
915 let mut all_ready_fut = spawn(registry.all_ready());
916 assert_pending!(all_ready_fut.poll());
917
918 handle.mark_ready();
920
921 assert!(all_ready_fut.is_woken());
922 assert_ready!(all_ready_fut.poll());
923
924 let mut all_ready_fut = spawn(registry.all_ready());
926 assert_ready!(all_ready_fut.poll());
927
928 handle.mark_not_ready();
930
931 let mut all_ready_fut = spawn(registry.all_ready());
932 assert_pending!(all_ready_fut.poll());
933 }
934
935 #[tokio::test(start_paused = true)]
936 async fn component_responds_before_timeout() {
937 let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
939
940 let mut live_future = spawn(handle.live());
943 assert_pending!(live_future.poll());
944 assert_eq!(runner_state.pending_probe_timeouts(), 0);
945 assert_eq!(runner_state.pending_scheduled_probes(), 0);
946
947 drive_until_quiesced(&mut registry);
949 assert_eq!(runner_state.pending_probe_timeouts(), 1);
950 assert_eq!(runner_state.pending_scheduled_probes(), 0);
951
952 assert!(!component_live(®istry_state, COMPONENT_ID));
954
955 assert!(live_future.is_woken());
959 assert_ready!(live_future.poll());
960
961 assert!(registry.is_woken());
965 drive_until_quiesced(&mut registry);
966
967 assert!(component_live(®istry_state, COMPONENT_ID));
968
969 assert_eq!(runner_state.pending_probe_timeouts(), 0);
972 assert_eq!(runner_state.pending_scheduled_probes(), 1);
973 }
974
975 #[tokio::test(start_paused = true)]
976 async fn component_responds_after_timeout() {
977 let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
979
980 let mut live_future = spawn(handle.live());
983 assert_pending!(live_future.poll());
984 assert_eq!(runner_state.pending_probe_timeouts(), 0);
985 assert_eq!(runner_state.pending_scheduled_probes(), 0);
986
987 drive_until_quiesced(&mut registry);
989 assert_eq!(runner_state.pending_probe_timeouts(), 1);
990 assert_eq!(runner_state.pending_scheduled_probes(), 0);
991
992 assert!(!component_live(®istry_state, COMPONENT_ID));
994
995 assert!(live_future.is_woken());
999 assert!(!registry.is_woken());
1000
1001 tokio::time::advance(DEFAULT_PROBE_TIMEOUT_DUR + Duration::from_secs(1)).await;
1002
1003 assert!(registry.is_woken());
1007 drive_until_quiesced(&mut registry);
1008
1009 assert!(!component_live(®istry_state, COMPONENT_ID));
1010
1011 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1014 assert_eq!(runner_state.pending_scheduled_probes(), 1);
1015
1016 assert_ready!(live_future.poll());
1018
1019 assert!(registry.is_woken());
1020 drive_until_quiesced(&mut registry);
1021
1022 assert!(component_live(®istry_state, COMPONENT_ID));
1023
1024 assert_eq!(runner_state.pending_probe_timeouts(), 0);
1027 assert_eq!(runner_state.pending_scheduled_probes(), 1);
1028 }
1029
1030 #[track_caller]
1031 #[allow(clippy::type_complexity)]
1032 fn initialize_registry_with_component_and_shutdown(
1033 component_id: &str,
1034 ) -> (
1035 Health,
1036 Spawn<impl Future<Output = ()>>,
1037 Arc<Mutex<RegistryState>>,
1038 Arc<RunnerState>,
1039 oneshot::Sender<()>,
1040 ) {
1041 let registry = HealthRegistry::new();
1042 let registry_state = registry.state();
1043 let handle = registry.register_component(component_id).unwrap();
1044 let runner = registry.into_runner().expect("should not fail to create runner");
1045 let runner_state = runner.state();
1046
1047 let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
1048 let registry_task = spawn(runner.run(shutdown_rx.map(|_| ())));
1049
1050 (handle, registry_task, registry_state, runner_state, shutdown_tx)
1051 }
1052
1053 #[tokio::test(start_paused = true)]
1054 async fn respawn_preserves_fresh_component_health() {
1055 let (mut handle, mut registry, registry_state, _runner_state, shutdown_tx) =
1057 initialize_registry_with_component_and_shutdown(COMPONENT_ID);
1058 drive_until_quiesced(&mut registry);
1059
1060 let mut live_future = spawn(handle.live());
1062 assert_ready!(live_future.poll());
1063 drive_until_quiesced(&mut registry);
1064 assert!(component_live(®istry_state, COMPONENT_ID));
1065
1066 let _ = shutdown_tx.send(());
1068 assert_ready!(registry.poll());
1069
1070 let registry = HealthRegistry {
1072 inner: Arc::clone(®istry_state),
1073 };
1074 let runner = registry
1075 .into_runner()
1076 .expect("should be able to respawn after shutdown");
1077 let _runner_state = runner.state();
1078 let mut registry = spawn(runner.run(std::future::pending()));
1079 drive_until_quiesced(&mut registry);
1080
1081 assert!(component_live(®istry_state, COMPONENT_ID));
1083 }
1084
1085 #[tokio::test(start_paused = true)]
1086 async fn respawn_resets_stale_component_health() {
1087 let (mut handle, mut registry, registry_state, _runner_state, shutdown_tx) =
1089 initialize_registry_with_component_and_shutdown(COMPONENT_ID);
1090 drive_until_quiesced(&mut registry);
1091
1092 let mut live_future = spawn(handle.live());
1094 assert_ready!(live_future.poll());
1095 drive_until_quiesced(&mut registry);
1096 assert!(component_live(®istry_state, COMPONENT_ID));
1097
1098 let _ = shutdown_tx.send(());
1100 assert_ready!(registry.poll());
1101
1102 tokio::time::advance(DEFAULT_PROBE_TIMEOUT_DUR + Duration::from_secs(1)).await;
1104
1105 let registry = HealthRegistry {
1107 inner: Arc::clone(®istry_state),
1108 };
1109 let runner = registry
1110 .into_runner()
1111 .expect("should be able to respawn after shutdown");
1112 let _runner_state = runner.state();
1113 let mut registry = spawn(runner.run(std::future::pending()));
1114 drive_until_quiesced(&mut registry);
1115
1116 assert!(!component_live(®istry_state, COMPONENT_ID));
1118 }
1119}