mod claims;
mod holds;
use core::fmt;
use crate::{swap_apart, Duration, Instant, Mac, PortId};
pub use claims::{LearnRefusal, Learned};
pub use holds::RejectEffect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reachability {
Reachable,
Busy,
Unreachable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Convergence {
#[default]
Standard,
Hardened,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Route {
port: PortId,
direct: bool,
next_hop: Mac,
last_seen: Option<Instant>,
last_used: Option<Instant>,
reachability: Reachability,
busy_until: Option<Instant>,
flap_count: u8,
last_port_change: Option<Instant>,
}
impl Route {
const ZERO: Self = Self {
port: PortId(0),
direct: false,
next_hop: Mac::EMPTY,
last_seen: None,
last_used: None,
reachability: Reachability::Reachable,
busy_until: None,
flap_count: 0,
last_port_change: None,
};
#[must_use]
pub const fn direct(port: PortId) -> Self {
let mut route = Self::ZERO;
route.port = port;
route.direct = true;
route
}
#[must_use]
pub const fn learned(port: PortId, next_hop: Mac, now: Instant) -> Self {
let mut route = Self::ZERO;
route.port = port;
route.next_hop = next_hop;
route.last_seen = Some(now);
route
}
#[must_use]
pub const fn port(&self) -> PortId {
self.port
}
#[must_use]
pub const fn is_direct(&self) -> bool {
self.direct
}
#[must_use]
pub const fn next_hop(&self) -> &Mac {
&self.next_hop
}
#[must_use]
pub const fn last_seen(&self) -> Option<Instant> {
self.last_seen
}
#[must_use]
pub const fn last_used(&self) -> Option<Instant> {
self.last_used
}
#[must_use]
pub const fn reachability(&self) -> Reachability {
self.reachability
}
#[must_use]
pub const fn busy_until(&self) -> Option<Instant> {
self.busy_until
}
#[must_use]
pub const fn flap_count(&self) -> u8 {
self.flap_count
}
#[must_use]
pub const fn last_port_change(&self) -> Option<Instant> {
self.last_port_change
}
fn served_via(&self, port: PortId, peer: &[u8]) -> bool {
!self.direct && self.port == port && self.next_hop == *peer
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Pending {
pub port: PortId,
pub first_seen: Instant,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ClaimCounters {
pub reject_applied: u64,
pub reject_dampened: u64,
pub reject_dampened_same_state: u64,
pub reject_dampened_hold_down: u64,
pub reject_kept: u64,
pub learned_ok: u64,
pub learned_cap_ignored: u64,
pub flap_warned: u64,
pub pending_started: u64,
pub corroborated_applied: u64,
pub pending_expired: u64,
pub disconnect_removal_ignored: u64,
pub hold_evicted: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timing {
pub busy_for: Duration,
pub hold_down: Duration,
pub corroboration: Duration,
}
impl Timing {
pub const DEFAULT: Self = Self {
busy_for: Duration::from_secs(30),
hold_down: Duration::from_secs(30),
corroboration: Duration::from_secs(60),
};
}
impl Default for Timing {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy)]
struct Slot {
network: u16,
route: Route,
pending: Option<Pending>,
}
const VACANT: Slot = Slot {
network: 0,
route: Route::ZERO,
pending: None,
};
const fn is_reserved(network: u16) -> bool {
network == 0 || network == 0xFFFF
}
#[derive(Clone)]
pub struct Table<const R: usize, const H: usize> {
slots: [Slot; R],
len: usize,
holds: holds::Holds<H>,
counters: ClaimCounters,
convergence: Convergence,
}
impl<const R: usize, const H: usize> fmt::Debug for Table<R, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Table")
.field("routes", &self.stored())
.field("holds", &self.holds)
.field("counters", &self.counters)
.field("convergence", &self.convergence)
.finish()
}
}
impl<const R: usize, const H: usize> Default for Table<R, H> {
fn default() -> Self {
Self::new()
}
}
impl<const R: usize, const H: usize> Table<R, H> {
pub const CAPACITY: usize = R;
#[must_use]
pub const fn new() -> Self {
Self {
slots: [VACANT; R],
len: 0,
holds: holds::Holds::new(),
counters: ClaimCounters {
reject_applied: 0,
reject_dampened: 0,
reject_dampened_same_state: 0,
reject_dampened_hold_down: 0,
reject_kept: 0,
learned_ok: 0,
learned_cap_ignored: 0,
flap_warned: 0,
pending_started: 0,
corroborated_applied: 0,
pending_expired: 0,
disconnect_removal_ignored: 0,
hold_evicted: 0,
},
convergence: Convergence::Standard,
}
}
pub(crate) fn clear(&mut self) {
self.slots[..self.len].fill(VACANT);
self.len = 0;
self.holds.clear();
self.counters = ClaimCounters::default();
}
pub const fn set_convergence(&mut self, convergence: Convergence) {
self.convergence = convergence;
}
#[must_use]
pub const fn convergence(&self) -> Convergence {
self.convergence
}
#[must_use]
pub const fn claim_counters(&self) -> ClaimCounters {
self.counters
}
#[must_use]
pub const fn len(&self) -> usize {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub const fn is_full(&self) -> bool {
self.len == R
}
fn stored(&self) -> &[Slot] {
&self.slots[..self.len]
}
fn find(&self, network: u16) -> Result<usize, usize> {
self.stored()
.binary_search_by_key(&network, |slot| slot.network)
}
fn slot_mut(&mut self, network: u16) -> Option<&mut Slot> {
let index = self.find(network).ok()?;
Some(&mut self.slots[index])
}
fn put(&mut self, network: u16, route: Route) -> bool {
let slot = Slot {
network,
route,
pending: None,
};
match self.find(network) {
Ok(index) => self.slots[index] = slot,
Err(_) if self.len == R => return false,
Err(index) => {
self.slots[self.len] = slot;
for at in (index..self.len).rev() {
swap_apart(&mut self.slots, at, at + 1);
}
self.len += 1;
}
}
true
}
#[must_use]
pub fn lookup(&self, network: u16) -> Option<Route> {
let index = self.find(network).ok()?;
Some(self.slots[index].route)
}
pub fn remove(&mut self, network: u16) -> Option<Route> {
let index = self.find(network).ok()?;
let route = self.slots[index].route;
for at in index + 1..self.len {
swap_apart(&mut self.slots, at - 1, at);
}
self.len -= 1;
self.slots[self.len] = VACANT;
self.holds.forget(network);
Some(route)
}
pub fn routes(&self) -> impl Iterator<Item = (u16, &Route)> + Clone + '_ {
self.stored().iter().map(|slot| (slot.network, &slot.route))
}
pub fn networks(&self) -> impl Iterator<Item = u16> + Clone + '_ {
self.stored().iter().map(|slot| slot.network)
}
pub fn networks_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
self.routes()
.filter(move |(_, route)| route.port == port)
.map(|(network, _)| network)
}
pub fn networks_not_on_port(&self, port: PortId) -> impl Iterator<Item = u16> + Clone + '_ {
self.routes()
.filter(move |(_, route)| route.port != port)
.map(|(network, _)| network)
}
pub fn add_direct(&mut self, network: u16, port: PortId) -> bool {
if is_reserved(network) {
return false;
}
self.holds.forget(network);
self.put(network, Route::direct(port))
}
pub fn add_learned(&mut self, network: u16, port: PortId, next_hop: Mac, now: Instant) -> bool {
if is_reserved(network) {
return false;
}
let last_used = match self.lookup(network) {
Some(existing) if existing.direct => return false,
Some(existing) if existing.port == port => existing.last_used,
_ => None,
};
self.holds.forget(network);
let mut route = Route::learned(port, next_hop, now);
route.last_used = last_used;
self.put(network, route)
}
pub fn mark_busy(&mut self, network: u16, deadline: Instant) {
if let Some(slot) = self.slot_mut(network) {
busy(&mut slot.route, deadline);
}
}
pub fn mark_available(&mut self, network: u16) {
if let Some(slot) = self.slot_mut(network) {
available(&mut slot.route);
}
}
pub fn mark_busy_via_peer(&mut self, port: PortId, peer: &[u8], deadline: Instant) -> usize {
let mut marked = 0;
for slot in &mut self.slots[..self.len] {
if slot.route.served_via(port, peer) && busy(&mut slot.route, deadline) {
marked += 1;
}
}
marked
}
pub fn mark_available_via_peer(&mut self, port: PortId, peer: &[u8]) -> usize {
let mut marked = 0;
for slot in &mut self.slots[..self.len] {
if slot.route.served_via(port, peer) {
available(&mut slot.route);
marked += 1;
}
}
marked
}
pub fn mark_unreachable(&mut self, network: u16) {
if let Some(slot) = self.slot_mut(network) {
let route = &mut slot.route;
if !route.direct {
route.reachability = Reachability::Unreachable;
route.busy_until = None;
}
}
}
#[must_use]
pub fn is_served_via_peer(&self, network: u16, port: PortId, peer: &[u8]) -> bool {
self.lookup(network)
.is_some_and(|route| route.served_via(port, peer))
}
pub fn routes_served_via_peer<'t>(
&'t self,
port: PortId,
peer: &'t [u8],
) -> impl Iterator<Item = u16> + Clone + 't {
self.routes()
.filter(move |(_, route)| route.served_via(port, peer))
.map(|(network, _)| network)
}
pub fn clear_expired_busy(&mut self, now: Instant) -> usize {
let mut cleared = 0;
for slot in &mut self.slots[..self.len] {
let route = &mut slot.route;
if route.busy_until.is_some_and(|deadline| now >= deadline) {
route.reachability = Reachability::Reachable;
route.busy_until = None;
cleared += 1;
}
}
cleared
}
#[must_use]
pub fn effective_reachability(&self, network: u16, now: Instant) -> Option<Reachability> {
let route = self.lookup(network)?;
Some(match route.busy_until {
Some(deadline) if route.reachability == Reachability::Busy && now >= deadline => {
Reachability::Reachable
}
_ => route.reachability,
})
}
pub fn touch(&mut self, network: u16, now: Instant) {
if let Some(slot) = self.slot_mut(network) {
if !slot.route.direct {
slot.route.last_used = Some(now);
}
}
}
pub fn purge_stale(
&mut self,
now: Instant,
max_age: Duration,
mut purged: impl FnMut(u16),
) -> usize {
let stale = |slot: &Slot| {
!slot.route.direct
&& slot
.route
.last_seen
.is_some_and(|seen| now.saturating_duration_since(seen) > max_age)
};
for slot in self.stored().iter().filter(|slot| stale(slot)) {
purged(slot.network);
}
let mut kept = 0;
for index in 0..self.len {
if stale(&self.slots[index]) {
self.holds.forget(self.slots[index].network);
} else {
swap_apart(&mut self.slots, kept, index);
kept += 1;
}
}
let removed = self.len - kept;
self.slots[kept..self.len].fill(VACANT);
self.len = kept;
removed
}
}
fn busy(route: &mut Route, deadline: Instant) -> bool {
let marks = !route.direct && route.reachability != Reachability::Unreachable;
if marks {
route.reachability = Reachability::Busy;
route.busy_until = Some(deadline);
}
marks
}
fn available(route: &mut Route) {
if !route.direct {
route.reachability = Reachability::Reachable;
route.busy_until = None;
}
}
#[cfg(test)]
mod tests;