use crate::stats::SharedStats;
use log::debug;
use rand::{RngExt, rng};
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadBalancingStrategy {
Random,
#[default]
Fastest,
PowerOfTwo,
}
impl FromStr for LoadBalancingStrategy {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"random" => Ok(LoadBalancingStrategy::Random),
"fastest" => Ok(LoadBalancingStrategy::Fastest),
"p2" | "power-of-two" | "poweroftwo" => Ok(LoadBalancingStrategy::PowerOfTwo),
_ => Err(format!(
"Invalid load balancing strategy: {s}. Valid options are: random, fastest, p2"
)),
}
}
}
impl std::fmt::Display for LoadBalancingStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LoadBalancingStrategy::Random => write!(f, "random"),
LoadBalancingStrategy::Fastest => write!(f, "fastest"),
LoadBalancingStrategy::PowerOfTwo => write!(f, "p2"),
}
}
}
pub async fn select_upstream_server<'a>(
upstream_servers: &'a [String],
strategy: LoadBalancingStrategy,
stats: Option<&Arc<SharedStats>>,
) -> Option<&'a String> {
if upstream_servers.is_empty() {
return None;
}
if upstream_servers.len() == 1 {
return Some(&upstream_servers[0]);
}
match strategy {
LoadBalancingStrategy::Random => {
let index = rng().random_range(0..upstream_servers.len());
Some(&upstream_servers[index])
}
LoadBalancingStrategy::Fastest => {
if stats.is_none() {
debug!("No stats available for fastest strategy, falling back to random");
let index = rng().random_range(0..upstream_servers.len());
return Some(&upstream_servers[index]);
}
let stats = stats.unwrap();
let resolvers_by_speed = stats.get_resolvers_by_speed().await;
if resolvers_by_speed.is_empty() {
debug!("No resolver stats available yet, falling back to random");
let index = rng().random_range(0..upstream_servers.len());
return Some(&upstream_servers[index]);
}
for (addr, _) in resolvers_by_speed {
if let Some(server) = upstream_servers
.iter()
.find(|s| s.parse::<SocketAddr>() == Ok(addr))
{
debug!("Selected fastest server: {server}");
return Some(server);
}
}
debug!("No matching resolver found in stats, falling back to random");
let index = rng().random_range(0..upstream_servers.len());
Some(&upstream_servers[index])
}
LoadBalancingStrategy::PowerOfTwo => {
if stats.is_none() || upstream_servers.len() < 2 {
debug!(
"No stats available for p2 strategy or fewer than 2 servers, falling back to random"
);
let index = rng().random_range(0..upstream_servers.len());
return Some(&upstream_servers[index]);
}
let stats = stats.unwrap();
let resolvers_by_speed = stats.get_resolvers_by_speed().await;
let global_stats = stats.get_stats().await;
if resolvers_by_speed.is_empty() {
debug!("No resolver stats available yet, falling back to random");
let index = rng().random_range(0..upstream_servers.len());
return Some(&upstream_servers[index]);
}
let fastest_time = resolvers_by_speed[0].1;
let threshold = fastest_time * 1.5;
let mut eligible_servers: Vec<&String> = Vec::new();
for server in upstream_servers {
match server.parse::<SocketAddr>() {
Ok(addr) => {
if let Some((_, response_time)) =
resolvers_by_speed.iter().find(|(a, _)| *a == addr)
{
if *response_time <= threshold {
eligible_servers.push(server);
debug!(
"Server {server} with response time {response_time}ms is eligible (threshold: {threshold}ms)"
);
} else {
debug!(
"Server {server} with response time {response_time}ms is too slow (threshold: {threshold}ms)"
);
}
} else {
if global_stats.get_resolver_stats(&addr).is_none() {
eligible_servers.push(server);
debug!("Server {server} has no stats, including it as eligible");
} else {
debug!("Server {server} has no successful probes, excluding it");
}
}
}
Err(_) => {
eligible_servers.push(server);
debug!(
"Failed to parse server address: {server}, including it as eligible"
);
}
}
}
if eligible_servers.is_empty() {
debug!("No eligible servers found, falling back to random selection");
let index = rng().random_range(0..upstream_servers.len());
return Some(&upstream_servers[index]);
}
if eligible_servers.len() == 1 {
debug!("Only one eligible server found: {}", eligible_servers[0]);
return Some(eligible_servers[0]);
}
let mut rng = rng();
let mut indices: Vec<usize> = (0..eligible_servers.len()).collect();
for i in 0..indices.len() {
let j = rng.random_range(i..indices.len());
indices.swap(i, j);
}
let index1 = indices[0];
let index2 = indices[1];
let server1 = eligible_servers[index1];
let server2 = eligible_servers[index2];
debug!("P2 randomly selected servers: {server1} and {server2}");
let addr1 = match server1.parse::<SocketAddr>() {
Ok(addr) => addr,
Err(_) => {
debug!("Failed to parse server address: {server1}");
return Some(server1);
}
};
let addr2 = match server2.parse::<SocketAddr>() {
Ok(addr) => addr,
Err(_) => {
debug!("Failed to parse server address: {server2}");
return Some(server2);
}
};
let stats1 = global_stats.get_resolver_stats(&addr1);
let stats2 = global_stats.get_resolver_stats(&addr2);
match (stats1, stats2) {
(Some(s1), Some(s2)) => {
if s1.avg_response_time_ms <= s2.avg_response_time_ms {
debug!(
"P2 selected server: {} ({}ms)",
server1, s1.avg_response_time_ms
);
Some(server1)
} else {
debug!(
"P2 selected server: {} ({}ms)",
server2, s2.avg_response_time_ms
);
Some(server2)
}
}
(Some(_), None) => {
debug!("P2 selected server: {server1} (no stats for {server2})");
Some(server1)
}
(None, Some(_)) => {
debug!("P2 selected server: {server2} (no stats for {server1})");
Some(server2)
}
(None, None) => {
debug!("No stats for either server, choosing randomly");
let index = rng.random_range(0..2);
if index == 0 {
Some(server1)
} else {
Some(server2)
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stats::SharedStats;
use std::time::Duration;
#[tokio::test]
async fn test_random_strategy() {
let upstream_servers = vec![
"127.0.0.1:53".to_string(),
"127.0.0.2:53".to_string(),
"127.0.0.3:53".to_string(),
];
let server =
select_upstream_server(&upstream_servers, LoadBalancingStrategy::Random, None).await;
assert!(server.is_some());
assert!(upstream_servers.contains(server.unwrap()));
}
#[tokio::test]
async fn test_fastest_strategy() {
let upstream_servers = vec![
"127.0.0.1:53".to_string(),
"127.0.0.2:53".to_string(),
"127.0.0.3:53".to_string(),
];
let stats = Arc::new(SharedStats::new());
let addr1 = "127.0.0.1:53".parse::<SocketAddr>().unwrap();
let addr2 = "127.0.0.2:53".parse::<SocketAddr>().unwrap();
let addr3 = "127.0.0.3:53".parse::<SocketAddr>().unwrap();
stats.record_success(addr1, Duration::from_millis(50)).await;
stats.record_success(addr2, Duration::from_millis(10)).await; stats.record_success(addr3, Duration::from_millis(30)).await;
let server = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::Fastest,
Some(&stats),
)
.await;
assert!(server.is_some());
assert_eq!(server.unwrap(), &"127.0.0.2:53".to_string());
}
#[tokio::test]
async fn test_failed_only_resolver_is_not_fastest() {
let upstream_servers = vec!["127.0.0.1:53".to_string(), "127.0.0.2:53".to_string()];
let stats = Arc::new(SharedStats::new());
let failed = upstream_servers[0].parse::<SocketAddr>().unwrap();
let healthy = upstream_servers[1].parse::<SocketAddr>().unwrap();
stats.record_failure(failed).await;
stats
.record_success(healthy, Duration::from_millis(10))
.await;
let fastest = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::Fastest,
Some(&stats),
)
.await;
let p2 = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::PowerOfTwo,
Some(&stats),
)
.await;
assert_eq!(fastest, Some(&upstream_servers[1]));
assert_eq!(p2, Some(&upstream_servers[1]));
}
#[tokio::test]
async fn test_fastest_strategy_matches_non_canonical_addresses() {
let upstream_servers = vec![
"[2001:DB8::1]:53".to_string(),
"[2001:db8::2]:53".to_string(),
];
let stats = Arc::new(SharedStats::new());
let fast = upstream_servers[0].parse::<SocketAddr>().unwrap();
let slow = upstream_servers[1].parse::<SocketAddr>().unwrap();
stats.record_success(fast, Duration::from_millis(10)).await;
stats.record_success(slow, Duration::from_millis(50)).await;
let server = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::Fastest,
Some(&stats),
)
.await;
assert_eq!(server, Some(&upstream_servers[0]));
}
#[tokio::test]
async fn test_p2_strategy() {
let _ = env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.is_test(true)
.try_init();
let upstream_servers = vec![
"127.0.0.1:53".to_string(), "127.0.0.2:53".to_string(), "127.0.0.3:53".to_string(), "127.0.0.4:53".to_string(), ];
let stats = Arc::new(SharedStats::new());
let addr1 = "127.0.0.1:53".parse::<SocketAddr>().unwrap();
let addr2 = "127.0.0.2:53".parse::<SocketAddr>().unwrap();
let addr3 = "127.0.0.3:53".parse::<SocketAddr>().unwrap();
let addr4 = "127.0.0.4:53".parse::<SocketAddr>().unwrap();
for _ in 0..10 {
stats.record_success(addr1, Duration::from_millis(50)).await;
stats.record_success(addr2, Duration::from_millis(10)).await; stats.record_success(addr3, Duration::from_millis(30)).await;
stats.record_success(addr4, Duration::from_millis(14)).await; }
let resolvers_by_speed = stats.get_resolvers_by_speed().await;
println!("Resolvers by speed:");
for (addr, time) in &resolvers_by_speed {
println!(" {} -> {}ms", addr, time);
}
let fastest_time = resolvers_by_speed[0].1;
let threshold = fastest_time * 1.5;
println!(
"Fastest time: {}ms, Threshold: {}ms",
fastest_time, threshold
);
struct MockLoadBalancer;
impl MockLoadBalancer {
async fn get_eligible_servers<'a>(
upstream_servers: &'a [String],
stats: &Arc<SharedStats>,
) -> Vec<&'a String> {
let resolvers_by_speed = stats.get_resolvers_by_speed().await;
if resolvers_by_speed.is_empty() {
return Vec::new();
}
let fastest_time = resolvers_by_speed[0].1;
let threshold = fastest_time * 1.5;
let mut eligible_servers: Vec<&String> = Vec::new();
for server in upstream_servers {
match server.parse::<SocketAddr>() {
Ok(addr) => {
if let Some((_, response_time)) =
resolvers_by_speed.iter().find(|(a, _)| *a == addr)
{
if *response_time <= threshold {
eligible_servers.push(server);
}
} else {
eligible_servers.push(server);
}
}
Err(_) => {
eligible_servers.push(server);
}
}
}
eligible_servers
}
}
let eligible_servers =
MockLoadBalancer::get_eligible_servers(&upstream_servers, &stats).await;
println!("Eligible servers:");
for server in &eligible_servers {
println!(" {}", server);
}
assert_eq!(eligible_servers.len(), 2);
assert!(eligible_servers.contains(&&"127.0.0.2:53".to_string()));
assert!(eligible_servers.contains(&&"127.0.0.4:53".to_string()));
let iterations = 1000; let mut selected_servers = std::collections::HashMap::new();
for _ in 0..iterations {
let server = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::PowerOfTwo,
Some(&stats),
)
.await;
assert!(server.is_some());
let selected = server.unwrap();
assert!(
selected == &"127.0.0.2:53".to_string() || selected == &"127.0.0.4:53".to_string()
);
*selected_servers.entry(selected.clone()).or_insert(0) += 1;
}
println!("Server selection counts:");
for (server, count) in &selected_servers {
println!(" {} selected {} times", server, count);
}
assert!(selected_servers.contains_key("127.0.0.2:53"));
}
#[tokio::test]
async fn test_p2_strategy_one_eligible() {
let upstream_servers = vec![
"127.0.0.1:53".to_string(),
"127.0.0.2:53".to_string(),
"127.0.0.3:53".to_string(),
];
let stats = Arc::new(SharedStats::new());
let addr1 = "127.0.0.1:53".parse::<SocketAddr>().unwrap();
let addr2 = "127.0.0.2:53".parse::<SocketAddr>().unwrap();
let addr3 = "127.0.0.3:53".parse::<SocketAddr>().unwrap();
stats.record_success(addr1, Duration::from_millis(60)).await;
stats.record_success(addr2, Duration::from_millis(10)).await; stats.record_success(addr3, Duration::from_millis(80)).await;
for _ in 0..10 {
let server = select_upstream_server(
&upstream_servers,
LoadBalancingStrategy::PowerOfTwo,
Some(&stats),
)
.await;
assert!(server.is_some());
assert_eq!(server.unwrap(), &"127.0.0.2:53".to_string());
}
}
#[tokio::test]
async fn test_from_str() {
assert_eq!(
LoadBalancingStrategy::from_str("random").unwrap(),
LoadBalancingStrategy::Random
);
assert_eq!(
LoadBalancingStrategy::from_str("fastest").unwrap(),
LoadBalancingStrategy::Fastest
);
assert_eq!(
LoadBalancingStrategy::from_str("p2").unwrap(),
LoadBalancingStrategy::PowerOfTwo
);
assert_eq!(
LoadBalancingStrategy::from_str("power-of-two").unwrap(),
LoadBalancingStrategy::PowerOfTwo
);
assert_eq!(
LoadBalancingStrategy::from_str("poweroftwo").unwrap(),
LoadBalancingStrategy::PowerOfTwo
);
assert!(LoadBalancingStrategy::from_str("invalid").is_err());
}
#[test]
fn test_default() {
assert_eq!(
LoadBalancingStrategy::default(),
LoadBalancingStrategy::Fastest
);
}
}