Skip to main content

a7n_core/
liveness.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//! Link liveness: ping cadence, round-trip time and the two drop rules
4//! (PROTOCOL.md 9). A pure state machine; the daemon passes `Instant` values
5//! in and acts on [`Poll`].
6
7use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10use a7n_proto::limits;
11use a7n_proto::v1::Ping;
12
13/// Which side opened the link; the silence limit differs so that both sides
14/// do not drop at the same instant (PROTOCOL.md 9).
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Role {
17    Initiator,
18    Responder,
19}
20
21impl Role {
22    fn silence_drop(self) -> Duration {
23        match self {
24            Role::Initiator => limits::INITIATOR_SILENCE_DROP,
25            Role::Responder => limits::RESPONDER_SILENCE_DROP,
26        }
27    }
28}
29
30/// Why the link must be closed.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DropReason {
33    /// Nothing arrived from the peer for the role's silence limit.
34    Silence,
35    /// The oldest `InputBatch` stayed unacknowledged too long.
36    UnackedInput,
37}
38
39/// What the driver must do now.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Poll {
42    Idle,
43    SendPing(Ping),
44    Drop(DropReason),
45}
46
47/// Liveness of one link.
48///
49/// Limits are inclusive: a link silent for exactly the limit is dropped.
50#[derive(Debug)]
51pub struct Liveness {
52    role: Role,
53    last_received: Instant,
54    last_ping_sent: Instant,
55    /// Never 0: in proto3 0 is "unset", and `echo == 0` must mean "no echo".
56    next_probe: u32,
57    /// Only the newest probe is timed; an echo of an older one is ignored.
58    probe_in_flight: Option<(u32, Instant)>,
59    unacked: VecDeque<(u64, Instant)>,
60}
61
62impl Liveness {
63    #[must_use]
64    pub fn new(role: Role, now: Instant) -> Self {
65        Self {
66            role,
67            last_received: now,
68            last_ping_sent: now,
69            next_probe: 1,
70            probe_in_flight: None,
71            unacked: VecDeque::new(),
72        }
73    }
74
75    /// Any frame or datagram arrived from the peer.
76    pub fn on_received(&mut self, now: Instant) {
77        self.last_received = now;
78    }
79
80    /// An `InputBatch` with `seq` was sent.
81    pub fn on_input_sent(&mut self, seq: u64, now: Instant) {
82        self.unacked.push_back((seq, now));
83    }
84
85    /// An `Ack` arrived. Acks are cumulative, so every batch up to `ack_seq`
86    /// is cleared. Returns the round-trip time of the newest cleared batch:
87    /// `now - sent_at - held_us`, never below zero.
88    pub fn on_ack(&mut self, ack_seq: u64, held_us: u32, now: Instant) -> Option<Duration> {
89        let mut newest_sent_at = None;
90        while let Some(&(seq, sent_at)) = self.unacked.front() {
91            if seq > ack_seq {
92                break;
93            }
94            newest_sent_at = Some(sent_at);
95            self.unacked.pop_front();
96        }
97        let held = Duration::from_micros(u64::from(held_us));
98        newest_sent_at.map(|sent_at| now.saturating_duration_since(sent_at).saturating_sub(held))
99    }
100
101    /// A `Ping` arrived. Returns the round-trip time when it echoes the
102    /// probe we are timing. A probe needs an answer from [`answer`].
103    pub fn on_ping(&mut self, ping: &Ping, now: Instant) -> Option<Duration> {
104        let (probe, sent_at) = self.probe_in_flight?;
105        if ping.echo != probe {
106            return None;
107        }
108        self.probe_in_flight = None;
109        Some(now.saturating_duration_since(sent_at))
110    }
111
112    /// Decides what to do at `now`. Call it when [`Self::next_deadline`]
113    /// passes and after every event.
114    pub fn poll(&mut self, now: Instant) -> Poll {
115        if now.saturating_duration_since(self.last_received) >= self.role.silence_drop() {
116            return Poll::Drop(DropReason::Silence);
117        }
118        if let Some(&(_, sent_at)) = self.unacked.front()
119            && now.saturating_duration_since(sent_at) >= limits::UNACKED_INPUT_DROP
120        {
121            return Poll::Drop(DropReason::UnackedInput);
122        }
123        if now.saturating_duration_since(self.last_ping_sent) < limits::PING_INTERVAL {
124            return Poll::Idle;
125        }
126        self.last_ping_sent = now;
127        Poll::SendPing(self.new_probe(now))
128    }
129
130    /// The earliest instant at which [`Self::poll`] may have work.
131    #[must_use]
132    pub fn next_deadline(&self) -> Instant {
133        let silence = self.last_received + self.role.silence_drop();
134        let ping = self.last_ping_sent + limits::PING_INTERVAL;
135        let unacked = self
136            .unacked
137            .front()
138            .map(|&(_, sent_at)| sent_at + limits::UNACKED_INPUT_DROP);
139        [Some(silence), Some(ping), unacked]
140            .into_iter()
141            .flatten()
142            .min()
143            .unwrap_or(silence)
144    }
145
146    fn new_probe(&mut self, now: Instant) -> Ping {
147        let probe = self.next_probe;
148        // Skip 0 on wrap, see `next_probe`.
149        self.next_probe = self.next_probe.checked_add(1).unwrap_or(1);
150        self.probe_in_flight = Some((probe, now));
151        Ping { probe, echo: 0 }
152    }
153}
154
155/// The reply to a received `Ping`: an echo for a probe, nothing for an echo
156/// (an echo is never echoed).
157#[must_use]
158pub fn answer(ping: &Ping) -> Option<Ping> {
159    if ping.probe == 0 {
160        return None;
161    }
162    Some(Ping {
163        probe: 0,
164        echo: ping.probe,
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    fn ms(millis: u64) -> Duration {
173        Duration::from_millis(millis)
174    }
175
176    #[test]
177    fn poll_silence_reaches_role_limit_drops_link() {
178        // Arrange: (role, silent for, dropped)
179        let cases = [
180            (Role::Initiator, 1999, false),
181            (Role::Initiator, 2000, true),
182            (Role::Responder, 2000, false),
183            (Role::Responder, 2499, false),
184            (Role::Responder, 2500, true),
185        ];
186
187        for (role, silent_ms, dropped) in cases {
188            let start = Instant::now();
189            let mut liveness = Liveness::new(role, start);
190            // Keep the ping rule out of the way.
191            liveness.last_ping_sent = start + ms(silent_ms);
192
193            // Act
194            let poll = liveness.poll(start + ms(silent_ms));
195
196            // Assert
197            assert_eq!(
198                poll == Poll::Drop(DropReason::Silence),
199                dropped,
200                "{role:?} {silent_ms} ms"
201            );
202        }
203    }
204
205    #[test]
206    fn poll_oldest_batch_unacked_for_1500ms_drops_link() {
207        // Arrange
208        let start = Instant::now();
209        let mut liveness = Liveness::new(Role::Responder, start);
210        liveness.on_input_sent(1, start);
211        liveness.on_input_sent(2, start + ms(400));
212        liveness.on_received(start + ms(1500));
213        liveness.last_ping_sent = start + ms(1500);
214
215        // Act
216        let before = liveness.poll(start + ms(1499));
217        let at_limit = liveness.poll(start + ms(1500));
218
219        // Assert
220        assert_eq!(before, Poll::Idle);
221        assert_eq!(at_limit, Poll::Drop(DropReason::UnackedInput));
222    }
223
224    #[test]
225    fn new_probe_counter_wrapped_skips_zero() {
226        // Arrange
227        let start = Instant::now();
228        let mut liveness = Liveness::new(Role::Initiator, start);
229        liveness.next_probe = u32::MAX;
230
231        // Act
232        let last = liveness.new_probe(start);
233        let wrapped = liveness.new_probe(start);
234
235        // Assert
236        assert_eq!(last.probe, u32::MAX);
237        assert_eq!(wrapped.probe, 1);
238    }
239
240    #[test]
241    fn answer_echo_is_never_echoed() {
242        // Arrange
243        let probe = Ping { probe: 7, echo: 0 };
244        let echo = Ping { probe: 0, echo: 7 };
245
246        // Act
247        let reply_to_probe = answer(&probe);
248        let reply_to_echo = answer(&echo);
249
250        // Assert
251        assert_eq!(reply_to_probe, Some(echo));
252        assert_eq!(reply_to_echo, None);
253    }
254}