use rand::Rng;
use super::{SelectionContext, Strategy};
use crate::LoadMetric;
#[derive(Debug)]
pub struct PowerOfTwoChoices;
impl PowerOfTwoChoices {
pub fn new() -> Self {
Self
}
}
impl Default for PowerOfTwoChoices {
fn default() -> Self {
Self
}
}
impl<N: LoadMetric> Strategy<N> for PowerOfTwoChoices {
fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
let eligible: Vec<usize> = (0..candidates.len())
.filter(|i| !ctx.is_excluded(*i))
.collect();
match eligible.len() {
0 => None,
1 => Some(eligible[0]),
len => {
let mut rng = rand::rng();
let ai = rng.random_range(0..len);
let mut bi = rng.random_range(0..len - 1);
if bi >= ai {
bi += 1;
}
let a = eligible[ai];
let b = eligible[bi];
if candidates[a].load_score() <= candidates[b].load_score() {
Some(a)
} else {
Some(b)
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct L(f64);
impl LoadMetric for L {
fn load_score(&self) -> f64 {
self.0
}
}
#[test]
fn picks_lower_of_two() {
let p2c = PowerOfTwoChoices::new();
let nodes = [L(10.0), L(1.0)];
let ctx = SelectionContext::default();
assert_eq!(p2c.select(&nodes, &ctx), Some(1));
}
#[test]
fn skips_excluded() {
let p2c = PowerOfTwoChoices::new();
let nodes = [L(1.0), L(2.0), L(3.0)];
let ctx = SelectionContext::builder().exclude(vec![0]).build();
let idx = p2c.select(&nodes, &ctx).unwrap();
assert!(idx == 1 || idx == 2);
}
#[test]
fn all_excluded_returns_none() {
let p2c = PowerOfTwoChoices::new();
let nodes = [L(1.0)];
let ctx = SelectionContext::builder().exclude(vec![0]).build();
assert_eq!(p2c.select(&nodes, &ctx), None);
}
}