diff --git a/holo-isis/src/debug.rs b/holo-isis/src/debug.rs index 48420e75..3a74018a 100644 --- a/holo-isis/src/debug.rs +++ b/holo-isis/src/debug.rs @@ -16,6 +16,7 @@ use crate::interface::{DisCandidate, Interface}; use crate::network::MulticastAddr; use crate::packet::pdu::{Lsp, Pdu}; use crate::packet::LevelNumber; +use crate::spf; // IS-IS debug messages. #[derive(Debug)] @@ -46,6 +47,9 @@ pub enum Debug<'a> { LspPurge(LevelNumber, &'a Lsp, LspPurgeReason), LspDelete(LevelNumber, &'a Lsp), LspRefresh(LevelNumber, &'a Lsp), + // SPF + SpfDelayFsmEvent(LevelNumber, spf::fsm::State, spf::fsm::Event), + SpfDelayFsmTransition(LevelNumber, spf::fsm::State, spf::fsm::State), } // Reason why an IS-IS instance is inactive. @@ -159,6 +163,14 @@ impl Debug<'_> { // Parent span(s): isis-instance debug!(?level, lsp_id = %lsp.lsp_id.to_yang(), seqno = %lsp.seqno, len = %lsp.raw.len(), ?reason, "{}", self); } + Debug::SpfDelayFsmEvent(level, state, event) => { + // Parent span(s): isis-instance + debug!(?level, ?state, ?event, "{}", self); + } + Debug::SpfDelayFsmTransition(level, old_state, new_state) => { + // Parent span(s): isis-instance + debug!(?level, ?old_state, ?new_state, "{}", self); + } } } } @@ -223,6 +235,12 @@ impl std::fmt::Display for Debug<'_> { Debug::LspRefresh(..) => { write!(f, "refreshing LSP") } + Debug::SpfDelayFsmEvent(..) => { + write!(f, "SPF Delay FSM event") + } + Debug::SpfDelayFsmTransition(..) => { + write!(f, "SPF Delay FSM state transition") + } } } } diff --git a/holo-isis/src/error.rs b/holo-isis/src/error.rs index f08cd6c5..2566409b 100644 --- a/holo-isis/src/error.rs +++ b/holo-isis/src/error.rs @@ -15,6 +15,8 @@ use crate::collections::{ use crate::instance::InstanceArenas; use crate::network::MulticastAddr; use crate::packet::error::DecodeError; +use crate::packet::LevelNumber; +use crate::spf; // IS-IS errors. #[derive(Debug)] @@ -30,6 +32,7 @@ pub enum Error { AdjacencyReject(InterfaceIndex, [u8; 6], AdjacencyRejectError), // Other CircuitIdAllocationFailed, + SpfDelayUnexpectedEvent(LevelNumber, spf::fsm::State, spf::fsm::Event), InterfaceStartError(String, Box), InstanceStartError(Box), } @@ -91,6 +94,9 @@ impl Error { Error::CircuitIdAllocationFailed => { warn!("{}", self); } + Error::SpfDelayUnexpectedEvent(level, state, event) => { + warn!(?level, ?state, ?event, "{}", self); + } Error::InterfaceStartError(name, error) => { error!(%name, error = %with_source(error), "{}", self); } @@ -121,6 +127,9 @@ impl std::fmt::Display for Error { Error::CircuitIdAllocationFailed => { write!(f, "failed to allocate Circuit ID") } + Error::SpfDelayUnexpectedEvent(..) => { + write!(f, "unexpected SPF Delay FSM event") + } Error::InterfaceStartError(..) => { write!(f, "failed to start interface") } diff --git a/holo-isis/src/events.rs b/holo-isis/src/events.rs index af5bb36d..3fd9b0e1 100644 --- a/holo-isis/src/events.rs +++ b/holo-isis/src/events.rs @@ -28,8 +28,8 @@ use crate::packet::error::{DecodeError, DecodeResult}; use crate::packet::pdu::{Hello, HelloVariant, Lsp, Pdu, Snp, SnpTlvs}; use crate::packet::tlv::LspEntry; use crate::packet::{LanId, LevelNumber, LevelType, LspId}; -use crate::tasks; use crate::tasks::messages::input::DisElectionMsg; +use crate::{spf, tasks}; // ===== Network PDU receipt ===== @@ -1168,3 +1168,15 @@ pub(crate) fn process_lsp_refresh( Ok(()) } + +// ===== SPF Delay FSM event ===== + +pub(crate) fn process_spf_delay_event( + instance: &mut InstanceUpView<'_>, + arenas: &mut InstanceArenas, + level: LevelNumber, + event: spf::fsm::Event, +) -> Result<(), Error> { + // Trigger SPF Delay FSM event. + spf::fsm(level, event, instance, arenas) +} diff --git a/holo-isis/src/instance.rs b/holo-isis/src/instance.rs index e8f34912..7ea1bb00 100644 --- a/holo-isis/src/instance.rs +++ b/holo-isis/src/instance.rs @@ -32,12 +32,14 @@ use crate::interface::CircuitIdAllocator; use crate::lsdb::{LspEntry, LspLogEntry}; use crate::northbound::configuration::InstanceCfg; use crate::packet::{LevelNumber, LevelType, Levels}; +use crate::spf::SpfScheduler; use crate::tasks::messages::input::{ AdjHoldTimerMsg, DisElectionMsg, LspDeleteMsg, LspOriginateMsg, LspPurgeMsg, LspRefreshMsg, NetRxPduMsg, SendCsnpMsg, SendPsnpMsg, + SpfDelayEventMsg, }; use crate::tasks::messages::{ProtocolInputMsg, ProtocolOutputMsg}; -use crate::{events, lsdb, southbound, tasks}; +use crate::{events, lsdb, southbound, spf, tasks}; #[derive(Debug)] pub struct Instance { @@ -73,6 +75,8 @@ pub struct InstanceState { pub lsp_orig_last: Option, pub lsp_orig_backoff: Option, pub lsp_orig_pending: Option, + // SPF scheduler state. + pub spf_sched: Levels, // Event counters. pub counters: Levels, pub discontinuity_time: DateTime, @@ -124,6 +128,8 @@ pub struct ProtocolInputChannelsTx { pub lsp_delete: UnboundedSender, // LSP refresh event. pub lsp_refresh: UnboundedSender, + // SPF Delay FSM event. + pub spf_delay_event: UnboundedSender, } #[derive(Debug)] @@ -146,6 +152,8 @@ pub struct ProtocolInputChannelsRx { pub lsp_delete: UnboundedReceiver, // LSP refresh event. pub lsp_refresh: UnboundedReceiver, + // SPF Delay FSM event. + pub spf_delay_event: UnboundedReceiver, } pub struct InstanceUpView<'a> { @@ -319,6 +327,7 @@ impl ProtocolInstance for Instance { let (lsp_purgep, lsp_purgec) = mpsc::unbounded_channel(); let (lsp_deletep, lsp_deletec) = mpsc::unbounded_channel(); let (lsp_refreshp, lsp_refreshc) = mpsc::unbounded_channel(); + let (spf_delay_eventp, spf_delay_eventc) = mpsc::unbounded_channel(); let tx = ProtocolInputChannelsTx { net_pdu_rx: net_pdu_rxp, @@ -330,6 +339,7 @@ impl ProtocolInstance for Instance { lsp_purge: lsp_purgep, lsp_delete: lsp_deletep, lsp_refresh: lsp_refreshp, + spf_delay_event: spf_delay_eventp, }; let rx = ProtocolInputChannelsRx { net_pdu_rx: net_pdu_rxc, @@ -341,6 +351,7 @@ impl ProtocolInstance for Instance { lsp_purge: lsp_purgec, lsp_delete: lsp_deletec, lsp_refresh: lsp_refreshc, + spf_delay_event: spf_delay_eventc, }; (tx, rx) @@ -362,6 +373,7 @@ impl InstanceState { lsp_orig_last: None, lsp_orig_backoff: None, lsp_orig_pending: None, + spf_sched: Default::default(), counters: Default::default(), discontinuity_time: Utc::now(), lsp_log: Default::default(), @@ -394,6 +406,14 @@ impl ProtocolInputChannelsTx { }; let _ = self.lsp_refresh.send(msg); } + + pub(crate) fn spf_delay_event( + &self, + level: LevelNumber, + event: spf::fsm::Event, + ) { + let _ = self.spf_delay_event.send(SpfDelayEventMsg { level, event }); + } } // ===== impl ProtocolInputChannelsRx ===== @@ -430,6 +450,9 @@ impl MessageReceiver for ProtocolInputChannelsRx { msg = self.lsp_refresh.recv() => { msg.map(ProtocolInputMsg::LspRefresh) } + msg = self.spf_delay_event.recv() => { + msg.map(ProtocolInputMsg::SpfDelayEvent) + } } } } @@ -602,6 +625,12 @@ fn process_protocol_msg( msg.lse_key, )?; } + // SPF Delay FSM event. + ProtocolInputMsg::SpfDelayEvent(msg) => { + events::process_spf_delay_event( + instance, arenas, msg.level, msg.event, + )? + } } Ok(()) diff --git a/holo-isis/src/lib.rs b/holo-isis/src/lib.rs index c87e7786..abd65ba6 100644 --- a/holo-isis/src/lib.rs +++ b/holo-isis/src/lib.rs @@ -25,4 +25,5 @@ pub mod network; pub mod northbound; pub mod packet; pub mod southbound; +pub mod spf; pub mod tasks; diff --git a/holo-isis/src/lsdb.rs b/holo-isis/src/lsdb.rs index 38d0cd03..6e1814c9 100644 --- a/holo-isis/src/lsdb.rs +++ b/holo-isis/src/lsdb.rs @@ -27,8 +27,8 @@ use crate::packet::consts::LspFlags; use crate::packet::pdu::{Lsp, LspTlvs}; use crate::packet::tlv::{ExtIpv4Reach, ExtIsReach, Ipv4Reach, IsReach, Nlpid}; use crate::packet::{LanId, LevelNumber, LspId}; -use crate::tasks; use crate::tasks::messages::input::LspPurgeMsg; +use crate::{spf, tasks}; // LSP ZeroAge lifetime. pub const LSP_ZERO_AGE_LIFETIME: u64 = 60; @@ -437,6 +437,14 @@ pub(crate) fn install<'a>( }; log_lsp(instance, level, lsp_log_id.clone(), None, reason); + // Schedule SPF run if necessary. + if content_change { + instance + .tx + .protocol_input + .spf_delay_event(level, spf::fsm::Event::Igp); + } + lse } diff --git a/holo-isis/src/northbound/configuration.rs b/holo-isis/src/northbound/configuration.rs index cf5eb230..315e3a5b 100644 --- a/holo-isis/src/northbound/configuration.rs +++ b/holo-isis/src/northbound/configuration.rs @@ -28,6 +28,7 @@ use crate::debug::InterfaceInactiveReason; use crate::instance::Instance; use crate::interface::InterfaceType; use crate::packet::{AreaAddr, LevelNumber, LevelType, SystemId}; +use crate::spf; use crate::tasks::messages::input::DisElectionMsg; #[derive(Debug, Default)] @@ -1069,7 +1070,16 @@ impl Provider for Instance { } } } - Event::RerunSpf => {} + Event::RerunSpf => { + if let Some((instance, _)) = self.as_up() { + for level in instance.config.levels() { + instance.tx.protocol_input.spf_delay_event( + level, + spf::fsm::Event::ConfigChange, + ); + } + } + } } } } @@ -1085,6 +1095,14 @@ impl InstanceCfg { true } + + // Returns the levels supported by the instance. + pub(crate) fn levels(&self) -> SmallVec<[LevelNumber; 2]> { + [LevelNumber::L1, LevelNumber::L2] + .into_iter() + .filter(|level| self.level_type.intersects(level)) + .collect() + } } impl InterfaceCfg { diff --git a/holo-isis/src/spf.rs b/holo-isis/src/spf.rs new file mode 100644 index 00000000..71781c7a --- /dev/null +++ b/holo-isis/src/spf.rs @@ -0,0 +1,269 @@ +// +// Copyright (c) The Holo Core Contributors +// +// SPDX-License-Identifier: MIT +// +// Sponsored by NLnet as part of the Next Generation Internet initiative. +// See: https://nlnet.nl/NGI0 +// + +use std::time::{Duration, Instant}; + +use chrono::Utc; +use holo_utils::task::TimeoutTask; + +use crate::adjacency::Adjacency; +use crate::collections::{Arena, Interfaces}; +use crate::debug::Debug; +use crate::error::Error; +use crate::instance::{InstanceArenas, InstanceUpView}; +use crate::lsdb::LspEntry; +use crate::packet::LevelNumber; +use crate::tasks; + +#[derive(Debug, Default)] +pub struct SpfScheduler { + pub last_event_rcvd: Option, + pub last_time: Option, + pub delay_state: fsm::State, + pub delay_timer: Option, + pub hold_down_timer: Option, + pub learn_timer: Option, +} + +// SPF Delay State Machine. +pub mod fsm { + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] + #[derive(Deserialize, Serialize)] + pub enum State { + #[default] + Quiet, + ShortWait, + LongWait, + } + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[derive(Deserialize, Serialize)] + pub enum Event { + Igp, + DelayTimer, + HoldDownTimer, + LearnTimer, + ConfigChange, + } +} + +// ===== global functions ===== + +pub(crate) fn fsm( + level: LevelNumber, + event: fsm::Event, + instance: &mut InstanceUpView<'_>, + arenas: &mut InstanceArenas, +) -> Result<(), Error> { + let spf_sched = instance.state.spf_sched.get_mut(level); + + Debug::SpfDelayFsmEvent(level, spf_sched.delay_state, event).log(); + + // Update time of last SPF triggering event. + spf_sched.last_event_rcvd = Some(Instant::now()); + + let new_fsm_state = match (spf_sched.delay_state, &event) { + // Transition 1: IGP event while in QUIET state. + (fsm::State::Quiet, fsm::Event::Igp) => { + // If SPF_TIMER is not already running, start it with value + // INITIAL_SPF_DELAY. + if spf_sched.delay_timer.is_none() { + let task = tasks::spf_delay_timer( + level, + fsm::Event::DelayTimer, + instance.config.spf_initial_delay, + &instance.tx.protocol_input.spf_delay_event, + ); + spf_sched.delay_timer = Some(task); + } + + // Start LEARN_TIMER with TIME_TO_LEARN_INTERVAL. + let task = tasks::spf_delay_timer( + level, + fsm::Event::LearnTimer, + instance.config.spf_time_to_learn, + &instance.tx.protocol_input.spf_delay_event, + ); + spf_sched.learn_timer = Some(task); + + // Start HOLDDOWN_TIMER with HOLDDOWN_INTERVAL. + let task = tasks::spf_delay_timer( + level, + fsm::Event::HoldDownTimer, + instance.config.spf_hold_down, + &instance.tx.protocol_input.spf_delay_event, + ); + spf_sched.hold_down_timer = Some(task); + + // Transition to SHORT_WAIT state. + Some(fsm::State::ShortWait) + } + // Transition 2: IGP event while in SHORT_WAIT. + (fsm::State::ShortWait, fsm::Event::Igp) => { + // Reset HOLDDOWN_TIMER to HOLDDOWN_INTERVAL. + if let Some(timer) = &mut spf_sched.hold_down_timer { + let timeout = + Duration::from_millis(instance.config.spf_hold_down.into()); + timer.reset(Some(timeout)); + } + + // If SPF_TIMER is not already running, start it with value + // SHORT_SPF_DELAY. + if spf_sched.delay_timer.is_none() { + let task = tasks::spf_delay_timer( + level, + fsm::Event::DelayTimer, + instance.config.spf_short_delay, + &instance.tx.protocol_input.spf_delay_event, + ); + spf_sched.delay_timer = Some(task); + } + + // Remain in current state. + None + } + // Transition 3: LEARN_TIMER expiration. + (fsm::State::ShortWait, fsm::Event::LearnTimer) => { + spf_sched.learn_timer = None; + + // Transition to LONG_WAIT state. + Some(fsm::State::LongWait) + } + // Transition 4: IGP event while in LONG_WAIT. + (fsm::State::LongWait, fsm::Event::Igp) => { + // Reset HOLDDOWN_TIMER to HOLDDOWN_INTERVAL. + if let Some(timer) = &mut spf_sched.hold_down_timer { + let timeout = + Duration::from_millis(instance.config.spf_hold_down.into()); + timer.reset(Some(timeout)); + } + + // If SPF_TIMER is not already running, start it with value + // LONG_SPF_DELAY. + if spf_sched.delay_timer.is_none() { + let task = tasks::spf_delay_timer( + level, + fsm::Event::DelayTimer, + instance.config.spf_long_delay, + &instance.tx.protocol_input.spf_delay_event, + ); + spf_sched.delay_timer = Some(task); + } + + // Remain in current state. + None + } + // Transition 5: HOLDDOWN_TIMER expiration while in LONG_WAIT. + (fsm::State::LongWait, fsm::Event::HoldDownTimer) => { + spf_sched.hold_down_timer = None; + + // Transition to QUIET state. + Some(fsm::State::Quiet) + } + // Transition 6: HOLDDOWN_TIMER expiration while in SHORT_WAIT. + (fsm::State::ShortWait, fsm::Event::HoldDownTimer) => { + spf_sched.hold_down_timer = None; + + // Deactivate LEARN_TIMER. + spf_sched.learn_timer = None; + + // Transition to QUIET state. + Some(fsm::State::Quiet) + } + // Transition 7: SPF_TIMER expiration while in QUIET. + // Transition 8: SPF_TIMER expiration while in SHORT_WAIT. + // Transition 9: SPF_TIMER expiration while in LONG_WAIT + ( + fsm::State::Quiet | fsm::State::ShortWait | fsm::State::LongWait, + fsm::Event::DelayTimer, + ) => { + spf_sched.delay_timer = None; + + // Compute SPF. + compute_spf( + level, + instance, + &arenas.interfaces, + &arenas.adjacencies, + &arenas.lsp_entries, + ); + + // Remain in current state. + None + } + // Custom FSM transition. + ( + fsm::State::Quiet | fsm::State::ShortWait | fsm::State::LongWait, + fsm::Event::ConfigChange, + ) => { + // Cancel the next scheduled SPF run, but preserve the other timers. + spf_sched.delay_timer = None; + + // Compute SPF. + compute_spf( + level, + instance, + &arenas.interfaces, + &arenas.adjacencies, + &arenas.lsp_entries, + ); + + // Remain in current state. + None + } + _ => { + return Err(Error::SpfDelayUnexpectedEvent( + level, + spf_sched.delay_state, + event, + )); + } + }; + + if let Some(new_fsm_state) = new_fsm_state { + let spf_sched = instance.state.spf_sched.get_mut(level); + if new_fsm_state != spf_sched.delay_state { + // Effectively transition to the new FSM state. + Debug::SpfDelayFsmTransition( + level, + spf_sched.delay_state, + new_fsm_state, + ) + .log(); + spf_sched.delay_state = new_fsm_state; + } + } + + Ok(()) +} + +// ===== helper functions ===== + +// This is the SPF main function. +fn compute_spf( + level: LevelNumber, + instance: &mut InstanceUpView<'_>, + _interfaces: &Interfaces, + _adjacencies: &Arena, + _lsp_entries: &Arena, +) { + let spf_sched = instance.state.spf_sched.get_mut(level); + + // TODO: Run SPF. + + // Update statistics. + instance.state.counters.get_mut(level).spf_runs += 1; + instance.state.discontinuity_time = Utc::now(); + + // Update time of last SPF computation. + let end_time = Instant::now(); + spf_sched.last_time = Some(end_time); +} diff --git a/holo-isis/src/tasks.rs b/holo-isis/src/tasks.rs index 1a6f4ad4..98e58ed4 100644 --- a/holo-isis/src/tasks.rs +++ b/holo-isis/src/tasks.rs @@ -23,7 +23,7 @@ use crate::interface::{Interface, InterfaceType}; use crate::network::MulticastAddr; use crate::packet::pdu::{Lsp, Pdu}; use crate::packet::{LevelNumber, LevelType, Levels}; -use crate::{lsdb, network}; +use crate::{lsdb, network, spf}; // // IS-IS tasks diagram: @@ -44,6 +44,7 @@ use crate::{lsdb, network}; // lsp_expiry_timer (Nx) -> | | // lsp_delete_timer (Nx) -> | | // lsp_refresh_timer (Nx) -> | | +// spf_delay_timer (Nx) -> | | // | | // +--------------+ // ibus_tx (1x) | ^ (1x) ibus_rx @@ -65,6 +66,7 @@ pub mod messages { use crate::packet::error::DecodeError; use crate::packet::pdu::Pdu; use crate::packet::LevelNumber; + use crate::spf; // Type aliases. pub type ProtocolInputMsg = input::ProtocolMsg; @@ -86,6 +88,7 @@ pub mod messages { LspPurge(LspPurgeMsg), LspDelete(LspDeleteMsg), LspRefresh(LspRefreshMsg), + SpfDelayEvent(SpfDelayEventMsg), } #[derive(Debug)] @@ -156,6 +159,12 @@ pub mod messages { pub level: LevelNumber, pub lse_key: LspEntryKey, } + #[derive(Debug)] + #[derive(Deserialize, Serialize)] + pub struct SpfDelayEventMsg { + pub level: LevelNumber, + pub event: spf::fsm::Event, + } } // Output messages (main task -> child task). @@ -564,3 +573,26 @@ pub(crate) fn lsp_refresh_timer( TimeoutTask {} } } + +// SPF delay timer task. +pub(crate) fn spf_delay_timer( + level: LevelNumber, + event: spf::fsm::Event, + timeout: u32, + spf_delay_eventp: &UnboundedSender, +) -> TimeoutTask { + #[cfg(not(feature = "testing"))] + { + let timeout = Duration::from_millis(timeout.into()); + let spf_delay_eventp = spf_delay_eventp.clone(); + + TimeoutTask::new(timeout, move || async move { + let msg = messages::input::SpfDelayEventMsg { level, event }; + let _ = spf_delay_eventp.send(msg); + }) + } + #[cfg(feature = "testing")] + { + TimeoutTask {} + } +}