Skip to main content

loadwise_core/strategy/
least_load.rs

1use super::{SelectionContext, Strategy};
2use crate::LoadMetric;
3
4/// Selects the node with the lowest load score.
5#[derive(Debug)]
6pub struct LeastLoad;
7
8impl LeastLoad {
9    pub fn new() -> Self {
10        Self
11    }
12}
13
14impl Default for LeastLoad {
15    fn default() -> Self {
16        Self
17    }
18}
19
20impl<N: LoadMetric> Strategy<N> for LeastLoad {
21    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
22        if candidates.is_empty() {
23            return None;
24        }
25
26        let mut best_idx = None;
27        let mut best_score = f64::INFINITY;
28
29        for (i, node) in candidates.iter().enumerate() {
30            if ctx.is_excluded(i) {
31                continue;
32            }
33            let score = node.load_score();
34            if score < best_score {
35                best_score = score;
36                best_idx = Some(i);
37            }
38        }
39
40        best_idx
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    struct L(f64);
49    impl LoadMetric for L {
50        fn load_score(&self) -> f64 {
51            self.0
52        }
53    }
54
55    #[test]
56    fn picks_lowest_load() {
57        let ll = LeastLoad::new();
58        let nodes = [L(3.0), L(1.0), L(2.0)];
59        assert_eq!(ll.select(&nodes, &SelectionContext::default()), Some(1));
60    }
61
62    #[test]
63    fn picks_first_on_tie() {
64        let ll = LeastLoad::new();
65        let nodes = [L(1.0), L(1.0), L(2.0)];
66        assert_eq!(ll.select(&nodes, &SelectionContext::default()), Some(0));
67    }
68
69    #[test]
70    fn skips_excluded_picks_next_best() {
71        let ll = LeastLoad::new();
72        let nodes = [L(1.0), L(2.0), L(3.0)];
73        // Exclude the best node (index 0)
74        let ctx = SelectionContext::builder().exclude(vec![0]).build();
75        assert_eq!(ll.select(&nodes, &ctx), Some(1));
76    }
77}