scion_stack/path/strategy/
scoring.rs1use std::{collections::BTreeMap, fmt::Display, sync::Arc, time::SystemTime};
25
26use crate::path::types::{PathManagerPath, Score};
27
28pub trait PathScoring: 'static + Send + Sync {
36 fn metric_name(&self) -> &'static str;
39 fn score(&self, path: &PathManagerPath, now: SystemTime) -> Score;
46}
47
48struct PathLengthScorer;
52
53impl PathScoring for PathLengthScorer {
54 fn metric_name(&self) -> &'static str {
55 "Path Length"
56 }
57
58 fn score(&self, path: &PathManagerPath, _now: SystemTime) -> Score {
59 let length = match &path.path.data_plane_path {
60 scion_proto::path::DataPlanePath::EmptyPath => 0,
61 scion_proto::path::DataPlanePath::Standard(encoded_standard_path) => {
62 encoded_standard_path
63 .segments()
64 .map(|seg| seg.hop_fields().len() - 2)
65 .sum()
66 }
67 scion_proto::path::DataPlanePath::Unsupported { .. } => {
68 HOP_COUNT_FOR_MIN_SCORE as usize
69 }
70 };
71
72 const MAX_SCORE: f32 = 1.0;
73 const MIN_SCORE: f32 = 0.0;
74 const HOP_COUNT_FOR_MIN_SCORE: f32 = 50.0;
75 const PER_HOP_PENALTY: f32 = (MAX_SCORE - MIN_SCORE) / HOP_COUNT_FOR_MIN_SCORE;
76 let score_value = MAX_SCORE - (length as f32 * PER_HOP_PENALTY);
77 Score::new_clamped(score_value)
78 }
79}
80
81pub struct PathReliabilityScorer;
85
86impl PathScoring for PathReliabilityScorer {
87 fn metric_name(&self) -> &'static str {
88 "Reliability"
89 }
90
91 fn score(&self, path: &PathManagerPath, now: SystemTime) -> Score {
92 path.reliability.score(now)
93 }
94}
95
96#[derive(Clone)]
98pub struct PathScorer {
99 scorers: Vec<(Arc<dyn PathScoring>, f32)>,
100}
101
102impl Default for PathScorer {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl PathScorer {
109 fn new() -> Self {
110 Self { scorers: vec![] }
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.scorers.is_empty()
116 }
117
118 pub const DEFAULT_RELIABILITY_IMPACT: f32 = 1.0;
120 pub const DEFAULT_LENGTH_IMPACT: f32 = 0.1;
122
123 pub(crate) fn use_default_scorers(&mut self) {
130 self.scorers.push((
131 Arc::new(PathReliabilityScorer),
132 Self::DEFAULT_RELIABILITY_IMPACT,
133 ));
134 self.scorers
135 .push((Arc::new(PathLengthScorer), Self::DEFAULT_LENGTH_IMPACT));
136 }
137
138 pub fn with_scorer(mut self, scorer: impl PathScoring + 'static, impact: f32) -> Self {
147 self.scorers.push((Arc::new(scorer), impact));
148 self
149 }
150
151 pub fn score(&self, path: &PathManagerPath, now: SystemTime) -> f32 {
156 let mut total_score = 0.0;
157 for (scorer, impact) in &self.scorers {
158 let score = scorer.score(path, now).value();
159 total_score += score * impact;
160 }
161 total_score
162 }
163
164 pub fn score_report(&self, path: &PathManagerPath, now: SystemTime) -> ScoreReport {
166 let mut report = ScoreReport::default();
167 for (scorer, impact) in &self.scorers {
168 let score = scorer.score(path, now).value();
169 report.add_score(scorer.metric_name(), score * impact);
170 }
171 report
172 }
173}
174
175#[derive(Default, Debug)]
179pub struct ScoreReport(pub BTreeMap<&'static str, f32>);
180
181impl ScoreReport {
182 fn add_score(&mut self, metric: &'static str, score: f32) {
183 self.0.insert(metric, score);
184 }
185}
186
187impl Display for ScoreReport {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 let total: f32 = self.0.values().sum();
190 for (metric, score) in &self.0 {
191 write!(f, "{}: {:.3} ", metric, score)?;
192 }
193 write!(f, "Total: {:.3}", total)?;
194
195 Ok(())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use std::{
202 cmp::Ordering,
203 hash::{DefaultHasher, Hash, Hasher},
204 net::{IpAddr, Ipv4Addr},
205 };
206
207 use scion_proto::{
208 address::{Asn, EndhostAddr, Isd, IsdAsn},
209 path::{Path, test_builder::TestPathBuilder},
210 };
211
212 use super::*;
213 use crate::path::types::PathManagerPath;
214
215 pub const SRC_ADDR: EndhostAddr = EndhostAddr::new(
216 IsdAsn::new(Isd(1), Asn(1)),
217 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
218 );
219 pub const DST_ADDR: EndhostAddr = EndhostAddr::new(
220 IsdAsn::new(Isd(2), Asn(1)),
221 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)),
222 );
223
224 pub fn path(hop_count: u16, timestamp: u32, exp_units: u8, asn_seed: u32) -> Path {
225 let mut builder = TestPathBuilder::new(SRC_ADDR, DST_ADDR)
226 .using_info_timestamp(timestamp)
227 .with_hop_expiry(exp_units)
228 .up();
229
230 builder = builder.add_hop(0, 1);
231
232 for cnt in 0..hop_count {
233 let mut hash = DefaultHasher::new();
234 asn_seed.hash(&mut hash);
235 cnt.hash(&mut hash);
236 let hash = hash.finish() as u32;
237
238 builder = builder.with_asn(hash).add_hop(cnt + 1, cnt + 2);
239 }
240
241 builder = builder.add_hop(1, 0);
242
243 builder.build(timestamp).path()
244 }
245
246 struct Simulation {
247 paths: Vec<PathManagerPath>,
248 current_path_index: usize,
249 scoring: PathScorer,
250 actions: Vec<(usize, SimulationAction)>,
252 switch_threshold: f32,
253 }
254
255 enum SimulationAction {
256 UpdateReliability { path_index: usize, score: Score },
257 Evaluate,
258 }
259
260 impl Simulation {
261 fn new(
262 scoring: PathScorer,
263 initial_paths: Vec<PathManagerPath>,
264 switch_threshold: f32,
265 ) -> Self {
266 Self {
267 paths: initial_paths,
268 current_path_index: 0,
269 scoring,
270 actions: vec![],
271 switch_threshold,
272 }
273 }
274
275 fn add_step(self, time: usize, action: SimulationAction) -> Self {
276 let mut sim = self;
277 sim.actions.push((time, action));
278 sim
279 }
280
281 fn run(&mut self) {
282 let actions = std::mem::take(&mut self.actions);
283 const BASE_TIME: SystemTime = SystemTime::UNIX_EPOCH;
284 for (time_delta, action) in actions.into_iter() {
285 println!("(Time +{}s) -------------------", time_delta);
286 let timestamp = BASE_TIME + std::time::Duration::from_secs(time_delta as u64);
287 match action {
288 SimulationAction::UpdateReliability { path_index, score } => {
289 println!(
290 "Updating reliability of path {} to score {:.3}",
291 path_index,
292 score.value(),
293 );
294 let path = &mut self.paths[path_index];
295 path.reliability.update(score, timestamp);
296 self.maybe_switch_path(timestamp);
297 }
298 SimulationAction::Evaluate => {
299 println!("Evaluating paths");
300 self.maybe_switch_path(timestamp);
301 }
302 }
303 println!("-------------------------------");
304 self.print_all(timestamp);
305 println!("-------------------------------");
306 }
307 }
308
309 fn maybe_switch_path(&mut self, now: SystemTime) {
310 let best_path_index = self.best_path_idx(now);
311 let current_path = &self.paths[self.current_path_index];
312 let current_score = self.scoring.score(current_path, now);
313
314 let best_path = &self.paths[best_path_index];
315 let best_score = self.scoring.score(best_path, now);
316
317 let diff = best_score - current_score;
318
319 if best_path_index == self.current_path_index {
320 println!(
321 "Staying on current path {} (score {:.3}) is best path",
322 self.current_path_index, current_score,
323 );
324 return;
325 }
326
327 if diff > self.switch_threshold {
328 println!(
329 "Switching from path {} (score {:.3}) to path {} (score {:.3})",
330 self.current_path_index, current_score, best_path_index, best_score
331 );
332 println!("New path: {}", self.scoring.score_report(best_path, now));
333 println!("Old path: {}", self.scoring.score_report(current_path, now));
334
335 self.current_path_index = best_path_index;
336 } else {
337 println!(
338 "Staying on current path {} (score {:.3}), best path {} (score {:.3}) diff {:.3} below threshold {:.3}",
339 self.current_path_index,
340 current_score,
341 best_path_index,
342 best_score,
343 diff,
344 self.switch_threshold
345 );
346 }
347 }
348
349 fn print_all(&self, now: SystemTime) {
350 let mut sorted = self.paths.iter().enumerate().collect::<Vec<_>>();
351 sorted.sort_by(|(_, a), (_, b)| {
352 let score_a = self.scoring.score(a, now);
353 let score_b = self.scoring.score(b, now);
354 score_b.partial_cmp(&score_a).unwrap_or(Ordering::Equal)
355 });
356
357 for (idx, path) in sorted.iter() {
358 println!("Path {}: {}", idx, self.scoring.score_report(path, now));
359 }
360 }
361
362 fn best_path_idx(&self, now: SystemTime) -> usize {
363 self.paths
364 .iter()
365 .enumerate()
366 .max_by(|(_, a), (_, b)| {
367 let score_a = self.scoring.score(a, now);
368 let score_b = self.scoring.score(b, now);
369 score_a.partial_cmp(&score_b).unwrap_or(Ordering::Equal)
370 })
371 .unwrap()
372 .0
373 }
374 }
375
376 #[test]
377 #[ignore = "Simulation test for manual inspection"]
378 fn simulation() {
379 const SWITCH_THRESHOLD: f32 = 0.4;
382
383 let paths: Vec<_> = (1..=10)
384 .map(|len| {
385 let path = path(len, 1000, 100, len as u32);
386 PathManagerPath::new(path)
387 })
388 .collect();
389
390 let scoring = PathScorer::new()
391 .with_scorer(PathReliabilityScorer, 1.0)
392 .with_scorer(PathLengthScorer, 0.125);
393
394 use SimulationAction::*;
395 Simulation::new(scoring, paths, SWITCH_THRESHOLD)
396 .add_step(0, Evaluate)
397 .add_step(
398 10,
399 UpdateReliability {
400 path_index: 0,
401 score: Score::new_clamped(-0.5),
402 },
403 )
404 .add_step(
405 20,
406 UpdateReliability {
407 path_index: 5,
408 score: Score::new_clamped(0.5),
409 },
410 )
411 .add_step(30, Evaluate)
412 .add_step(60, Evaluate)
413 .add_step(600, Evaluate)
414 .add_step(1200, Evaluate)
415 .run();
416 }
417}