use std::sync::Arc;
use praxis_filter::FilterPipeline;
use super::metrics;
use crate::http::pingora::context::PingoraRequestCtx;
pub(super) fn record_passive_health(
pipeline: &FilterPipeline,
error: Option<&pingora_core::Error>,
ctx: &PingoraRequestCtx,
) {
let cluster_name = ctx.cluster.as_ref().or(ctx.metrics_cluster.as_ref());
let Some(cluster_name) = cluster_name else {
return;
};
let Some(idx) = ctx.selected_endpoint_index else {
return;
};
let Some(registry) = pipeline.health_registry() else {
return;
};
let Some(health) = registry.get(cluster_name) else {
return;
};
if !ctx.upstream_contacted {
return;
}
let is_downstream_error = error.is_some_and(|e| matches!(e.esource(), pingora_core::ErrorSource::Downstream));
if ended_by_client(error, ctx.upstream_response_status) {
return;
}
let is_failure =
ctx.upstream_response_status.is_some_and(|s| s >= 500) || (error.is_some() && !is_downstream_error);
apply_passive_threshold(health, idx, cluster_name, is_failure);
}
pub(super) fn ended_by_client(error: Option<&pingora_core::Error>, upstream_status: Option<u16>) -> bool {
upstream_status.is_none() && error.is_some_and(|e| matches!(e.esource(), pingora_core::ErrorSource::Downstream))
}
fn apply_passive_threshold(
health: &praxis_core::health::ClusterHealthEntry,
idx: usize,
cluster_name: &Arc<str>,
is_failure: bool,
) {
if is_failure {
if let Some(threshold) = health.passive_unhealthy_threshold()
&& health
.endpoints()
.get(idx)
.is_some_and(|ep| ep.record_failure(threshold))
{
tracing::warn!(
cluster = %cluster_name,
endpoint_index = idx,
threshold,
"passive health: endpoint marked unhealthy"
);
emit_passive_health_transition(health, cluster_name, metrics::HEALTH_RESULT_UNHEALTHY);
}
} else if let Some(threshold) = health.passive_healthy_threshold()
&& health
.endpoints()
.get(idx)
.is_some_and(|ep| ep.record_success(threshold))
{
tracing::info!(
cluster = %cluster_name,
endpoint_index = idx,
threshold,
"passive health: endpoint recovered"
);
emit_passive_health_transition(health, cluster_name, metrics::HEALTH_RESULT_HEALTHY);
}
}
fn emit_passive_health_transition(
health: &praxis_core::health::ClusterHealthEntry,
cluster_name: &Arc<str>,
result: &'static str,
) {
let (healthy, total) = metrics::count_healthy_endpoints(health);
metrics::record_health_transition(
::metrics::SharedString::from(Arc::clone(cluster_name)),
result,
healthy,
total,
);
}
#[cfg(test)]
mod tests {
use pingora_core::{Error, ErrorType};
use super::ended_by_client;
#[test]
fn a_client_abort_before_any_upstream_response_ended_by_client() {
let error = Error::new_down(ErrorType::ConnectionClosed);
assert!(
ended_by_client(Some(&error), None),
"a downstream error with no upstream status must not be charged to the endpoint"
);
}
#[test]
fn upstream_errors_and_answered_requests_are_not_client_endings() {
let upstream = Error::new_up(ErrorType::ConnectionClosed);
let downstream = Error::new_down(ErrorType::ConnectionClosed);
assert!(
!ended_by_client(Some(&upstream), None),
"an upstream error is the endpoint's"
);
assert!(
!ended_by_client(Some(&downstream), Some(502)),
"once the upstream answered, its status is the signal"
);
assert!(!ended_by_client(None, None), "no error is not a client ending");
}
}