ndn_strategy/
best_route.rs

1use bytes::Bytes;
2use smallvec::{SmallVec, smallvec};
3
4use ndn_packet::{Name, NameComponent};
5use ndn_transport::{ForwardingAction, NackReason};
6
7use crate::{Strategy, StrategyContext};
8
9/// Best-route strategy: forward on the lowest-cost FIB nexthop, excluding the
10/// incoming face (split-horizon).
11pub struct BestRouteStrategy {
12    name: Name,
13}
14
15impl BestRouteStrategy {
16    /// NFD strategy name: `/localhost/nfd/strategy/best-route`
17    pub fn strategy_name() -> Name {
18        Name::from_components([
19            NameComponent::generic(Bytes::from_static(b"localhost")),
20            NameComponent::generic(Bytes::from_static(b"nfd")),
21            NameComponent::generic(Bytes::from_static(b"strategy")),
22            NameComponent::generic(Bytes::from_static(b"best-route")),
23        ])
24    }
25
26    pub fn new() -> Self {
27        Self {
28            name: Self::strategy_name(),
29        }
30    }
31}
32
33impl Default for BestRouteStrategy {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl Strategy for BestRouteStrategy {
40    fn name(&self) -> &Name {
41        &self.name
42    }
43
44    fn decide(&self, ctx: &StrategyContext<'_>) -> Option<SmallVec<[ForwardingAction; 2]>> {
45        let Some(fib) = ctx.fib_entry else {
46            return Some(smallvec![ForwardingAction::Nack(NackReason::NoRoute)]);
47        };
48        let nexthops = fib.nexthops_excluding(ctx.in_face);
49        match nexthops.first() {
50            Some(nh) => Some(smallvec![ForwardingAction::Forward(smallvec![nh.face_id])]),
51            None => Some(smallvec![ForwardingAction::Nack(NackReason::NoRoute)]),
52        }
53    }
54
55    async fn after_receive_interest(
56        &self,
57        ctx: &StrategyContext<'_>,
58    ) -> SmallVec<[ForwardingAction; 2]> {
59        // Sync fast path handles all cases; this is unreachable when
60        // called through ErasedStrategy but kept for direct Strategy use.
61        self.decide(ctx).unwrap()
62    }
63
64    async fn after_receive_data(
65        &self,
66        _ctx: &StrategyContext<'_>,
67    ) -> SmallVec<[ForwardingAction; 2]> {
68        // Fan-back to in-record faces is handled by the engine via PIT lookup.
69        SmallVec::new()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::MeasurementsTable;
77    use crate::context::{FibEntry, FibNexthop};
78    use ndn_transport::FaceId;
79    use std::sync::Arc;
80
81    fn make_ctx<'a>(
82        name: &'a Arc<Name>,
83        in_face: FaceId,
84        fib_entry: Option<&'a FibEntry>,
85        measurements: &'a MeasurementsTable,
86    ) -> StrategyContext<'a> {
87        static EMPTY: std::sync::LazyLock<ndn_transport::AnyMap> =
88            std::sync::LazyLock::new(ndn_transport::AnyMap::new);
89        StrategyContext {
90            name,
91            in_face,
92            fib_entry,
93            pit_token: None,
94            measurements,
95            extensions: &EMPTY,
96        }
97    }
98
99    #[tokio::test]
100    async fn no_fib_entry_returns_nack_no_route() {
101        let strategy = BestRouteStrategy::new();
102        let name = Arc::new(Name::root());
103        let measurements = MeasurementsTable::new();
104        let ctx = make_ctx(&name, FaceId(0), None, &measurements);
105        let actions = strategy.after_receive_interest(&ctx).await;
106        assert!(matches!(
107            actions.as_slice(),
108            [ForwardingAction::Nack(NackReason::NoRoute)]
109        ));
110    }
111
112    #[tokio::test]
113    async fn best_nexthop_selected() {
114        let strategy = BestRouteStrategy::new();
115        let name = Arc::new(Name::root());
116        let measurements = MeasurementsTable::new();
117        let fib = FibEntry {
118            nexthops: vec![
119                FibNexthop {
120                    face_id: FaceId(2),
121                    cost: 10,
122                },
123                FibNexthop {
124                    face_id: FaceId(3),
125                    cost: 20,
126                },
127            ],
128        };
129        let ctx = make_ctx(&name, FaceId(1), Some(&fib), &measurements);
130        let actions = strategy.after_receive_interest(&ctx).await;
131        // First nexthop not equal to in_face should be forwarded
132        if let [ForwardingAction::Forward(faces)] = actions.as_slice() {
133            assert_eq!(faces[0], FaceId(2));
134        } else {
135            panic!("expected Forward");
136        }
137    }
138
139    #[tokio::test]
140    async fn split_horizon_excludes_in_face() {
141        let strategy = BestRouteStrategy::new();
142        let name = Arc::new(Name::root());
143        let measurements = MeasurementsTable::new();
144        // Only nexthop is the same as in_face → no route
145        let fib = FibEntry {
146            nexthops: vec![FibNexthop {
147                face_id: FaceId(1),
148                cost: 0,
149            }],
150        };
151        let ctx = make_ctx(&name, FaceId(1), Some(&fib), &measurements);
152        let actions = strategy.after_receive_interest(&ctx).await;
153        assert!(matches!(
154            actions.as_slice(),
155            [ForwardingAction::Nack(NackReason::NoRoute)]
156        ));
157    }
158
159    #[tokio::test]
160    async fn after_receive_data_returns_empty() {
161        let strategy = BestRouteStrategy::new();
162        let name = Arc::new(Name::root());
163        let measurements = MeasurementsTable::new();
164        let ctx = make_ctx(&name, FaceId(0), None, &measurements);
165        let actions = strategy.after_receive_data(&ctx).await;
166        assert!(actions.is_empty());
167    }
168
169    #[test]
170    fn strategy_name() {
171        let s = BestRouteStrategy::new();
172        let comps = s.name().components();
173        assert_eq!(comps.len(), 4);
174        assert_eq!(comps[3].value.as_ref(), b"best-route");
175    }
176}