Skip to main content

loadwise_core/strategy/
round_robin.rs

1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use super::{SelectionContext, Strategy};
4
5/// Simple round-robin: cycles through candidates sequentially.
6#[derive(Debug)]
7pub struct RoundRobin {
8    counter: AtomicUsize,
9}
10
11impl RoundRobin {
12    pub fn new() -> Self {
13        Self {
14            counter: AtomicUsize::new(0),
15        }
16    }
17}
18
19impl Default for RoundRobin {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl<N> Strategy<N> for RoundRobin {
26    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
27        if candidates.is_empty() {
28            return None;
29        }
30        let len = candidates.len();
31        let start = self.counter.fetch_add(1, Ordering::Relaxed);
32        for i in 0..len {
33            let idx = (start + i) % len;
34            if !ctx.is_excluded(idx) {
35                return Some(idx);
36            }
37        }
38        None
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn cycles_through_candidates() {
48        let rr = RoundRobin::new();
49        let nodes = [1, 2, 3];
50        let ctx = SelectionContext::default();
51
52        assert_eq!(rr.select(&nodes, &ctx), Some(0));
53        assert_eq!(rr.select(&nodes, &ctx), Some(1));
54        assert_eq!(rr.select(&nodes, &ctx), Some(2));
55        assert_eq!(rr.select(&nodes, &ctx), Some(0));
56    }
57
58    #[test]
59    fn empty_returns_none() {
60        let rr = RoundRobin::new();
61        let nodes: [i32; 0] = [];
62        assert_eq!(rr.select(&nodes, &SelectionContext::default()), None);
63    }
64
65    #[test]
66    fn skips_excluded() {
67        let rr = RoundRobin::new();
68        let nodes = [1, 2, 3];
69        let ctx = SelectionContext::builder().exclude(vec![1]).build();
70
71        // counter=0 → start=0, idx=0 not excluded → Some(0)
72        assert_eq!(rr.select(&nodes, &ctx), Some(0));
73        // counter=1 → start=1, idx=1 excluded, idx=2 ok → Some(2)
74        assert_eq!(rr.select(&nodes, &ctx), Some(2));
75        // counter=2 → start=2, idx=2 not excluded → Some(2)
76        assert_eq!(rr.select(&nodes, &ctx), Some(2));
77        // counter=3 → start=0, idx=0 not excluded → Some(0)
78        assert_eq!(rr.select(&nodes, &ctx), Some(0));
79    }
80
81    #[test]
82    fn all_excluded_returns_none() {
83        let rr = RoundRobin::new();
84        let nodes = [1, 2];
85        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
86        assert_eq!(rr.select(&nodes, &ctx), None);
87    }
88}