loadwise-core 0.1.0

Core traits, strategies, and in-memory stores for loadwise
Documentation
use std::sync::atomic::{AtomicUsize, Ordering};

use super::{SelectionContext, Strategy};

/// Simple round-robin: cycles through candidates sequentially.
#[derive(Debug)]
pub struct RoundRobin {
    counter: AtomicUsize,
}

impl RoundRobin {
    pub fn new() -> Self {
        Self {
            counter: AtomicUsize::new(0),
        }
    }
}

impl Default for RoundRobin {
    fn default() -> Self {
        Self::new()
    }
}

impl<N> Strategy<N> for RoundRobin {
    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
        if candidates.is_empty() {
            return None;
        }
        let len = candidates.len();
        let start = self.counter.fetch_add(1, Ordering::Relaxed);
        for i in 0..len {
            let idx = (start + i) % len;
            if !ctx.is_excluded(idx) {
                return Some(idx);
            }
        }
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cycles_through_candidates() {
        let rr = RoundRobin::new();
        let nodes = [1, 2, 3];
        let ctx = SelectionContext::default();

        assert_eq!(rr.select(&nodes, &ctx), Some(0));
        assert_eq!(rr.select(&nodes, &ctx), Some(1));
        assert_eq!(rr.select(&nodes, &ctx), Some(2));
        assert_eq!(rr.select(&nodes, &ctx), Some(0));
    }

    #[test]
    fn empty_returns_none() {
        let rr = RoundRobin::new();
        let nodes: [i32; 0] = [];
        assert_eq!(rr.select(&nodes, &SelectionContext::default()), None);
    }

    #[test]
    fn skips_excluded() {
        let rr = RoundRobin::new();
        let nodes = [1, 2, 3];
        let ctx = SelectionContext::builder().exclude(vec![1]).build();

        // counter=0 → start=0, idx=0 not excluded → Some(0)
        assert_eq!(rr.select(&nodes, &ctx), Some(0));
        // counter=1 → start=1, idx=1 excluded, idx=2 ok → Some(2)
        assert_eq!(rr.select(&nodes, &ctx), Some(2));
        // counter=2 → start=2, idx=2 not excluded → Some(2)
        assert_eq!(rr.select(&nodes, &ctx), Some(2));
        // counter=3 → start=0, idx=0 not excluded → Some(0)
        assert_eq!(rr.select(&nodes, &ctx), Some(0));
    }

    #[test]
    fn all_excluded_returns_none() {
        let rr = RoundRobin::new();
        let nodes = [1, 2];
        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
        assert_eq!(rr.select(&nodes, &ctx), None);
    }
}