saluki_core/health/
mod.rs

1//! Health registry for tracking component readiness and liveness.
2
3use 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
40/// A handle for updating the health of a component.
41pub 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    /// Marks the component as ready.
50    pub fn mark_ready(&mut self) {
51        self.update_readiness(true);
52    }
53
54    /// Marks the component as not ready.
55    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        // Wake any tasks waiting in `HealthRegistry::all_ready` so they can re-check whether all components are ready.
64        if ready {
65            self.readiness_notify.notify_waiters();
66        }
67    }
68
69    /// Waits for a liveness probe to be sent to the component, and then responds to it.
70    ///
71    /// This should generally be polled as part of a `select!` block to ensure it's checked alongside other
72    /// asynchronous operations.
73    pub async fn live(&mut self) {
74        // Simply wait for the health registry to send us a liveness probe, and if we receive one, we respond back to it
75        // immediately.
76        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        // We consider a component ready if it's marked as ready (duh) and it's not dead.
166        //
167        // Being "dead" is a special case as it means the component is very likely not even running at all, not just
168        // responding slowly or deadlocked. In these cases, it can't possibly be ready since it's not even running.
169        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        // We use the default timeout as the latency for when the component is not considered alive.
187        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        // We use the default timeout as the latency for when the component is not considered alive.
196        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/// A registry of components and their health.
276///
277/// `HealthRegistry` is responsible for tracking the health of all registered components, by storing both their
278/// readiness, which indicates whether or not they're initialized and generally ready to process data, as well as
279/// probing their liveness, which indicates if they're currently responding, or able to respond, to requests.
280///
281/// # Telemetry
282///
283/// The health registry emits some internal telemetry about the status of registered components. In particular, three
284/// metrics are emitted:
285///
286/// - `health_component_ready`: whether or not a component is ready (`gauge`, `0` for not ready, `1` for ready)
287/// - `health_component_live`: whether or not a component is alive (`gauge`, `0` for not alive/unknown, `1` for alive, `-1` for dead)
288/// - `health_component_liveness_latency_seconds`: the response latency of the component for liveness probes (`histogram`,
289///   in seconds)
290///
291/// All metrics have a `component_id` tag that corresponds to the name of the component that was given when registering it.
292#[derive(Clone)]
293pub struct HealthRegistry {
294    inner: Arc<Mutex<RegistryState>>,
295}
296
297impl HealthRegistry {
298    /// Creates an empty registry.
299    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    /// Registers a component with the registry, keyed by its canonical [`SubsystemIdentifier`].
311    ///
312    /// Returns `None` if a component with the same identifier is already registered. Otherwise, a handle is returned
313    /// that must be used by the component to set its readiness as well as respond to liveness probes. See
314    /// [`Health::mark_ready`], [`Health::mark_not_ready`], and [`Health::live`] for more information.
315    pub fn register_component(&self, id: &SubsystemIdentifier) -> Option<Health> {
316        let mut inner = self.inner.lock().unwrap();
317
318        // Make sure we don't already have this component registered.
319        if !inner.registered_components.insert(id.clone()) {
320            return None;
321        }
322
323        // Add the component state.
324        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        // Mark ourselves as having a pending component that needs to be scheduled.
332        inner.pending_components.push(component_id);
333        inner.pending_components_notify.notify_one();
334
335        Some(handle)
336    }
337
338    /// Gets an API handler for reporting the health of all components.
339    ///
340    /// This handler exposes routes for querying the readiness and liveness of all registered components. See
341    /// [`HealthAPIHandler`] for more information about routes and responses.
342    pub fn api_handler(&self) -> HealthAPIHandler {
343        HealthAPIHandler::from_state(Arc::clone(&self.inner))
344    }
345
346    /// Waits until all registered components are ready.
347    ///
348    /// If no components are registered, or all currently registered components are ready, the method returns immediately. Otherwise,
349    /// the method will return as soon as all registered components transition to ready.
350    ///
351    /// Note that components can be registered while this method is waiting, which will influence how long this method
352    /// takes to return. Callers should ensure that all components have been registered before calling this method.
353    pub async fn all_ready(&self) {
354        self.all_ready_matching(|_| true).await
355    }
356
357    /// Waits until all currently registered components whose name matches `predicate` are ready.
358    ///
359    /// This is a scoped variant of [`all_ready`][Self::all_ready] that only considers components whose name satisfies
360    /// the given predicate, which is useful for waiting on a specific subsystem's components to become ready without
361    /// waiting on every other component in the registry.
362    ///
363    /// If no registered component matches the predicate, the method returns immediately. As with
364    /// [`all_ready`][Self::all_ready], components can be registered while this method is waiting, so callers should
365    /// ensure all components they care about have been registered before calling this method.
366    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            // Register as a waiter _before_ checking to avoid missing notifications during the check.
377            let notified = readiness_notify.notified();
378
379            if self.check_ready_matching(&predicate) {
380                return;
381            }
382
383            notified.await;
384        }
385    }
386
387    /// Waits until all currently registered components that are strict descendants of `root` are ready.
388    ///
389    /// This is a convenience wrapper over [`all_ready_matching`][Self::all_ready_matching] for the common case of
390    /// waiting on a single subsystem: it matches exactly the components whose identifier is a strict descendant of
391    /// `root` (see [`SubsystemIdentifier::is_ancestor_of`]). The same registration caveats as
392    /// [`all_ready`][Self::all_ready] apply.
393    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    /// Returns a JSON snapshot of the current readiness and liveness state of all registered components.
410    ///
411    /// Each component appears as a key in the returned JSON object, with its `live` and `ready` boolean fields
412    /// reflecting the state at the time of the call. This is the same data exposed by the `/health/ready` and
413    /// `/health/live` HTTP endpoints, but collected in a single pass for use outside of the HTTP handler path (for
414    /// example, when building a diagnostic artifact).
415    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    /// Creates a [`HealthRegistryWorker`] that can be added to a supervisor to run the health registry.
437    ///
438    /// The worker handles the lifecycle of the health registry runner, including registering the health API routes
439    /// dynamically and running the liveness probing event loop.
440    pub fn worker(&self) -> HealthRegistryWorker {
441        HealthRegistryWorker::new(self.clone())
442    }
443
444    pub(crate) fn into_runner(self) -> Result<Runner, GenericError> {
445        // Make sure the runner hasn't already been spawned.
446        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
461/// A guard that returns the response receiver back to the registry when dropped.
462///
463/// This allows the health registry runner to be restarted gracefully: whenever the runner task
464/// finishes and this guard is dropped (for example, after a shutdown or task cancellation), the
465/// receiver is returned to the registry state so that a subsequent call to `spawn()` can succeed.
466struct 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(&registry),
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        // Drain all pending components.
562        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        // Check if our component is already dead, in which case we don't need to send a liveness probe.
571        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        // Our component _isn't_ dead, so try to send a liveness probe to it.
579        //
580        // We'll register an entry in `pending_timeouts` that automatically marks the component as not live if we don't
581        // receive a response to the liveness probe within the timeout duration.
582        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            // We failed to send the probe to the component due to the component being dead. We'll drop our pending
592            // timeout as we're going to mark this component dead right now.
593            //
594            // When our send fails due to the channel being full, that's OK: it means it's going to be handled by an
595            // existing timeout and will be probed again later.
596            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        // First, drain any pending components to avoid scheduling them twice.
616        // This handles the case where components were registered before the runner started.
617        let _pending = self.drain_pending_components();
618
619        // Drain any queued probe responses from the previous runner. These responses were sent by
620        // components before the runner shut down but weren't processed. Processing them now updates
621        // `last_response` timestamps, which affects the staleness check below — a response that
622        // arrived just before shutdown should count as fresh.
623        while let Ok(response) = responses_rx.try_recv() {
624            self.handle_component_probe_response(response);
625        }
626
627        // Determine which components have stale probe results. Components whose last response is
628        // within the probe timeout are considered fresh and their health state is preserved,
629        // avoiding unnecessary bursts of failed liveness/readiness probes on runner restart.
630        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        // Only reset health to Unknown for components with stale probe results.
647        for &component_id in &stale_component_ids {
648            self.process_component_health_update(component_id, HealthUpdate::Unknown);
649        }
650
651        // Schedule immediate probes for all components regardless of staleness.
652        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        // Clear any pending timeouts for this component and schedule the next probe.
675        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        // Update the component's health to show as alive.
684        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        // Only schedule the next probe if we successfully removed the timeout, meaning it hadn't fired yet.
691        // This prevents duplicate probe scheduling when a response arrives after a timeout.
692        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        // Update the component's health to show as not alive.
702        self.process_component_health_update(component_id, HealthUpdate::Unknown);
703
704        // Schedule the next probe for this component.
705        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        // Update the component's health state based on the given update.
710        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        // Take the response receiver out of the guard so we can use it in the select loop.
728        // It will be put back when the guard is dropped.
729        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        // Schedule probes for all existing components. This allows the runner to "pick up where it
736        // left off" after a restart - any components that were registered before the runner was
737        // restarted will be immediately probed.
738        self.schedule_all_existing_components(&mut responses_rx);
739
740        // Pin the shutdown future so we can poll it in the select loop.
741        pin!(shutdown);
742
743        loop {
744            select! {
745                // Shutdown signal received - exit the run loop gracefully.
746                _ = &mut shutdown => {
747                    info!("Health checker shutting down.");
748                    break;
749                },
750
751                // A component has been scheduled to have a liveness probe sent to it.
752                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                        // If we got a health update for this component, that means we detected that it's dead, so we need
759                        // to do an out-of-band update to its health.
760                        self.process_component_health_update(component_id, health_update);
761                    }
762                },
763
764                // A component's outstanding liveness probe has expired.
765                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                // A probe response has been received.
774                Some(response) = responses_rx.recv() => {
775                    self.handle_component_probe_response(response);
776                },
777
778                // A component is pending finalization of their registration.
779                _ = self.pending_components_notify.notified() => {
780                    // Drain all pending components, give them a clean initial state of "unknown", and immediately schedule a probe for them.
781                    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        // Put the receiver back in the guard so it can be returned to the registry state when dropped.
791        self.guard.responses_rx = Some(responses_rx);
792
793        // When we exit the loop, the RunnerGuard will be dropped, returning the response receiver
794        // back to the registry state so that a subsequent spawn() can succeed.
795    }
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        // Add our component to the registry:
827        let handle = registry
828            .register_component(&SubsystemIdentifier::from_dotted(component_id))
829            .unwrap();
830
831        // Extract the registry runner task and poll it until it's quiesced.
832        //
833        // This ensures that the component is registered, and that it schedules/sends an initial probe request to the component:
834        let runner = registry.into_runner().expect("should not fail to create runner");
835        let runner_state = runner.state();
836
837        // Create a shutdown future that never resolves (for tests that don't need shutdown).
838        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        // Registering the same component twice should fail:
876        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        // First runner creation should succeed. We hold on to it so the RunnerGuard doesn't
890        // return the receiver back to the registry state.
891        let _runner = registry.into_runner().expect("first runner creation should succeed");
892
893        // Second creation should fail while the first runner still holds the receiver.
894        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        // First runner creation should succeed.
904        let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
905        let runner = registry.into_runner().expect("first runner creation should succeed");
906
907        // Run the runner on a spawned task so we can trigger shutdown.
908        let join_handle = tokio::spawn(runner.run(shutdown_rx.map(|_| ())));
909
910        // Trigger shutdown.
911        let _ = shutdown_tx.send(());
912
913        // Wait for the runner to stop.
914        join_handle.await.expect("runner should complete without panic");
915
916        // Now we should be able to create a runner again (the RunnerGuard returned the receiver).
917        let _runner2 = registry2
918            .into_runner()
919            .expect("should be able to create runner after shutdown");
920
921        // But not a third time while the second runner holds the receiver.
922        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        // An empty registry is always ready, so `all_ready` resolves immediately:
933        let mut all_ready_fut = spawn(registry.all_ready());
934        assert_ready!(all_ready_fut.poll());
935
936        // Components start out as not ready, so adding this component changes the registry to not ready overall:
937        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        // Now mark the component as ready. `all_ready` should resolve on the next poll:
945        handle.mark_ready();
946
947        assert!(all_ready_fut.is_woken());
948        assert_ready!(all_ready_fut.poll());
949
950        // Ensure a fresh `all_ready` call immediately observes all components being ready:
951        let mut all_ready_fut = spawn(registry.all_ready());
952        assert_ready!(all_ready_fut.poll());
953
954        // Finally, make sure that the readiness state isn't latched, as `all_ready` should always reflect the current state:
955        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        // Verifies the documented `health_component_ready` telemetry: a `component_id`-tagged gauge set
964        // to 1 when the component is ready and 0 when it is not.
965        let recorder = TestRecorder::default();
966        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
967
968        // Register the component only after installing the recorder so its telemetry binds to it.
969        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        // Components start out not ready.
977        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        // Verifies the documented `health_component_live` gauge (1 = alive, 0 = unknown, -1 = dead) and
989        // `health_component_liveness_latency_seconds` histogram, both tagged with `component_id`.
990        let recorder = TestRecorder::default();
991        let _recorder_guard = metrics::set_default_local_recorder(&recorder);
992
993        // Register and start the runner after installing the recorder so the telemetry binds to it.
994        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        // Drive the runner so it registers the component and sends the initial liveness probe.
1011        let mut live_future = spawn(handle.live());
1012        assert_pending!(live_future.poll());
1013        drive_until_quiesced(&mut registry_task);
1014
1015        // No probe response has been handled yet: the component isn't live and no latency was recorded.
1016        assert!(!component_live(&registry_state, COMPONENT_ID));
1017        assert_eq!(live_gauge(), Some(0.0));
1018        assert_eq!(latency_samples(), Some(Vec::new()));
1019
1020        // Respond to the probe; the runner then observes the response and marks the component live.
1021        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(&registry_state, COMPONENT_ID));
1027        assert_eq!(live_gauge(), Some(1.0));
1028
1029        // Exactly one probe response was handled, so exactly one latency sample was recorded.
1030        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        // Create our registry with a registered component:
1042        let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
1043
1044        // Manually create our `live` call and ensure that it's not ready yet, as the registry task has not yet been driven,
1045        // which means the component hasn't been registered yet and no probe request has been sent:
1046        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 our registry task until it us quiesced to ensure the component is registered and that a probe request is sent:
1052        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        // Ensure our component is not live since, despite being registered, we haven't received a probe response for it yet:
1057        assert!(!component_live(&registry_state, COMPONENT_ID));
1058
1059        // After polling the registry task, we should have sent a probe request which will have now woken up our `live` future.
1060        //
1061        // Poll the future which should then respond to the probe request:
1062        assert!(live_future.is_woken());
1063        assert_ready!(live_future.poll());
1064
1065        // The registry task should have been woken by the probe response.
1066        //
1067        // Drive the registry task until it is quiesced and ensure that the component is now live:
1068        assert!(registry.is_woken());
1069        drive_until_quiesced(&mut registry);
1070
1071        assert!(component_live(&registry_state, COMPONENT_ID));
1072
1073        // Since the probe response was received, we should have a pending schedule probe now since this is a "normal" probe now,
1074        // and isn't the initial probe request which is scheduled immediately:
1075        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        // Create our registry with a registered component:
1082        let (mut handle, mut registry, registry_state, runner_state) = initialize_registry_with_component(COMPONENT_ID);
1083
1084        // Manually create our `live` call and ensure that it's not ready yet, as the registry task has not yet been driven,
1085        // which means the component hasn't been registered yet and no probe request has been sent:
1086        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 our registry task until it us quiesced to ensure the component is registered and that a probe request is sent:
1092        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        // Ensure our component is not live since, despite being registered, we haven't received a probe response for it yet:
1097        assert!(!component_live(&registry_state, COMPONENT_ID));
1098
1099        // After polling the registry task, we should have sent a probe request which will have now woken up
1100        // our `live` future, but we won't yet poll it. In fact, we'll advance time _past_ the probe timeout to simulate
1101        // the probe timeout expiring:
1102        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        // The registry task should have been woken by the probe timeout expiring.
1108        //
1109        // Drive the registry task until it is quiesced and ensure that the component is still not live:
1110        assert!(registry.is_woken());
1111        drive_until_quiesced(&mut registry);
1112
1113        assert!(!component_live(&registry_state, COMPONENT_ID));
1114
1115        // Since the probe response was not received, we should have a pending schedule probe now since this is a "normal" probe now,
1116        // and isn't the initial probe request which is scheduled immediately:
1117        assert_eq!(runner_state.pending_probe_timeouts(), 0);
1118        assert_eq!(runner_state.pending_scheduled_probes(), 1);
1119
1120        // Now, we'll actually drive the `live` future to respond to the probe request, which should mark the component as live:
1121        assert_ready!(live_future.poll());
1122
1123        assert!(registry.is_woken());
1124        drive_until_quiesced(&mut registry);
1125
1126        assert!(component_live(&registry_state, COMPONENT_ID));
1127
1128        // However, since the first probe response timed out, and we haven't yet fired off our scheduled probe, receiving this late
1129        // response should not trigger the scheduling of another probe:
1130        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        // Create our registry with a registered component and drive the runner until the initial probe is sent:
1162        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        // Respond to the probe request so the component becomes live:
1167        let mut live_future = spawn(handle.live());
1168        assert_ready!(live_future.poll());
1169        drive_until_quiesced(&mut registry);
1170        assert!(component_live(&registry_state, COMPONENT_ID));
1171
1172        // Shut down the runner gracefully, which returns the response receiver to the registry state:
1173        let _ = shutdown_tx.send(());
1174        assert_ready!(registry.poll());
1175
1176        // Respawn the runner immediately (no time advance), so the probe result is still fresh:
1177        let registry = HealthRegistry {
1178            inner: Arc::clone(&registry_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        // The component's health should be preserved as Live since its last response is fresh:
1188        assert!(component_live(&registry_state, COMPONENT_ID));
1189    }
1190
1191    #[tokio::test(start_paused = true)]
1192    async fn respawn_resets_stale_component_health() {
1193        // Create our registry with a registered component and drive the runner until the initial probe is sent:
1194        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        // Respond to the probe request so the component becomes live:
1199        let mut live_future = spawn(handle.live());
1200        assert_ready!(live_future.poll());
1201        drive_until_quiesced(&mut registry);
1202        assert!(component_live(&registry_state, COMPONENT_ID));
1203
1204        // Shut down the runner gracefully, which returns the response receiver to the registry state:
1205        let _ = shutdown_tx.send(());
1206        assert_ready!(registry.poll());
1207
1208        // Advance time past the probe timeout so the last response becomes stale:
1209        tokio::time::advance(DEFAULT_PROBE_TIMEOUT_DUR + Duration::from_secs(1)).await;
1210
1211        // Respawn the runner. The stale component should be reset to Unknown:
1212        let registry = HealthRegistry {
1213            inner: Arc::clone(&registry_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        // The component's health should have been reset to Unknown since its last response is stale:
1223        assert!(!component_live(&registry_state, COMPONENT_ID));
1224    }
1225}