use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
use crate::error::ProxyResult;
use crate::types::{CapabilityRequirement, ProxyCapabilities, ProxyMetrics};
mod least_used;
mod random;
mod round_robin;
#[cfg(feature = "bayesian-rotation")]
pub mod thompson;
mod weighted;
pub use least_used::LeastUsedStrategy;
pub use random::RandomStrategy;
pub use round_robin::RoundRobinStrategy;
#[cfg(feature = "bayesian-rotation")]
pub use thompson::ThompsonStrategy;
pub use weighted::WeightedStrategy;
#[derive(Debug, Clone)]
pub struct ProxyCandidate {
pub id: Uuid,
pub weight: u32,
pub metrics: Arc<ProxyMetrics>,
pub healthy: bool,
pub capabilities: ProxyCapabilities,
}
#[async_trait]
pub trait RotationStrategy: Send + Sync + 'static {
async fn select<'a>(&self, candidates: &'a [ProxyCandidate])
-> ProxyResult<&'a ProxyCandidate>;
}
pub type BoxedRotationStrategy = Arc<dyn RotationStrategy>;
pub trait BayesianObserver: Send + Sync + 'static {
fn observe(&self, proxy_id: Uuid, success: bool);
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopBayesianObserver;
impl BayesianObserver for NoopBayesianObserver {
#[inline]
fn observe(&self, _proxy_id: Uuid, _success: bool) {}
}
pub type BoxedBayesianObserver = Arc<dyn BayesianObserver>;
#[must_use]
pub fn healthy_candidates(all: &[ProxyCandidate]) -> Vec<&ProxyCandidate> {
all.iter().filter(|c| c.healthy).collect()
}
#[must_use]
pub fn capable_healthy_candidates<'a>(
all: &'a [ProxyCandidate],
req: &CapabilityRequirement,
) -> Vec<&'a ProxyCandidate> {
all.iter()
.filter(|c| c.healthy && c.capabilities.satisfies(req))
.collect()
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::error::ProxyError;
use crate::types::{CapabilityRequirement, ProxyCapabilities};
use std::sync::atomic::Ordering;
pub fn candidate(id: u128, healthy: bool, weight: u32, requests: u64) -> ProxyCandidate {
let metrics = Arc::new(ProxyMetrics::default());
metrics.requests_total.store(requests, Ordering::Relaxed);
ProxyCandidate {
id: Uuid::from_u128(id),
weight,
metrics,
healthy,
capabilities: ProxyCapabilities::default(),
}
}
pub fn candidate_with_caps(
id: u128,
healthy: bool,
weight: u32,
caps: ProxyCapabilities,
) -> ProxyCandidate {
let metrics = Arc::new(ProxyMetrics::default());
ProxyCandidate {
id: Uuid::from_u128(id),
weight,
metrics,
healthy,
capabilities: caps,
}
}
#[tokio::test]
async fn healthy_candidates_filters() {
let c = vec![
candidate(1, true, 1, 0),
candidate(2, false, 1, 0),
candidate(3, true, 1, 0),
];
let healthy = healthy_candidates(&c);
assert_eq!(healthy.len(), 2);
assert!(healthy.iter().all(|c| c.healthy));
}
#[tokio::test]
async fn all_unhealthy_returns_error() {
let c = vec![candidate(1, false, 1, 0), candidate(2, false, 1, 0)];
assert!(matches!(
RoundRobinStrategy::default().select(&c).await,
Err(ProxyError::AllProxiesUnhealthy)
));
}
#[test]
fn capable_healthy_candidates_filters_by_capability() {
let c = vec![
candidate_with_caps(
1,
true,
1,
ProxyCapabilities {
supports_https_connect: true,
..Default::default()
},
),
candidate_with_caps(2, true, 1, ProxyCapabilities::default()),
candidate_with_caps(
3,
false,
1,
ProxyCapabilities {
supports_https_connect: true,
..Default::default()
},
),
];
let req = CapabilityRequirement {
require_https_connect: true,
..Default::default()
};
let result = capable_healthy_candidates(&c, &req);
assert_eq!(result.len(), 1);
assert_eq!(
result.first().map(|candidate| candidate.id),
Some(Uuid::from_u128(1))
);
}
#[test]
fn capable_healthy_candidates_empty_req_behaves_like_healthy() {
let c = vec![
candidate(1, true, 1, 0),
candidate(2, false, 1, 0),
candidate(3, true, 1, 0),
];
let req = CapabilityRequirement::default();
let result = capable_healthy_candidates(&c, &req);
assert_eq!(result.len(), 2);
}
#[test]
fn capable_healthy_candidates_returns_empty_when_none_match() {
let c = vec![candidate(1, true, 1, 0), candidate(2, true, 1, 0)];
let req = CapabilityRequirement {
require_socks5_udp: true,
..Default::default()
};
let result = capable_healthy_candidates(&c, &req);
assert!(result.is_empty());
}
#[test]
fn geo_country_filter_matches_exact_country() {
let gb_proxy_caps = ProxyCapabilities {
geo_country: Some("GB".into()),
..Default::default()
};
let us_proxy_caps = ProxyCapabilities {
geo_country: Some("US".into()),
..Default::default()
};
let c = vec![
candidate_with_caps(1, true, 1, gb_proxy_caps),
candidate_with_caps(2, true, 1, us_proxy_caps),
candidate_with_caps(3, true, 1, ProxyCapabilities::default()),
];
let req = CapabilityRequirement {
require_geo_country: Some("GB".into()),
..Default::default()
};
let result = capable_healthy_candidates(&c, &req);
assert_eq!(result.len(), 1);
assert_eq!(
result.first().map(|candidate| candidate.id),
Some(Uuid::from_u128(1))
);
}
}