use crate::config::ConfigError;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::{sync::Arc, time::Instant};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LoadBalancerError {
#[error("Rate limit exceeded for endpoint: {0}")]
RateLimited(String),
#[error("Upstream error: {0}")]
UpstreamError(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Bad request: {0}")]
BadRequest(String),
}
impl From<ConfigError> for LoadBalancerError {
fn from(err: ConfigError) -> Self {
LoadBalancerError::ConfigError(err.to_string())
}
}
#[derive(Copy, Clone, Debug)]
pub enum ErrorKind {
Timeout,
ConnectionError,
Http5xx,
Http4xx,
RateLimit,
}
pub enum RequestOutcome {
Success { latency_ms: u64 },
Failure { error_kind: ErrorKind, latency_ms: u64 },
}
#[derive(Debug, Default)]
pub struct EndpointMetrics {
pub ema_latency_ms: AtomicU64,
pub ema_error_penalty_ms: AtomicU64,
pub consecutive_failures: AtomicU32,
pub last_selected: AtomicU64,
}
impl EndpointMetrics {
pub fn update(&self, outcome: &RequestOutcome, latency_smoothing_factor: f64) {
match outcome {
RequestOutcome::Success { latency_ms } => {
self.update_ema(&self.ema_latency_ms, *latency_ms, latency_smoothing_factor);
self.consecutive_failures.store(0, Ordering::Relaxed);
self.update_ema(&self.ema_error_penalty_ms, 0, 0.5); }
RequestOutcome::Failure { error_kind, latency_ms } => {
let penalty = self.calculate_penalty(*error_kind, *latency_ms);
self.update_ema(&self.ema_error_penalty_ms, penalty, 0.8); let _ = self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn update_ema(&self, atomic: &AtomicU64, new_value: u64, alpha: f64) {
let _ = atomic.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current_ema| {
let new_ema = (alpha * new_value as f64) + ((1.0 - alpha) * current_ema as f64);
Some(new_ema.round() as u64)
});
}
fn calculate_penalty(&self, error_kind: ErrorKind, latency_ms: u64) -> u64 {
let base_penalty = match error_kind {
ErrorKind::Timeout => 30_000, ErrorKind::ConnectionError => 20_000, ErrorKind::Http5xx => 15_000, ErrorKind::Http4xx => 5_000, ErrorKind::RateLimit => 10_000, };
base_penalty.max(latency_ms)
}
pub fn get_total_cost(&self) -> u64 {
let success_latency = self.ema_latency_ms.load(Ordering::Relaxed);
let error_penalty = self.ema_error_penalty_ms.load(Ordering::Relaxed);
let raw_cost = success_latency.saturating_add(error_penalty);
raw_cost.max(1000) }
}
#[derive(Debug, Clone)]
pub struct RpcEndpoint {
pub name: String,
pub url: String,
pub healthy: bool,
pub last_check: Instant,
pub cooldown_until: Option<Instant>,
pub cooldown_attempts: u32,
pub rate_limit_per_sec: u32,
pub burst_size: u32,
pub weight: u32,
pub metrics: Arc<EndpointMetrics>,
}
impl RpcEndpoint {
pub fn is_in_cooldown(&self) -> bool {
if let Some(until) = self.cooldown_until {
Instant::now() < until
} else {
false
}
}
pub fn cooldown_remaining_secs(&self) -> i64 {
match self.cooldown_until {
Some(until) => {
let now = Instant::now();
if until > now {
(until.duration_since(now).as_secs()) as i64
} else {
0
}
}
None => 0,
}
}
pub fn is_available(&self) -> bool {
self.healthy && !self.is_in_cooldown()
}
}