1use bacnet_bip_codec::BvlcResultCode;
21use bacnet_time::{Duration, Instant};
22
23use super::config::{Foreign, Retry};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct Lease {
44 pub since: Instant,
46 pub until: Instant,
48}
49
50impl Lease {
51 #[must_use]
53 pub fn is_live_at(&self, now: Instant) -> bool {
54 now < self.until
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum AttemptOutcome {
62 Pending,
64 Accepted,
66 Refused,
68 TimedOut,
70 LocalError,
72 Cancelled,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum RegistrationEvent {
80 Registered {
83 since: Instant,
85 until: Instant,
87 },
88 Renewed {
90 until: Instant,
92 },
93 Refused {
95 code: BvlcResultCode,
97 },
98 Unanswered,
101 Lapsed,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[non_exhaustive]
108pub struct RegistrationSnapshot {
109 pub lease: Option<Lease>,
111 pub registered: bool,
113 pub lease_remaining: Option<Duration>,
115 pub attempt_in_flight: bool,
117 pub next_attempt: Option<Instant>,
120 pub last_outcome: Option<AttemptOutcome>,
122 pub consecutive_failures: u32,
124 pub first_resolved: bool,
127 pub held: u16,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum Admission {
134 Send,
136 Hold,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub(crate) enum BroadcastDecision {
145 Send { lapsed: bool },
146 Hold,
147 Full,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub(crate) struct HeldVerdict {
153 pub(crate) release: bool,
154 pub(crate) count: u16,
155}
156
157#[derive(Debug, Clone, Copy)]
159struct Recent {
160 starts: [Instant; Recent::LEN],
161 len: u8,
162 next: u8,
163 evicted: Option<Instant>,
165}
166
167impl Recent {
168 const LEN: usize = 4;
169
170 const fn new() -> Self {
171 Self {
172 starts: [Instant::ZERO; Self::LEN],
173 len: 0,
174 next: 0,
175 evicted: None,
176 }
177 }
178
179 fn push(&mut self, start: Instant) {
180 let slot = &mut self.starts[usize::from(self.next)];
181 if usize::from(self.len) == Self::LEN {
182 self.evicted = Some(*slot);
183 } else {
184 self.len += 1;
185 }
186 *slot = start;
187 self.next = (self.next + 1) & (Self::LEN as u8 - 1);
188 }
189
190 fn earliest_after(&self, floor: Option<Instant>) -> Option<Instant> {
194 let inside = |start: Instant| floor.is_none_or(|floor| start > floor);
195 if self.evicted.is_some_and(inside) {
196 return Some(floor.unwrap_or(Instant::ZERO));
197 }
198 self.starts[..usize::from(self.len)]
199 .iter()
200 .copied()
201 .filter(|start| inside(*start))
202 .min()
203 }
204}
205
206#[derive(Debug, Clone, Copy)]
208struct Attempt {
209 start: Instant,
211}
212
213#[derive(Debug, Clone)]
215pub(crate) struct Lifecycle {
216 ttl: Duration,
217 renewal: Duration,
218 timeout: Duration,
219 retry: Retry,
220 hold_limit: u16,
221 lease: Option<Lease>,
222 lapsed: bool,
224 unanswered_since: Option<Instant>,
226 recent: Recent,
227 attempt: Option<Attempt>,
228 last_start: Option<Instant>,
229 last_outcome: Option<AttemptOutcome>,
230 failures: u32,
231 next: Option<Instant>,
232 first_resolved: bool,
233 held: u16,
234}
235
236impl Lifecycle {
237 pub(crate) fn new(foreign: &Foreign, now: Instant) -> Self {
239 Self {
240 ttl: foreign.ttl_duration(),
241 renewal: foreign.effective_renewal(),
242 timeout: foreign.effective_response_timeout(),
243 retry: foreign.retry,
244 hold_limit: foreign.hold,
245 lease: None,
246 lapsed: false,
247 unanswered_since: None,
248 recent: Recent::new(),
249 attempt: None,
250 last_start: None,
251 last_outcome: None,
252 failures: 0,
253 next: Some(now),
254 first_resolved: false,
255 held: 0,
256 }
257 }
258
259 pub(crate) fn retarget(&mut self, foreign: &Foreign, now: Instant, same_bbmd: bool) {
273 self.ttl = foreign.ttl_duration();
274 self.renewal = foreign.effective_renewal();
275 self.timeout = foreign.effective_response_timeout();
276 self.retry = foreign.retry;
277 self.hold_limit = foreign.hold;
278 if !same_bbmd {
279 self.lease = None;
280 self.lapsed = false;
281 self.unanswered_since = None;
282 self.recent = Recent::new();
283 self.first_resolved = false;
285 }
286 self.attempt = None;
287 self.last_start = None;
288 self.failures = 0;
289 self.next = Some(now);
290 }
291
292 pub(crate) fn answer_window(&self) -> Duration {
301 let three = self.timeout.saturating_mul(2).saturating_add(self.timeout);
302 three.min(self.renewal)
303 }
304
305 pub(crate) fn due_at(&self) -> Option<Instant> {
307 self.next.filter(|_| self.attempt.is_none())
308 }
309
310 pub(crate) fn lapse_at(&self) -> Option<Instant> {
312 self.lease.filter(|_| !self.lapsed).map(|lease| lease.until)
313 }
314
315 pub(crate) fn lapse(&mut self) {
317 self.lapsed = true;
318 }
319
320 pub(crate) fn admitted(&mut self, now: Instant) {
322 self.attempt = Some(Attempt { start: now });
323 self.next = None;
324 self.last_start = Some(now);
325 self.last_outcome = Some(AttemptOutcome::Pending);
326 }
327
328 pub(crate) fn sent(&mut self) {
330 if let Some(attempt) = self.attempt {
331 self.unanswered_since.get_or_insert(attempt.start);
332 self.recent.push(attempt.start);
333 }
334 }
335
336 pub(crate) fn accepted(
339 &mut self,
340 now: Instant,
341 ) -> (Option<RegistrationEvent>, Option<HeldVerdict>) {
342 let start = self.attempt.take().map_or(now, |attempt| attempt.start);
343 let floor = now.checked_sub(self.answer_window());
346 let since = [self.unanswered_since, self.recent.earliest_after(floor)]
347 .into_iter()
348 .flatten()
349 .fold(start, Instant::min);
350 let until = since + self.ttl;
351 let was_live = self.lease.is_some_and(|lease| lease.is_live_at(now));
352 self.lease = Some(Lease { since, until });
353 self.lapsed = until <= now;
354 self.unanswered_since = None;
355 self.failures = 0;
356 self.next = Some(
359 match self.retry {
360 Retry::Backoff => since,
361 Retry::AtRenewal => start,
362 } + self.renewal,
363 );
364 self.last_outcome = Some(AttemptOutcome::Accepted);
365 let event = match (self.lapsed, was_live) {
366 (true, _) => None,
367 (false, true) => Some(RegistrationEvent::Renewed { until }),
368 (false, false) => Some(RegistrationEvent::Registered { since, until }),
369 };
370 (event, self.resolve_first(true))
371 }
372
373 pub(crate) fn refused(&mut self, now: Instant) -> Option<HeldVerdict> {
376 let start = self.attempt.take().map_or(now, |attempt| attempt.start);
377 self.unanswered_since = None;
378 self.failures = 0;
379 self.next = Some(start + self.renewal);
380 self.last_outcome = Some(AttemptOutcome::Refused);
381 self.resolve_first(false)
382 }
383
384 pub(crate) fn failed(
388 &mut self,
389 now: Instant,
390 outcome: Option<AttemptOutcome>,
391 ) -> Option<HeldVerdict> {
392 let start = self.attempt.take().map_or(now, |attempt| attempt.start);
393 self.failures = self.failures.saturating_add(1);
394 let at_renewal = start + self.renewal;
395 self.next = Some(match self.retry {
396 Retry::AtRenewal => at_renewal,
397 Retry::Backoff => at_renewal.min(now + self.backoff()),
398 });
399 if outcome.is_some() {
400 self.last_outcome = outcome;
401 }
402 self.resolve_first(true)
403 }
404
405 fn backoff(&self) -> Duration {
408 let mut wait = self.timeout;
409 for _ in 1..self.failures {
410 if wait >= self.renewal {
411 break;
412 }
413 wait = wait.saturating_mul(2);
414 }
415 wait.min(self.renewal)
416 }
417
418 pub(crate) fn hint(&mut self, now: Instant) {
423 if self.attempt.is_some() {
424 return;
425 }
426 let earliest = self
427 .last_start
428 .map_or(now, |start| (start + self.timeout).max(now));
429 if self.next.is_some_and(|next| next > earliest) {
430 self.next = Some(earliest);
431 }
432 }
433
434 pub(crate) fn admit_broadcast(&mut self, now: Instant) -> BroadcastDecision {
436 if !self.first_resolved && self.hold_limit > 0 {
437 if self.held >= self.hold_limit {
438 return BroadcastDecision::Full;
439 }
440 self.held += 1;
441 return BroadcastDecision::Hold;
442 }
443 let lapsed = !self.lease.is_some_and(|lease| lease.is_live_at(now));
444 BroadcastDecision::Send { lapsed }
445 }
446
447 fn resolve_first(&mut self, release: bool) -> Option<HeldVerdict> {
449 if self.first_resolved {
450 return None;
451 }
452 self.first_resolved = true;
453 let count = core::mem::take(&mut self.held);
454 (count > 0).then_some(HeldVerdict { release, count })
455 }
456
457 pub(crate) fn snapshot(&self, now: Instant) -> RegistrationSnapshot {
458 let registered = self.lease.is_some_and(|lease| lease.is_live_at(now));
459 RegistrationSnapshot {
460 lease: self.lease,
461 registered,
462 lease_remaining: self
463 .lease
464 .filter(|_| registered)
465 .map(|lease| lease.until - now),
466 attempt_in_flight: self.attempt.is_some(),
467 next_attempt: self.due_at(),
468 last_outcome: self.last_outcome,
469 consecutive_failures: self.failures,
470 first_resolved: self.first_resolved,
471 held: self.held,
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::{Instant, Recent};
479
480 fn at(secs: u64) -> Instant {
481 Instant::from_secs(secs)
482 }
483
484 #[test]
485 fn the_ring_answers_with_the_earliest_start_after_the_floor() {
486 let mut recent = Recent::new();
487 assert_eq!(recent.earliest_after(None), None);
488 for start in [10, 20, 30] {
489 recent.push(at(start));
490 }
491 assert_eq!(recent.earliest_after(None), Some(at(10)));
492 assert_eq!(recent.earliest_after(Some(at(10))), Some(at(20)));
493 assert_eq!(recent.earliest_after(Some(at(9))), Some(at(10)));
494 assert_eq!(recent.earliest_after(Some(at(30))), None);
495 }
496
497 #[test]
498 fn a_start_pushed_out_of_the_ring_falls_back_to_the_floor() {
499 let mut recent = Recent::new();
500 for start in [10, 20, 30, 40, 50] {
501 recent.push(at(start));
502 }
503 assert_eq!(recent.earliest_after(Some(at(5))), Some(at(5)));
505 assert_eq!(recent.earliest_after(None), Some(Instant::ZERO));
506 assert_eq!(recent.earliest_after(Some(at(10))), Some(at(20)));
508 assert_eq!(recent.earliest_after(Some(at(25))), Some(at(30)));
509 recent.push(at(60));
510 assert_eq!(recent.earliest_after(Some(at(15))), Some(at(15)));
511 assert_eq!(recent.earliest_after(Some(at(20))), Some(at(30)));
512 }
513}