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}