1use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10use a7n_proto::limits;
11use a7n_proto::v1::Ping;
12
13#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DropReason {
33 Silence,
35 UnackedInput,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Poll {
42 Idle,
43 SendPing(Ping),
44 Drop(DropReason),
45}
46
47#[derive(Debug)]
51pub struct Liveness {
52 role: Role,
53 last_received: Instant,
54 last_ping_sent: Instant,
55 next_probe: u32,
57 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 pub fn on_received(&mut self, now: Instant) {
77 self.last_received = now;
78 }
79
80 pub fn on_input_sent(&mut self, seq: u64, now: Instant) {
82 self.unacked.push_back((seq, now));
83 }
84
85 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 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 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 #[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 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#[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 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 liveness.last_ping_sent = start + ms(silent_ms);
192
193 let poll = liveness.poll(start + ms(silent_ms));
195
196 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 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 let before = liveness.poll(start + ms(1499));
217 let at_limit = liveness.poll(start + ms(1500));
218
219 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 let start = Instant::now();
228 let mut liveness = Liveness::new(Role::Initiator, start);
229 liveness.next_probe = u32::MAX;
230
231 let last = liveness.new_probe(start);
233 let wrapped = liveness.new_probe(start);
234
235 assert_eq!(last.probe, u32::MAX);
237 assert_eq!(wrapped.probe, 1);
238 }
239
240 #[test]
241 fn answer_echo_is_never_echoed() {
242 let probe = Ping { probe: 7, echo: 0 };
244 let echo = Ping { probe: 0, echo: 7 };
245
246 let reply_to_probe = answer(&probe);
248 let reply_to_echo = answer(&echo);
249
250 assert_eq!(reply_to_probe, Some(echo));
252 assert_eq!(reply_to_echo, None);
253 }
254}