use std::collections::HashMap;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use super::GeoLocation;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RoutingPolicy {
Nearest,
LowestLatency,
MostCapacity,
Pinned,
RoundRobin,
WeightedRandom,
}
impl Default for RoutingPolicy {
fn default() -> Self {
Self::LowestLatency
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegionRoute {
pub region: String,
pub weight: u32,
pub is_fallback: bool,
pub priority: u32,
}
impl RegionRoute {
pub fn new(region: impl Into<String>) -> Self {
Self {
region: region.into(),
weight: 100,
is_fallback: false,
priority: 0,
}
}
pub fn as_fallback(mut self) -> Self {
self.is_fallback = true;
self.priority = 100;
self
}
pub fn with_weight(mut self, weight: u32) -> Self {
self.weight = weight;
self
}
pub fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
}
pub struct GeoRouter {
locations: RwLock<HashMap<String, GeoLocation>>,
latencies: RwLock<HashMap<String, u32>>,
health: RwLock<HashMap<String, bool>>,
default_policy: RwLock<RoutingPolicy>,
rr_counter: RwLock<usize>,
static_routes: RwLock<HashMap<String, Vec<RegionRoute>>>,
}
impl GeoRouter {
pub fn new() -> Self {
Self {
locations: RwLock::new(HashMap::new()),
latencies: RwLock::new(HashMap::new()),
health: RwLock::new(HashMap::new()),
default_policy: RwLock::new(RoutingPolicy::LowestLatency),
rr_counter: RwLock::new(0),
static_routes: RwLock::new(HashMap::new()),
}
}
pub fn set_default_policy(&self, policy: RoutingPolicy) {
*self.default_policy.write() = policy;
}
pub fn register_location(&self, region: impl Into<String>, location: GeoLocation) {
self.locations.write().insert(region.into(), location);
}
pub fn update_latency(&self, region: impl Into<String>, latency_ms: u32) {
self.latencies.write().insert(region.into(), latency_ms);
}
pub fn update_health(&self, region: impl Into<String>, healthy: bool) {
self.health.write().insert(region.into(), healthy);
}
pub fn add_static_route(&self, service: impl Into<String>, route: RegionRoute) {
let mut routes = self.static_routes.write();
routes.entry(service.into())
.or_insert_with(Vec::new)
.push(route);
}
pub fn route(&self, client_location: Option<&GeoLocation>, policy: Option<RoutingPolicy>) -> Option<String> {
let policy = policy.unwrap_or_else(|| *self.default_policy.read());
let health = self.health.read();
let healthy_regions: Vec<_> = health.iter()
.filter(|(_, &healthy)| healthy)
.map(|(r, _)| r.clone())
.collect();
if healthy_regions.is_empty() {
return None;
}
match policy {
RoutingPolicy::Nearest => {
self.route_nearest(client_location, &healthy_regions)
}
RoutingPolicy::LowestLatency => {
self.route_lowest_latency(&healthy_regions)
}
RoutingPolicy::MostCapacity => {
healthy_regions.first().cloned()
}
RoutingPolicy::RoundRobin => {
self.route_round_robin(&healthy_regions)
}
RoutingPolicy::WeightedRandom => {
self.route_weighted_random(&healthy_regions)
}
RoutingPolicy::Pinned => {
healthy_regions.first().cloned()
}
}
}
pub fn route_service(&self, service: &str, client_location: Option<&GeoLocation>) -> Option<String> {
let routes = self.static_routes.read();
if let Some(service_routes) = routes.get(service) {
let health = self.health.read();
let mut sorted_routes = service_routes.clone();
sorted_routes.sort_by_key(|r| r.priority);
for route in sorted_routes {
if health.get(&route.region).copied().unwrap_or(false) {
return Some(route.region);
}
}
}
self.route(client_location, None)
}
fn route_nearest(&self, client_location: Option<&GeoLocation>, healthy_regions: &[String]) -> Option<String> {
let client_loc = client_location?;
let locations = self.locations.read();
healthy_regions.iter()
.filter_map(|r| {
locations.get(r).map(|loc| (r, client_loc.distance_to(loc)))
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(r, _)| r.clone())
}
fn route_lowest_latency(&self, healthy_regions: &[String]) -> Option<String> {
let latencies = self.latencies.read();
healthy_regions.iter()
.filter_map(|r| latencies.get(r).map(|&lat| (r, lat)))
.min_by_key(|(_, lat)| *lat)
.map(|(r, _)| r.clone())
.or_else(|| healthy_regions.first().cloned())
}
fn route_round_robin(&self, healthy_regions: &[String]) -> Option<String> {
if healthy_regions.is_empty() {
return None;
}
let mut counter = self.rr_counter.write();
let index = *counter % healthy_regions.len();
*counter = counter.wrapping_add(1);
healthy_regions.get(index).cloned()
}
fn route_weighted_random(&self, healthy_regions: &[String]) -> Option<String> {
if healthy_regions.is_empty() {
return None;
}
let latencies = self.latencies.read();
let weights: Vec<_> = healthy_regions.iter()
.map(|r| {
let latency = latencies.get(r).copied().unwrap_or(100);
let weight = 10000 / (latency.max(1) as u64);
(r, weight)
})
.collect();
let total_weight: u64 = weights.iter().map(|(_, w)| w).sum();
if total_weight == 0 {
return healthy_regions.first().cloned();
}
let rand = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos() as u64)
.unwrap_or(0);
let target = rand % total_weight;
let mut cumulative = 0u64;
for (region, weight) in weights {
cumulative += weight;
if cumulative > target {
return Some(region.clone());
}
}
healthy_regions.first().cloned()
}
pub fn get_preferred_regions(&self, client_location: Option<&GeoLocation>) -> Vec<String> {
let health = self.health.read();
let latencies = self.latencies.read();
let locations = self.locations.read();
let mut regions: Vec<_> = health.iter()
.filter(|(_, &healthy)| healthy)
.map(|(r, _)| {
let latency = latencies.get(r).copied().unwrap_or(u32::MAX);
let distance = client_location
.and_then(|cl| locations.get(r).map(|rl| cl.distance_to(rl)))
.unwrap_or(f64::MAX);
(r.clone(), latency, distance)
})
.collect();
regions.sort_by(|a, b| {
a.1.cmp(&b.1)
.then_with(|| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
});
regions.into_iter().map(|(r, _, _)| r).collect()
}
}
impl Default for GeoRouter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_routing_policy() {
let router = GeoRouter::new();
router.update_health("us-east-1", true);
router.update_health("eu-west-1", true);
router.update_latency("us-east-1", 10);
router.update_latency("eu-west-1", 100);
let result = router.route(None, Some(RoutingPolicy::LowestLatency));
assert_eq!(result, Some("us-east-1".to_string()));
}
#[test]
fn test_nearest_routing() {
let router = GeoRouter::new();
let ny = GeoLocation { latitude: 40.7128, longitude: -74.0060 };
let virginia = GeoLocation { latitude: 39.0, longitude: -77.0 };
let ireland = GeoLocation { latitude: 53.0, longitude: -8.0 };
router.register_location("us-east-1", virginia);
router.register_location("eu-west-1", ireland);
router.update_health("us-east-1", true);
router.update_health("eu-west-1", true);
let result = router.route(Some(&ny), Some(RoutingPolicy::Nearest));
assert_eq!(result, Some("us-east-1".to_string()));
}
#[test]
fn test_round_robin() {
let router = GeoRouter::new();
router.update_health("region-1", true);
router.update_health("region-2", true);
router.update_health("region-3", true);
let r1 = router.route(None, Some(RoutingPolicy::RoundRobin));
let r2 = router.route(None, Some(RoutingPolicy::RoundRobin));
let r3 = router.route(None, Some(RoutingPolicy::RoundRobin));
let r4 = router.route(None, Some(RoutingPolicy::RoundRobin));
assert_ne!(r1, r2);
assert_ne!(r2, r3);
assert_eq!(r1, r4); }
#[test]
fn test_static_routes() {
let router = GeoRouter::new();
router.update_health("us-east-1", true);
router.update_health("eu-west-1", true);
router.add_static_route("my-service", RegionRoute::new("eu-west-1").with_priority(0));
router.add_static_route("my-service", RegionRoute::new("us-east-1").as_fallback());
let result = router.route_service("my-service", None);
assert_eq!(result, Some("eu-west-1".to_string()));
}
}