use std::sync::atomic::AtomicU64;
use axum::http::StatusCode;
use tracing::{info, warn};
pub(crate) static NEXT_PROXY_REQUEST_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone)]
pub(crate) struct UpstreamTarget {
pub(crate) name: String,
pub(crate) url: String,
pub(crate) api_key: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActiveRoute {
Primary,
Backup,
}
#[derive(Debug, Clone)]
pub(crate) struct UpstreamRouter {
pub(crate) primary: UpstreamTarget,
backup: Option<UpstreamTarget>,
active: ActiveRoute,
primary_healthy: bool,
cooldown_remaining: u64,
connection_errors: u32,
}
const CONNECTION_ERROR_THRESHOLD: u32 = 3;
const COOLDOWN_MINUTES: u64 = 5;
impl UpstreamRouter {
pub(crate) fn new(primary: UpstreamTarget, backup: Option<UpstreamTarget>) -> Self {
let primary_healthy = backup.is_some(); Self {
primary,
backup,
active: ActiveRoute::Primary,
primary_healthy,
cooldown_remaining: 0,
connection_errors: 0,
}
}
pub(crate) fn active_target(&self) -> &UpstreamTarget {
match self.active {
ActiveRoute::Primary => &self.primary,
ActiveRoute::Backup => self.backup.as_ref().unwrap_or(&self.primary),
}
}
pub(crate) fn record_response_status(&mut self, status: StatusCode) {
self.connection_errors = 0;
if status == StatusCode::TOO_MANY_REQUESTS
&& self.backup.is_some()
&& self.active == ActiveRoute::Primary
{
warn!("primary upstream returned 429 — failing over to backup");
self.active = ActiveRoute::Backup;
self.primary_healthy = false;
self.cooldown_remaining = COOLDOWN_MINUTES;
}
}
pub(crate) fn mark_primary_healthy(&mut self) {
if self.backup.is_none() {
return;
}
if self.cooldown_remaining > 0 {
return;
}
if !self.primary_healthy {
info!("primary health check passed — failing back to primary");
self.primary_healthy = true;
self.active = ActiveRoute::Primary;
}
self.connection_errors = 0;
}
pub(crate) fn mark_primary_unhealthy(&mut self) {
if self.backup.is_none() {
return;
}
if self.active == ActiveRoute::Primary {
warn!("primary health check failed — failing over to backup");
self.active = ActiveRoute::Backup;
self.cooldown_remaining = COOLDOWN_MINUTES;
}
self.primary_healthy = false;
}
pub(crate) fn record_connection_failure(&mut self) {
if self.backup.is_none() {
return;
}
self.connection_errors += 1;
if self.connection_errors >= CONNECTION_ERROR_THRESHOLD
&& self.active == ActiveRoute::Primary
{
warn!(
connection_errors = self.connection_errors,
"primary connection errors exceeded threshold ({CONNECTION_ERROR_THRESHOLD}) — \
failing over to backup"
);
self.active = ActiveRoute::Backup;
self.primary_healthy = false;
self.cooldown_remaining = COOLDOWN_MINUTES;
} else if self.active == ActiveRoute::Primary {
warn!(
connection_errors = self.connection_errors,
"primary connection failed — monitoring (failover at {CONNECTION_ERROR_THRESHOLD} \
errors)"
);
}
}
pub(crate) fn tick_cooldown(&mut self) {
if self.cooldown_remaining > 0 {
self.cooldown_remaining = self.cooldown_remaining.saturating_sub(1);
}
}
#[cfg(test)]
pub(crate) fn active_route(&self) -> ActiveRoute {
self.active
}
#[cfg(test)]
pub(crate) fn is_primary_healthy(&self) -> bool {
self.primary_healthy
}
}