Skip to main content

brokapi_runtime/
health.rs

1//! Connection health classification.
2
3use std::time::{Duration, Instant};
4
5/// How healthy a connection looks, judged by how long it has been silent.
6///
7/// The [`Supervisor`](crate::Supervisor) maps each class to an action.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum HealthClass {
10    /// Data arrived recently. Nothing to do.
11    Healthy,
12    /// The connection has been quiet for longer than the degraded threshold but not yet long
13    /// enough to be declared dead. Missing subscriptions should be retried.
14    Degraded,
15    /// Quiet for at least the dead threshold. The connection should be replaced.
16    Dead,
17}
18
19/// Receive-side health signals of one connection.
20///
21/// # Examples
22///
23/// ```
24/// use std::time::{Duration, Instant};
25/// use brokapi_runtime::{ConnectionHealth, HealthClass};
26///
27/// let t0 = Instant::now();
28/// let mut health = ConnectionHealth::new(t0);
29/// let (degraded_after, dead_after) = (Duration::from_secs(15), Duration::from_secs(60));
30///
31/// assert_eq!(health.classify(t0 + Duration::from_secs(5), dead_after, degraded_after), HealthClass::Healthy);
32/// assert_eq!(health.classify(t0 + Duration::from_secs(20), dead_after, degraded_after), HealthClass::Degraded);
33/// assert_eq!(health.classify(t0 + Duration::from_secs(60), dead_after, degraded_after), HealthClass::Dead);
34///
35/// // A frame arrives: the silence timer restarts.
36/// health.on_rx(t0 + Duration::from_secs(61));
37/// assert_eq!(health.classify(t0 + Duration::from_secs(62), dead_after, degraded_after), HealthClass::Healthy);
38/// ```
39#[derive(Debug, Clone)]
40pub struct ConnectionHealth {
41    /// When the last frame arrived. Starts at the creation time.
42    pub last_rx: Instant,
43    /// Round-trip time of the last ping, if the owner measures it. The runtime does not fill it.
44    pub rtt: Option<Duration>,
45    /// Number of sequence gaps seen on this connection. The runtime does not count them by itself.
46    pub seq_gaps: u64,
47}
48
49impl ConnectionHealth {
50    /// Creates the health record of a connection that is considered just heard from at `now`.
51    pub fn new(now: Instant) -> Self {
52        Self {
53            last_rx: now,
54            rtt: None,
55            seq_gaps: 0,
56        }
57    }
58
59    /// Records that a frame arrived at `now`.
60    pub fn on_rx(&mut self, now: Instant) {
61        self.last_rx = now;
62    }
63
64    /// Classifies the connection by the time since the last frame.
65    ///
66    /// Silence of `dead_after` or more gives [`HealthClass::Dead`]. Silence of `degraded_after` or
67    /// more gives [`HealthClass::Degraded`]. Otherwise [`HealthClass::Healthy`]. The caller should
68    /// pass `degraded_after < dead_after`, otherwise a connection is never reported as degraded.
69    /// A `now` earlier than `last_rx` counts as no silence.
70    pub fn classify(
71        &self,
72        now: Instant,
73        dead_after: Duration,
74        degraded_after: Duration,
75    ) -> HealthClass {
76        let silent = now.duration_since(self.last_rx);
77        if silent >= dead_after {
78            HealthClass::Dead
79        } else if silent >= degraded_after {
80            HealthClass::Degraded
81        } else {
82            HealthClass::Healthy
83        }
84    }
85}