use core::fmt;
use super::{Reachability, Table, Timing};
use crate::message::RejectReason;
use crate::{swap_apart, Instant, PortId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RejectEffect {
Applied,
SameState,
HeldDown,
Kept,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Hold {
pub(crate) network: u16,
pub(crate) port: PortId,
pub(crate) since: Instant,
}
const NO_HOLD: Hold = Hold {
network: 0,
port: PortId(0),
since: Instant::ZERO,
};
#[derive(Clone)]
pub(super) struct Holds<const H: usize> {
records: [Hold; H],
len: usize,
}
impl<const H: usize> fmt::Debug for Holds<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(self.stored()).finish()
}
}
impl<const H: usize> Holds<H> {
pub(super) const fn new() -> Self {
Self {
records: [NO_HOLD; H],
len: 0,
}
}
fn stored(&self) -> &[Hold] {
&self.records[..self.len]
}
fn since(&self, network: u16, port: PortId) -> Option<Instant> {
self.stored()
.iter()
.find(|hold| hold.network == network && hold.port == port)
.map(|hold| hold.since)
}
fn record(&mut self, network: u16, port: PortId, now: Instant) -> bool {
let hold = Hold {
network,
port,
since: now,
};
let stored = &mut self.records[..self.len];
if let Some(existing) = stored
.iter_mut()
.find(|hold| hold.network == network && hold.port == port)
{
*existing = hold;
return false;
}
if self.len < H {
self.records[self.len] = hold;
self.len += 1;
return false;
}
if let Some(oldest) = self.records.iter_mut().min_by_key(|hold| hold.since) {
*oldest = hold;
}
true
}
pub(super) fn clear(&mut self) {
self.records[..self.len].fill(NO_HOLD);
self.len = 0;
}
pub(super) fn forget(&mut self, network: u16) {
let mut kept = 0;
for index in 0..self.len {
if self.records[index].network != network {
swap_apart(&mut self.records, kept, index);
kept += 1;
}
}
self.records[kept..self.len].fill(NO_HOLD);
self.len = kept;
}
}
impl<const R: usize, const H: usize> Table<R, H> {
pub fn apply_reject(
&mut self,
network: u16,
port: PortId,
reason: RejectReason,
now: Instant,
timing: &Timing,
) -> RejectEffect {
let learned = self.lookup(network).is_some_and(|route| !route.direct);
let other = !matches!(
reason,
RejectReason::NOT_DIRECTLY_CONNECTED | RejectReason::ROUTER_BUSY
);
if learned && other {
let kept = &mut self.counters.reject_kept;
*kept = kept.saturating_add(1);
return RejectEffect::Kept;
}
let changes = self.lookup(network).is_some_and(|route| {
let lapsed = route.busy_until.is_some_and(|deadline| now >= deadline);
!route.direct
&& match reason {
RejectReason::NOT_DIRECTLY_CONNECTED => {
route.reachability != Reachability::Unreachable
}
RejectReason::ROUTER_BUSY => {
route.reachability != Reachability::Unreachable
&& (route.reachability != Reachability::Busy || lapsed)
}
_ => false,
}
});
let held_down = self
.holds
.since(network, port)
.is_some_and(|since| now.saturating_duration_since(since) < timing.hold_down);
let counters = &mut self.counters;
if !changes || held_down {
counters.reject_dampened = counters.reject_dampened.saturating_add(1);
return if changes {
counters.reject_dampened_hold_down =
counters.reject_dampened_hold_down.saturating_add(1);
RejectEffect::HeldDown
} else {
counters.reject_dampened_same_state =
counters.reject_dampened_same_state.saturating_add(1);
RejectEffect::SameState
};
}
counters.reject_applied = counters.reject_applied.saturating_add(1);
if reason == RejectReason::NOT_DIRECTLY_CONNECTED {
self.mark_unreachable(network);
} else {
self.mark_busy(network, now + timing.busy_for);
}
if self.holds.record(network, port, now) {
self.counters.hold_evicted = self.counters.hold_evicted.saturating_add(1);
}
RejectEffect::Applied
}
#[cfg(test)]
pub(crate) fn held_since(&self, network: u16, port: PortId) -> Option<Instant> {
self.holds.since(network, port)
}
#[cfg(test)]
pub(crate) fn holds(&self) -> impl Iterator<Item = Hold> + '_ {
self.holds.stored().iter().copied()
}
}