mod background;
mod load_balancer;
pub mod discovery;
pub mod health_check;
pub mod resolver;
pub mod selector;
pub use load_balancer::LoadBalancer;
use crate::discovery::Discovery;
use crate::health_check::HealthCheck;
use crate::health_check::health::Health;
use arc_swap::ArcSwap;
use derivative::Derivative;
use http::Extensions;
use satex_core::Error;
use std::collections::{BTreeSet, HashMap};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::net::{AddrParseError, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use tokio::spawn;
use tracing::{info, warn};
#[derive(Derivative)]
#[derivative(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Debug)]
pub struct Backend {
pub addr: SocketAddr,
pub weight: usize,
#[derivative(PartialEq = "ignore")]
#[derivative(PartialOrd = "ignore")]
#[derivative(Hash = "ignore")]
#[derivative(Ord = "ignore")]
pub extension: Extensions,
}
impl Backend {
pub fn new(addr: impl Into<SocketAddr>) -> Self {
Self::new_with_weight(addr.into(), 1)
}
pub fn new_with_weight(addr: impl Into<SocketAddr>, weight: usize) -> Self {
Self {
addr: addr.into(),
weight,
extension: Extensions::new(),
}
}
pub(crate) fn key(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish()
}
}
impl FromStr for Backend {
type Err = AddrParseError;
fn from_str(addr: &str) -> Result<Self, Self::Err> {
SocketAddr::from_str(addr).map(Backend::new)
}
}
impl<'a> TryFrom<(&'a str, usize)> for Backend {
type Error = AddrParseError;
fn try_from((addr, weight): (&'a str, usize)) -> Result<Self, Self::Error> {
let addr = SocketAddr::from_str(addr)?;
Ok(Backend::new_with_weight(addr, weight))
}
}
pub struct Backends {
discovery: Box<dyn Discovery + Send + Sync>,
health_check: Option<Arc<dyn HealthCheck + Send + Sync>>,
backends: ArcSwap<BTreeSet<Backend>>,
health: ArcSwap<HashMap<u64, Health>>,
}
impl Backends {
pub fn new(discovery: impl Discovery + Send + Sync + 'static) -> Self {
Self {
discovery: Box::new(discovery),
health_check: None,
backends: Default::default(),
health: Default::default(),
}
}
pub(crate) fn with_health_check(
self,
health_check: impl HealthCheck + Send + Sync + 'static,
) -> Self {
Self {
health_check: Some(Arc::new(health_check)),
..self
}
}
fn do_update<F>(
&self,
new_backends: BTreeSet<Backend>,
enablement: HashMap<u64, bool>,
callback: F,
) where
F: Fn(Arc<BTreeSet<Backend>>),
{
if (**self.backends.load()) != new_backends {
let old_health = self.health.load();
let mut new_health = HashMap::with_capacity(new_backends.len());
for backend in new_backends.iter() {
let key = backend.key();
let health = old_health.get(&key).cloned().unwrap_or_default();
if let Some(enabled) = enablement.get(&key) {
health.enable(*enabled);
}
new_health.insert(key, health);
}
let new_backends = Arc::new(new_backends);
callback(new_backends.clone());
self.backends.store(new_backends);
self.health.store(Arc::new(new_health));
} else {
for (key, backend_enabled) in enablement.iter() {
if let Some(health) = self.health.load().get(key) {
health.enable(*backend_enabled);
}
}
}
}
pub fn ready(&self, backend: &Backend) -> bool {
self.health
.load()
.get(&backend.key())
.map_or(self.health_check.is_none(), |h| h.ready())
}
pub fn set_enable(&self, backend: &Backend, enabled: bool) {
if let Some(health) = self.health.load().get(&backend.key()) {
health.enable(enabled)
};
}
pub fn items(&self) -> Arc<BTreeSet<Backend>> {
self.backends.load_full()
}
pub async fn update<F>(&self, callback: F) -> Result<(), Error>
where
F: Fn(Arc<BTreeSet<Backend>>),
{
let (new_backends, enablement) = self.discovery.discover().await?;
self.do_update(new_backends, enablement, callback);
Ok(())
}
pub async fn run_health_check(&self, parallel: bool) {
use crate::health_check::HealthCheck;
async fn check_and_report(
backend: &Backend,
check: &Arc<dyn HealthCheck + Send + Sync>,
health_table: &HashMap<u64, Health>,
) {
let errored = check.check(backend).await.err();
if let Some(health) = health_table.get(&backend.key()) {
let flipped =
health.observe_health(errored.is_none(), check.threshold(errored.is_none()));
if flipped {
check.status_change(backend, errored.is_none()).await;
if let Some(e) = errored {
warn!("{backend:?} becomes unhealthy, {e}");
} else {
info!("{backend:?} becomes healthy");
}
}
}
}
let Some(health_check) = self.health_check.as_ref() else {
return;
};
let backends = self.backends.load();
if parallel {
let health_table = self.health.load_full();
let jobs = backends.iter().map(|backend| {
let backend = backend.clone();
let check = health_check.clone();
let ht = health_table.clone();
spawn(async move {
check_and_report(&backend, &check, &ht).await;
})
});
futures::future::join_all(jobs).await;
} else {
for backend in backends.iter() {
check_and_report(backend, health_check, &self.health.load()).await;
}
}
}
}