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}