use dashmap::DashMap;
use http::StatusCode;
use pingap_core::Rate;
use pingora::lb::Backends;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct WindowStats {
pub failure_rate_percent: f64,
pub rps: f64,
pub total_requests: u64,
}
pub struct ConsecutiveCounters {
pub failures: AtomicU32,
pub successes: AtomicU32,
}
impl ConsecutiveCounters {
pub fn new() -> Self {
Self {
failures: AtomicU32::new(0),
successes: AtomicU32::new(0),
}
}
}
pub struct BackendStats {
failure_status_codes: Option<HashSet<StatusCode>>,
interval: Duration,
rate: Rate,
consecutive_counters: DashMap<String, ConsecutiveCounters>,
}
const FAILURE_KEY: &str = "failure";
const TOTAL_KEY: &str = "total";
#[inline]
fn make_key<'a>(prefix: &'static str, address: &'a str) -> Cow<'a, str> {
Cow::Owned(format!("{prefix}-{address}"))
}
impl BackendStats {
pub fn new(interval: Duration, failure_status_codes: Vec<u16>) -> Self {
let failure_status_codes = failure_status_codes
.iter()
.flat_map(|code| StatusCode::from_u16(*code).ok())
.collect::<HashSet<StatusCode>>();
Self {
interval,
rate: Rate::new(interval),
failure_status_codes: if failure_status_codes.is_empty() {
None
} else {
Some(failure_status_codes)
},
consecutive_counters: DashMap::new(),
}
}
pub fn on_transport_failure(&self, address: &str) {
let key = make_key(FAILURE_KEY, address);
self.rate.observe(&key, 1);
}
pub fn on_response(&self, address: &str, status: StatusCode) -> bool {
let total_key = make_key(TOTAL_KEY, address);
self.rate.observe(&total_key, 1);
let is_request_failure =
self.failure_status_codes.as_ref().map_or_else(
|| status.is_server_error(),
|codes| codes.contains(&status),
);
let counters = self
.consecutive_counters
.entry(address.to_string())
.or_insert_with(ConsecutiveCounters::new);
if is_request_failure {
counters.successes.store(0, Ordering::Relaxed);
let failure_key = make_key(FAILURE_KEY, address);
self.rate.observe(&failure_key, 1);
counters.failures.fetch_add(1, Ordering::Relaxed);
} else {
counters.successes.fetch_add(1, Ordering::Relaxed);
counters.failures.store(0, Ordering::Relaxed);
}
is_request_failure
}
#[inline]
pub(crate) fn get_window_stats(&self, address: &str) -> WindowStats {
let interval = self.interval.as_secs_f64();
let total_key = make_key(TOTAL_KEY, address);
let failure_key = make_key(FAILURE_KEY, address);
let rps = self.rate.rate(&total_key);
let total = rps * interval;
let failure = self.rate.rate(&failure_key) * interval;
let failure_rate_percent = if total > 0.0 {
(failure / total) * 100.0
} else {
0.0
};
WindowStats {
total_requests: total as u64,
failure_rate_percent,
rps,
}
}
pub fn get_consecutive_successes(&self, address: &str) -> u32 {
self.consecutive_counters
.get(address)
.map(|entry| entry.successes.load(Ordering::Relaxed))
.unwrap_or(0)
}
pub fn get_consecutive_failures(&self, address: &str) -> u32 {
self.consecutive_counters
.get(address)
.map(|entry| entry.failures.load(Ordering::Relaxed))
.unwrap_or(0)
}
pub fn get_all_stats(
&self,
backends: &Backends,
) -> HashMap<String, WindowStats> {
let backends = backends
.get_backend()
.iter()
.map(|backend| backend.addr.to_string())
.collect::<HashSet<String>>();
self.consecutive_counters
.retain(|key, _| backends.contains(key));
backends
.into_iter()
.map(|address| {
let stats = self.get_window_stats(&address);
(address, stats)
})
.collect()
}
}