use bacnet_bip_codec::BvlcResultCode;
use bacnet_time::{Duration, Instant};
use super::config::{Foreign, Retry};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Lease {
pub since: Instant,
pub until: Instant,
}
impl Lease {
#[must_use]
pub fn is_live_at(&self, now: Instant) -> bool {
now < self.until
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AttemptOutcome {
Pending,
Accepted,
Refused,
TimedOut,
LocalError,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RegistrationEvent {
Registered {
since: Instant,
until: Instant,
},
Renewed {
until: Instant,
},
Refused {
code: BvlcResultCode,
},
Unanswered,
Lapsed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RegistrationSnapshot {
pub lease: Option<Lease>,
pub registered: bool,
pub lease_remaining: Option<Duration>,
pub attempt_in_flight: bool,
pub next_attempt: Option<Instant>,
pub last_outcome: Option<AttemptOutcome>,
pub consecutive_failures: u32,
pub first_resolved: bool,
pub held: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Admission {
Send,
Hold,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BroadcastDecision {
Send { lapsed: bool },
Hold,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct HeldVerdict {
pub(crate) release: bool,
pub(crate) count: u16,
}
#[derive(Debug, Clone, Copy)]
struct Recent {
starts: [Instant; Recent::LEN],
len: u8,
next: u8,
evicted: Option<Instant>,
}
impl Recent {
const LEN: usize = 4;
const fn new() -> Self {
Self {
starts: [Instant::ZERO; Self::LEN],
len: 0,
next: 0,
evicted: None,
}
}
fn push(&mut self, start: Instant) {
let slot = &mut self.starts[usize::from(self.next)];
if usize::from(self.len) == Self::LEN {
self.evicted = Some(*slot);
} else {
self.len += 1;
}
*slot = start;
self.next = (self.next + 1) & (Self::LEN as u8 - 1);
}
fn earliest_after(&self, floor: Option<Instant>) -> Option<Instant> {
let inside = |start: Instant| floor.is_none_or(|floor| start > floor);
if self.evicted.is_some_and(inside) {
return Some(floor.unwrap_or(Instant::ZERO));
}
self.starts[..usize::from(self.len)]
.iter()
.copied()
.filter(|start| inside(*start))
.min()
}
}
#[derive(Debug, Clone, Copy)]
struct Attempt {
start: Instant,
}
#[derive(Debug, Clone)]
pub(crate) struct Lifecycle {
ttl: Duration,
renewal: Duration,
timeout: Duration,
retry: Retry,
hold_limit: u16,
lease: Option<Lease>,
lapsed: bool,
unanswered_since: Option<Instant>,
recent: Recent,
attempt: Option<Attempt>,
last_start: Option<Instant>,
last_outcome: Option<AttemptOutcome>,
failures: u32,
next: Option<Instant>,
first_resolved: bool,
held: u16,
}
impl Lifecycle {
pub(crate) fn new(foreign: &Foreign, now: Instant) -> Self {
Self {
ttl: foreign.ttl_duration(),
renewal: foreign.effective_renewal(),
timeout: foreign.effective_response_timeout(),
retry: foreign.retry,
hold_limit: foreign.hold,
lease: None,
lapsed: false,
unanswered_since: None,
recent: Recent::new(),
attempt: None,
last_start: None,
last_outcome: None,
failures: 0,
next: Some(now),
first_resolved: false,
held: 0,
}
}
pub(crate) fn retarget(&mut self, foreign: &Foreign, now: Instant, same_bbmd: bool) {
self.ttl = foreign.ttl_duration();
self.renewal = foreign.effective_renewal();
self.timeout = foreign.effective_response_timeout();
self.retry = foreign.retry;
self.hold_limit = foreign.hold;
if !same_bbmd {
self.lease = None;
self.lapsed = false;
self.unanswered_since = None;
self.recent = Recent::new();
self.first_resolved = false;
}
self.attempt = None;
self.last_start = None;
self.failures = 0;
self.next = Some(now);
}
pub(crate) fn answer_window(&self) -> Duration {
let three = self.timeout.saturating_mul(2).saturating_add(self.timeout);
three.min(self.renewal)
}
pub(crate) fn due_at(&self) -> Option<Instant> {
self.next.filter(|_| self.attempt.is_none())
}
pub(crate) fn lapse_at(&self) -> Option<Instant> {
self.lease.filter(|_| !self.lapsed).map(|lease| lease.until)
}
pub(crate) fn lapse(&mut self) {
self.lapsed = true;
}
pub(crate) fn admitted(&mut self, now: Instant) {
self.attempt = Some(Attempt { start: now });
self.next = None;
self.last_start = Some(now);
self.last_outcome = Some(AttemptOutcome::Pending);
}
pub(crate) fn sent(&mut self) {
if let Some(attempt) = self.attempt {
self.unanswered_since.get_or_insert(attempt.start);
self.recent.push(attempt.start);
}
}
pub(crate) fn accepted(
&mut self,
now: Instant,
) -> (Option<RegistrationEvent>, Option<HeldVerdict>) {
let start = self.attempt.take().map_or(now, |attempt| attempt.start);
let floor = now.checked_sub(self.answer_window());
let since = [self.unanswered_since, self.recent.earliest_after(floor)]
.into_iter()
.flatten()
.fold(start, Instant::min);
let until = since + self.ttl;
let was_live = self.lease.is_some_and(|lease| lease.is_live_at(now));
self.lease = Some(Lease { since, until });
self.lapsed = until <= now;
self.unanswered_since = None;
self.failures = 0;
self.next = Some(
match self.retry {
Retry::Backoff => since,
Retry::AtRenewal => start,
} + self.renewal,
);
self.last_outcome = Some(AttemptOutcome::Accepted);
let event = match (self.lapsed, was_live) {
(true, _) => None,
(false, true) => Some(RegistrationEvent::Renewed { until }),
(false, false) => Some(RegistrationEvent::Registered { since, until }),
};
(event, self.resolve_first(true))
}
pub(crate) fn refused(&mut self, now: Instant) -> Option<HeldVerdict> {
let start = self.attempt.take().map_or(now, |attempt| attempt.start);
self.unanswered_since = None;
self.failures = 0;
self.next = Some(start + self.renewal);
self.last_outcome = Some(AttemptOutcome::Refused);
self.resolve_first(false)
}
pub(crate) fn failed(
&mut self,
now: Instant,
outcome: Option<AttemptOutcome>,
) -> Option<HeldVerdict> {
let start = self.attempt.take().map_or(now, |attempt| attempt.start);
self.failures = self.failures.saturating_add(1);
let at_renewal = start + self.renewal;
self.next = Some(match self.retry {
Retry::AtRenewal => at_renewal,
Retry::Backoff => at_renewal.min(now + self.backoff()),
});
if outcome.is_some() {
self.last_outcome = outcome;
}
self.resolve_first(true)
}
fn backoff(&self) -> Duration {
let mut wait = self.timeout;
for _ in 1..self.failures {
if wait >= self.renewal {
break;
}
wait = wait.saturating_mul(2);
}
wait.min(self.renewal)
}
pub(crate) fn hint(&mut self, now: Instant) {
if self.attempt.is_some() {
return;
}
let earliest = self
.last_start
.map_or(now, |start| (start + self.timeout).max(now));
if self.next.is_some_and(|next| next > earliest) {
self.next = Some(earliest);
}
}
pub(crate) fn admit_broadcast(&mut self, now: Instant) -> BroadcastDecision {
if !self.first_resolved && self.hold_limit > 0 {
if self.held >= self.hold_limit {
return BroadcastDecision::Full;
}
self.held += 1;
return BroadcastDecision::Hold;
}
let lapsed = !self.lease.is_some_and(|lease| lease.is_live_at(now));
BroadcastDecision::Send { lapsed }
}
fn resolve_first(&mut self, release: bool) -> Option<HeldVerdict> {
if self.first_resolved {
return None;
}
self.first_resolved = true;
let count = core::mem::take(&mut self.held);
(count > 0).then_some(HeldVerdict { release, count })
}
pub(crate) fn snapshot(&self, now: Instant) -> RegistrationSnapshot {
let registered = self.lease.is_some_and(|lease| lease.is_live_at(now));
RegistrationSnapshot {
lease: self.lease,
registered,
lease_remaining: self
.lease
.filter(|_| registered)
.map(|lease| lease.until - now),
attempt_in_flight: self.attempt.is_some(),
next_attempt: self.due_at(),
last_outcome: self.last_outcome,
consecutive_failures: self.failures,
first_resolved: self.first_resolved,
held: self.held,
}
}
}
#[cfg(test)]
mod tests {
use super::{Instant, Recent};
fn at(secs: u64) -> Instant {
Instant::from_secs(secs)
}
#[test]
fn the_ring_answers_with_the_earliest_start_after_the_floor() {
let mut recent = Recent::new();
assert_eq!(recent.earliest_after(None), None);
for start in [10, 20, 30] {
recent.push(at(start));
}
assert_eq!(recent.earliest_after(None), Some(at(10)));
assert_eq!(recent.earliest_after(Some(at(10))), Some(at(20)));
assert_eq!(recent.earliest_after(Some(at(9))), Some(at(10)));
assert_eq!(recent.earliest_after(Some(at(30))), None);
}
#[test]
fn a_start_pushed_out_of_the_ring_falls_back_to_the_floor() {
let mut recent = Recent::new();
for start in [10, 20, 30, 40, 50] {
recent.push(at(start));
}
assert_eq!(recent.earliest_after(Some(at(5))), Some(at(5)));
assert_eq!(recent.earliest_after(None), Some(Instant::ZERO));
assert_eq!(recent.earliest_after(Some(at(10))), Some(at(20)));
assert_eq!(recent.earliest_after(Some(at(25))), Some(at(30)));
recent.push(at(60));
assert_eq!(recent.earliest_after(Some(at(15))), Some(at(15)));
assert_eq!(recent.earliest_after(Some(at(20))), Some(at(30)));
}
}