loadwise_core/strategy/
p2c.rs1use rand::Rng;
2
3use super::{SelectionContext, Strategy};
4use crate::LoadMetric;
5
6#[derive(Debug)]
15pub struct PowerOfTwoChoices;
16
17impl PowerOfTwoChoices {
18 pub fn new() -> Self {
19 Self
20 }
21}
22
23impl Default for PowerOfTwoChoices {
24 fn default() -> Self {
25 Self
26 }
27}
28
29impl<N: LoadMetric> Strategy<N> for PowerOfTwoChoices {
30 fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
31 let eligible: Vec<usize> = (0..candidates.len())
32 .filter(|i| !ctx.is_excluded(*i))
33 .collect();
34 match eligible.len() {
35 0 => None,
36 1 => Some(eligible[0]),
37 len => {
38 let mut rng = rand::rng();
39 let ai = rng.random_range(0..len);
40 let mut bi = rng.random_range(0..len - 1);
41 if bi >= ai {
42 bi += 1;
43 }
44 let a = eligible[ai];
45 let b = eligible[bi];
46 if candidates[a].load_score() <= candidates[b].load_score() {
47 Some(a)
48 } else {
49 Some(b)
50 }
51 }
52 }
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 struct L(f64);
61 impl LoadMetric for L {
62 fn load_score(&self) -> f64 {
63 self.0
64 }
65 }
66
67 #[test]
68 fn picks_lower_of_two() {
69 let p2c = PowerOfTwoChoices::new();
70 let nodes = [L(10.0), L(1.0)];
72 let ctx = SelectionContext::default();
73 assert_eq!(p2c.select(&nodes, &ctx), Some(1));
74 }
75
76 #[test]
77 fn skips_excluded() {
78 let p2c = PowerOfTwoChoices::new();
79 let nodes = [L(1.0), L(2.0), L(3.0)];
80 let ctx = SelectionContext::builder().exclude(vec![0]).build();
81 let idx = p2c.select(&nodes, &ctx).unwrap();
83 assert!(idx == 1 || idx == 2);
84 }
85
86 #[test]
87 fn all_excluded_returns_none() {
88 let p2c = PowerOfTwoChoices::new();
89 let nodes = [L(1.0)];
90 let ctx = SelectionContext::builder().exclude(vec![0]).build();
91 assert_eq!(p2c.select(&nodes, &ctx), None);
92 }
93}