Skip to main content

loadwise_core/strategy/
chain.rs

1use super::{SelectionContext, Strategy};
2
3/// Tries the primary strategy first, falls back to secondary if primary returns `None`.
4///
5/// This is a **generic combinator** — zero-cost at runtime because both strategies are
6/// monomorphised. Use [`FallbackChain`] when you need a dynamic list of strategies.
7///
8/// # When does fallback trigger?
9///
10/// A strategy returns `None` when `candidates` is empty **or** all candidates are
11/// excluded via [`SelectionContext::exclude`]. `WithFallback` is most useful when
12/// composed with filtered candidate lists or retry-with-exclude patterns.
13///
14/// # Examples
15///
16/// ```
17/// # extern crate loadwise_core as loadwise;
18/// use loadwise::{Strategy, SelectionContext};
19/// use loadwise::strategy::{Random, RoundRobin, WithFallback};
20///
21/// // Prefer random, but fall back to round-robin if it returns None.
22/// let strategy = WithFallback::new(Random::new(), RoundRobin::new());
23///
24/// let nodes = [1, 2, 3];
25/// let ctx = SelectionContext::default();
26/// assert!(strategy.select(&nodes, &ctx).is_some());
27/// ```
28pub struct WithFallback<P, F> {
29    primary: P,
30    fallback: F,
31}
32
33impl<P: std::fmt::Debug, F: std::fmt::Debug> std::fmt::Debug for WithFallback<P, F> {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.debug_struct("WithFallback")
36            .field("primary", &self.primary)
37            .field("fallback", &self.fallback)
38            .finish()
39    }
40}
41
42impl<P, F> WithFallback<P, F> {
43    pub fn new(primary: P, fallback: F) -> Self {
44        Self { primary, fallback }
45    }
46}
47
48impl<N, P, F> Strategy<N> for WithFallback<P, F>
49where
50    P: Strategy<N>,
51    F: Strategy<N>,
52{
53    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
54        self.primary
55            .select(candidates, ctx)
56            .or_else(|| self.fallback.select(candidates, ctx))
57    }
58}
59
60/// Tries each strategy in order until one returns a result.
61///
62/// Unlike [`WithFallback`] (which is generic and zero-cost), `FallbackChain` uses
63/// trait objects — suitable when the strategy list is built at runtime.
64pub struct FallbackChain<N> {
65    strategies: Vec<Box<dyn Strategy<N> + Send + Sync>>,
66}
67
68impl<N> std::fmt::Debug for FallbackChain<N> {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("FallbackChain")
71            .field("strategies", &self.strategies.len())
72            .finish()
73    }
74}
75
76impl<N> FallbackChain<N> {
77    pub fn new(strategies: Vec<Box<dyn Strategy<N> + Send + Sync>>) -> Self {
78        Self { strategies }
79    }
80}
81
82impl<N> Strategy<N> for FallbackChain<N> {
83    fn select(&self, candidates: &[N], ctx: &SelectionContext) -> Option<usize> {
84        self.strategies
85            .iter()
86            .find_map(|s| s.select(candidates, ctx))
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::strategy::RoundRobin;
94
95    #[test]
96    fn with_fallback_propagates_exclude() {
97        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
98        let nodes = [1, 2];
99        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
100        // Both strategies see the same exclude — both return None
101        assert_eq!(strategy.select(&nodes, &ctx), None);
102    }
103
104    #[test]
105    fn fallback_chain_propagates_exclude() {
106        let chain: FallbackChain<i32> = FallbackChain::new(vec![
107            Box::new(RoundRobin::new()),
108            Box::new(RoundRobin::new()),
109        ]);
110        let nodes = [1, 2];
111        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
112        assert_eq!(chain.select(&nodes, &ctx), None);
113    }
114
115    #[test]
116    fn empty_candidates_returns_none() {
117        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
118        let nodes: [i32; 0] = [];
119        let ctx = SelectionContext::default();
120        assert_eq!(strategy.select(&nodes, &ctx), None);
121    }
122
123    #[test]
124    fn fallback_used_when_primary_excludes_all() {
125        // Primary: RoundRobin on [1, 2] with both excluded → None
126        // Fallback: RoundRobin on same → also None (same ctx)
127        // This verifies fallback IS called even though primary had candidates.
128        let strategy = WithFallback::new(RoundRobin::new(), RoundRobin::new());
129        let nodes = [1, 2];
130        let ctx = SelectionContext::builder().exclude(vec![0, 1]).build();
131        assert_eq!(strategy.select(&nodes, &ctx), None);
132    }
133}