use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use net_io::{Endpoint, Event, EventSink};
use crate::client::IngressKind;
use crate::config::ProfileId;
use crate::stats::Counters;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Health {
Ok,
Degraded(Vec<HealthIssue>),
Failing(Vec<HealthIssue>),
}
impl Health {
#[must_use]
pub fn is_ok(&self) -> bool {
matches!(self, Health::Ok)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HealthIssue {
ListenerAcceptFailing {
listener: String,
},
WorkerStalled {
index: u16,
},
Overloaded {
shed_permille: u16,
},
Draining,
NotifyFailing,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ListenerState {
Bound,
Accepting,
DoorClosed,
Failing,
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct ListenerInfo {
pub name: String,
pub local: Endpoint,
pub ingress: IngressKind,
pub profile: ProfileId,
pub state: ListenerState,
pub public_bind_reason: Option<&'static str>,
}
const MAX_TRACKED: usize = 1024;
type Millis = AtomicU64;
pub(crate) struct HealthState {
started: Instant,
failing_since: Box<[Millis]>,
window: Duration,
memory: Mutex<Memory>,
}
#[derive(Default)]
struct Memory {
progress: Vec<u64>,
requests: u64,
shed: u64,
}
impl HealthState {
pub(crate) fn new(listeners: usize, window: Duration) -> HealthState {
HealthState {
started: Instant::now(),
failing_since: (0..listeners.max(MAX_TRACKED))
.map(|_| AtomicU64::new(0))
.collect(),
window,
memory: Mutex::new(Memory::default()),
}
}
fn now_ms(&self) -> u64 {
(self.started.elapsed().as_millis() as u64).max(2)
}
fn on_event(&self, event: &Event) {
match event {
Event::AcceptError { listener, .. } => {
if let Some(c) = self.failing_since.get(usize::from(listener.get())) {
let _ =
c.compare_exchange(0, self.now_ms(), Ordering::AcqRel, Ordering::Relaxed);
}
}
Event::ListenerFailed { listener, .. } => {
if let Some(c) = self.failing_since.get(usize::from(listener.get())) {
c.store(1, Ordering::Release);
}
}
Event::Accepted { listener, .. } => {
if let Some(c) = self.failing_since.get(usize::from(listener.get())) {
if c.load(Ordering::Relaxed) > 1 {
c.store(0, Ordering::Release);
}
}
}
_ => {}
}
}
pub(crate) fn listener_failing(&self, index: usize) -> bool {
let Some(c) = self.failing_since.get(index) else {
return false;
};
match c.load(Ordering::Acquire) {
0 => false,
1 => true,
since => Duration::from_millis(self.now_ms().saturating_sub(since)) >= self.window,
}
}
pub(crate) fn compute(
&self,
names: &[(usize, String)],
counters: &Counters,
draining: bool,
notify_failing: bool,
) -> Health {
let mut issues = Vec::new();
if notify_failing {
issues.push(HealthIssue::NotifyFailing);
}
let mut failing_listeners = 0;
for (i, name) in names {
if self.listener_failing(*i) {
failing_listeners += 1;
issues.push(HealthIssue::ListenerAcceptFailing {
listener: name.clone(),
});
}
}
let mut memory = self.memory.lock().unwrap_or_else(PoisonError::into_inner);
let now: Vec<(u64, bool)> = counters.workers().collect();
let mut stalled = 0;
if memory.progress.len() == now.len() {
for (i, ((progress, in_call), before)) in now.iter().zip(&memory.progress).enumerate() {
if *in_call && progress == before {
stalled += 1;
issues.push(HealthIssue::WorkerStalled {
index: u16::try_from(i).unwrap_or(u16::MAX),
});
}
}
}
memory.progress = now.iter().map(|(p, _)| *p).collect();
let sum = counters.sum();
let requests = sum.requests.saturating_sub(memory.requests);
let shed = sum.shed.saturating_sub(memory.shed);
memory.requests = sum.requests;
memory.shed = sum.shed;
drop(memory);
if requests > 0 {
let permille = (shed.saturating_mul(1000) / requests.max(shed)).min(1000);
if permille > 200 {
issues.push(HealthIssue::Overloaded {
shed_permille: permille as u16,
});
}
}
if draining {
issues.push(HealthIssue::Draining);
}
let all_workers_stalled = !now.is_empty() && stalled == now.len();
if (!names.is_empty() && failing_listeners == names.len()) || all_workers_stalled {
Health::Failing(issues)
} else if issues.is_empty() {
Health::Ok
} else {
Health::Degraded(issues)
}
}
}
pub(crate) struct Tap<E: EventSink> {
pub(crate) inner: E,
pub(crate) state: Arc<HealthState>,
pub(crate) notifier: Arc<crate::notify::Notifier>,
}
impl<E: EventSink> EventSink for Tap<E> {
#[inline]
fn event(&self, event: &Event) {
self.state.on_event(event);
if let Event::DrainStarted { open } = event {
self.notifier.drain_started(*open);
}
self.inner.event(event);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::RefusalKind;
use crate::stats::Count;
#[test]
fn test_health_ok_then_overloaded_then_stalled() {
let state = HealthState::new(1, Duration::from_secs(30));
let counters = Counters::new(1);
let names = vec![(0, "web".to_owned())];
assert_eq!(state.compute(&names, &counters, false, false), Health::Ok);
for _ in 0..10 {
counters.count(0, Count::Requests);
}
for _ in 0..5 {
counters.count(0, Count::Shed);
counters.count(
0,
Count::Refused {
kind: RefusalKind::Busy,
draining: false,
},
);
}
let h = state.compute(&names, &counters, false, false);
assert!(
matches!(&h, Health::Degraded(i) if matches!(i[0], HealthIssue::Overloaded { shed_permille: 500 })),
"{h:?}"
);
counters.in_call(0, true);
let _ = state.compute(&names, &counters, false, false);
let h = state.compute(&names, &counters, true, false);
assert!(matches!(h, Health::Failing(_)), "{h:?}");
counters.in_call(0, false);
assert_eq!(state.compute(&names, &counters, false, false), Health::Ok);
}
}