use std::sync::atomic::{AtomicUsize, Ordering};
use rustlavel_core::Result;
use crate::instance::Instance;
pub trait Resolve: Send + Sync {
fn instances<'a>(
&'a self,
service: &'a str,
) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<Instance>>> + Send + 'a>>;
}
pub struct Static {
instances: Vec<Instance>,
}
impl Static {
pub fn new(instances: Vec<Instance>) -> Static {
Static { instances }
}
}
impl Resolve for Static {
fn instances<'a>(
&'a self,
service: &'a str,
) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<Instance>>> + Send + 'a>> {
let service = service.to_ascii_uppercase();
Box::pin(async move {
Ok(self.instances.iter().filter(|held| held.service == service).cloned().collect())
})
}
}
pub struct Dns {
suffix: String,
port: u16,
}
impl Dns {
pub fn new(suffix: impl Into<String>, port: u16) -> Dns {
Dns { suffix: suffix.into(), port }
}
fn host_for(&self, service: &str) -> String {
format!("{}{}", service.to_ascii_lowercase(), self.suffix)
}
}
impl Resolve for Dns {
fn instances<'a>(
&'a self,
service: &'a str,
) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<Instance>>> + Send + 'a>> {
let host = self.host_for(service);
let port = self.port;
let service = service.to_string();
Box::pin(async move {
Ok(vec![Instance::new(service, host, port)])
})
}
}
pub trait Balance: Send + Sync {
fn pick<'a>(&self, instances: &'a [Instance]) -> Option<&'a Instance>;
}
#[derive(Default)]
pub struct RoundRobin {
next: AtomicUsize,
}
impl RoundRobin {
pub fn new() -> RoundRobin {
RoundRobin::default()
}
}
impl Balance for RoundRobin {
fn pick<'a>(&self, instances: &'a [Instance]) -> Option<&'a Instance> {
if instances.is_empty() {
return None;
}
let at = self.next.fetch_add(1, Ordering::Relaxed);
instances.get(at % instances.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn instances(count: usize) -> Vec<Instance> {
(0..count).map(|n| Instance::new("orders", format!("10.0.0.{n}"), 8080)).collect()
}
#[tokio::test]
async fn a_static_resolver_answers_only_for_the_service_asked_for() {
let resolver = Static::new(vec![
Instance::new("orders", "10.0.0.1", 8080),
Instance::new("billing", "10.0.0.2", 8080),
]);
assert_eq!(resolver.instances("orders").await.unwrap().len(), 1);
assert_eq!(resolver.instances("ORDERS").await.unwrap().len(), 1);
assert_eq!(resolver.instances("nothing").await.unwrap().len(), 0);
}
#[tokio::test]
async fn dns_resolves_to_the_platform_name_rather_than_to_pods() {
let resolver = Dns::new(".default.svc.cluster.local", 8080);
let found = resolver.instances("orders").await.unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].url(), "http://orders.default.svc.cluster.local:8080");
}
#[test]
fn round_robin_gives_each_instance_a_turn() {
let pool = instances(3);
let balance = RoundRobin::new();
let picked: Vec<&str> =
(0..6).filter_map(|_| balance.pick(&pool)).map(|i| i.host.as_str()).collect();
assert_eq!(picked, ["10.0.0.0", "10.0.0.1", "10.0.0.2", "10.0.0.0", "10.0.0.1", "10.0.0.2"]);
}
#[test]
fn picking_from_nothing_is_nothing() {
assert!(RoundRobin::new().pick(&[]).is_none());
}
#[test]
fn the_counter_wraps_rather_than_saturating() {
let pool = instances(3);
let balance = RoundRobin::new();
balance.next.store(usize::MAX, Ordering::Relaxed);
assert!(balance.pick(&pool).is_some());
assert!(balance.pick(&pool).is_some());
}
}