use core::fmt;
use super::{is_reserved, Convergence, Pending, Route, Table, Timing};
use crate::{Duration, Instant, Mac, PortId};
const FLAP_WINDOW: Duration = Duration::from_secs(60);
const FLAP_WARNING: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LearnRefusal {
Reserved,
Direct,
Full,
}
impl fmt::Display for LearnRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Reserved => "network 0 and 0xFFFF name no single network",
Self::Direct => "the network is directly attached",
Self::Full => "the routing table is full",
})
}
}
impl core::error::Error for LearnRefusal {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[must_use]
pub enum Learned {
Refused(LearnRefusal),
Held,
Applied,
Moved {
from: PortId,
flaps: u8,
},
}
impl Learned {
#[must_use]
pub const fn applied(self) -> bool {
matches!(self, Self::Applied | Self::Moved { .. })
}
#[must_use]
pub const fn flapping(self) -> bool {
matches!(self, Self::Moved { flaps, .. } if flaps >= FLAP_WARNING)
}
}
impl<const R: usize, const H: usize> Table<R, H> {
pub fn apply_claim(
&mut self,
network: u16,
port: PortId,
next_hop: Mac,
now: Instant,
timing: &Timing,
) -> Learned {
let Ok(index) = self.find(network) else {
return self.learn(network, port, next_hop, now);
};
let pending = self.slots[index].pending.take();
if self.convergence == Convergence::Standard {
return self.learn(network, port, next_hop, now);
}
let counters = &mut self.counters;
let pending = match pending {
Some(pending)
if now.saturating_duration_since(pending.first_seen) > timing.corroboration =>
{
counters.pending_expired = counters.pending_expired.saturating_add(1);
None
}
live => live,
};
let route = self.slots[index].route;
if !route.direct && route.port != port {
if pending.is_some_and(|pending| pending.port == port) {
counters.corroborated_applied = counters.corroborated_applied.saturating_add(1);
} else {
self.slots[index].pending = Some(Pending {
port,
first_seen: now,
});
counters.pending_started = counters.pending_started.saturating_add(1);
return Learned::Held;
}
}
self.learn(network, port, next_hop, now)
}
pub fn learn(&mut self, network: u16, port: PortId, next_hop: Mac, now: Instant) -> Learned {
if is_reserved(network) {
return Learned::Refused(LearnRefusal::Reserved);
}
if let Some(existing) = self.lookup(network) {
if existing.direct {
return Learned::Refused(LearnRefusal::Direct);
}
if existing.port != port {
let flaps = match existing.last_port_change {
Some(changed) if now.saturating_duration_since(changed) < FLAP_WINDOW => {
existing.flap_count.saturating_add(1)
}
_ => 1,
};
if flaps >= FLAP_WARNING {
self.counters.flap_warned = self.counters.flap_warned.saturating_add(1);
}
let mut route = Route::learned(port, next_hop, now);
route.flap_count = flaps;
route.last_port_change = Some(now);
self.holds.forget(network);
self.put(network, route);
return Learned::Moved {
from: existing.port,
flaps,
};
}
}
if self.add_learned(network, port, next_hop, now) {
Learned::Applied
} else {
Learned::Refused(LearnRefusal::Full)
}
}
pub fn expire_pending(
&mut self,
now: Instant,
timing: &Timing,
mut expired: impl FnMut(u16),
) -> usize {
let window = timing.corroboration;
let mut count = 0;
for slot in &mut self.slots[..self.len] {
if slot
.pending
.is_some_and(|pending| now.saturating_duration_since(pending.first_seen) > window)
{
slot.pending = None;
self.counters.pending_expired = self.counters.pending_expired.saturating_add(1);
expired(slot.network);
count += 1;
}
}
count
}
#[must_use]
pub fn pending(&self, network: u16) -> Option<Pending> {
let index = self.find(network).ok()?;
self.slots[index].pending
}
#[must_use]
pub fn pending_count(&self) -> usize {
self.stored()
.iter()
.filter(|slot| slot.pending.is_some())
.count()
}
pub fn apply_management_update(
&mut self,
network: u16,
port: PortId,
next_hop: Mac,
now: Instant,
) -> bool {
self.add_learned(network, port, next_hop, now)
}
pub fn apply_management_removal(&mut self, network: u16) -> bool {
match self.lookup(network) {
Some(route) if !route.direct => self.remove(network).is_some(),
_ => false,
}
}
pub(crate) fn record_learned(&mut self) {
self.counters.learned_ok = self.counters.learned_ok.saturating_add(1);
}
pub(crate) fn record_learning_cap(&mut self) {
self.counters.learned_cap_ignored = self.counters.learned_cap_ignored.saturating_add(1);
}
pub(crate) fn record_disconnect_removal_ignored(&mut self) {
self.counters.disconnect_removal_ignored =
self.counters.disconnect_removal_ignored.saturating_add(1);
}
}