Skip to main content

stygian_proxy/strategy/
round_robin.rs

1//! Round-robin proxy rotation strategy.
2
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use async_trait::async_trait;
6
7use crate::error::ProxyResult;
8use crate::strategy::{ProxyCandidate, RotationStrategy, healthy_candidates};
9
10/// Cycles through healthy proxies in order, distributing load evenly.
11///
12/// Uses a lock-free [`AtomicUsize`] counter, so no `Mutex` is needed even
13/// under high concurrency.
14///
15/// # Example
16/// ```
17/// # tokio_test::block_on(async {
18/// use stygian_proxy::strategy::{RoundRobinStrategy, RotationStrategy, ProxyCandidate};
19/// use stygian_proxy::types::ProxyMetrics;
20/// use std::sync::Arc;
21/// use uuid::Uuid;
22///
23/// let strategy = RoundRobinStrategy::default();
24/// let candidates = vec![
25///     ProxyCandidate { id: Uuid::new_v4(), weight: 1, metrics: Arc::new(ProxyMetrics::default()), healthy: true },
26///     ProxyCandidate { id: Uuid::new_v4(), weight: 1, metrics: Arc::new(ProxyMetrics::default()), healthy: true },
27/// ];
28/// let a = strategy.select(&candidates).await.unwrap().id;
29/// let b = strategy.select(&candidates).await.unwrap().id;
30/// assert_ne!(a, b, "round-robin should alternate between two proxies");
31/// # })
32/// ```
33#[derive(Debug, Default)]
34pub struct RoundRobinStrategy {
35    counter: AtomicUsize,
36}
37
38#[async_trait]
39impl RotationStrategy for RoundRobinStrategy {
40    async fn select<'a>(
41        &self,
42        candidates: &'a [ProxyCandidate],
43    ) -> ProxyResult<&'a ProxyCandidate> {
44        use crate::error::ProxyError;
45
46        let healthy = healthy_candidates(candidates);
47        if healthy.is_empty() {
48            return Err(ProxyError::AllProxiesUnhealthy);
49        }
50        let idx = self
51            .counter
52            .fetch_add(1, Ordering::Relaxed)
53            .wrapping_rem(healthy.len());
54        Ok(healthy[idx])
55    }
56}