use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use aspero_core::{Error, ProviderId, ResiliencePolicy};
use jiff::Timestamp;
use tokio::time::Instant;
const UNKNOWN_RESET: Duration = Duration::from_secs(3600);
#[derive(Default)]
pub(crate) struct Health {
providers: Mutex<HashMap<ProviderId, State>>,
}
#[derive(Default)]
struct State {
failures: u32,
open_until: Option<Instant>,
probing: bool,
blocked: Option<(Error, Option<Instant>)>,
}
pub(crate) struct Pass {
health: Arc<Health>,
provider: ProviderId,
probe: bool,
recorded: bool,
}
impl Health {
pub(crate) fn admit(
self: &Arc<Self>,
provider: &ProviderId,
now: Timestamp,
) -> Result<Pass, Error> {
let mut providers = self.providers.lock().expect("provider health");
let state = providers.entry(provider.clone()).or_default();
let at = Instant::now();
if let Some((error, until)) = &state.blocked {
match until {
Some(until) if at >= *until => state.blocked = None,
_ => return Err(error.clone()),
}
}
let mut probe = false;
if let Some(until) = state.open_until {
if at < until || state.probing {
return Err(Error::CircuitOpen {
until: Some(wall(now, until.saturating_duration_since(at))),
});
}
state.probing = true;
probe = true;
}
Ok(Pass {
health: self.clone(),
provider: provider.clone(),
probe,
recorded: false,
})
}
pub(crate) fn unavailable(&self, now: Timestamp) -> Vec<(ProviderId, Error)> {
let providers = self.providers.lock().expect("provider health");
let at = Instant::now();
let mut out: Vec<(ProviderId, Error)> = providers
.iter()
.filter_map(|(p, s)| {
if let Some((error, until)) = &s.blocked
&& until.is_none_or(|u| at < u)
{
return Some((p.clone(), error.clone()));
}
let until = s.open_until.filter(|u| at < *u)?;
let until = Some(wall(now, until - at));
Some((p.clone(), Error::CircuitOpen { until }))
})
.collect();
out.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
out
}
}
impl Pass {
pub(crate) fn record(
mut self,
policy: &ResiliencePolicy,
error: Option<&Error>,
now: Timestamp,
) {
self.recorded = true;
let mut providers = self.health.providers.lock().expect("provider health");
let state = providers.entry(self.provider.clone()).or_default();
if self.probe {
state.probing = false;
}
let at = Instant::now();
match error {
Some(e @ Error::AuthRejected { .. }) => state.blocked = Some((e.clone(), None)),
Some(e @ Error::QuotaExhausted { resets_at }) => {
let wait = resets_at
.and_then(|r| Duration::try_from(r.duration_since(now)).ok())
.unwrap_or(UNKNOWN_RESET);
state.blocked = Some((e.clone(), Some(at + wait)));
}
Some(e) if e.trips_breaker() => {
state.failures += 1;
if self.probe || state.failures >= policy.breaker_threshold {
state.open_until = Some(at + policy.breaker_cooldown);
}
}
_ => {
state.failures = 0;
state.open_until = None;
}
}
}
}
impl Drop for Pass {
fn drop(&mut self) {
if self.probe && !self.recorded {
let mut providers = self.health.providers.lock().expect("provider health");
if let Some(state) = providers.get_mut(&self.provider) {
state.probing = false;
}
}
}
}
fn wall(now: Timestamp, after: Duration) -> Timestamp {
jiff::SignedDuration::try_from(after)
.ok()
.and_then(|d| now.checked_add(d).ok())
.unwrap_or(now)
}