0.34.6_repo_cleanup

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 09:31:15 +02:00
1 parent ce1a90397c
commit 133d2f0f91
273 files changed
+5969 -6926

No files matched your search

-12
View File
@@ -112,14 +112,10 @@ impl BackendState {
}
}
pub fn healthy(&self) -> bool {
// No lock/clock acquisition for a healthy backend. Only an unhealthy
// backend enters the quarantine/cooldown recovery path below.
if self.healthy_cached.load(Ordering::Acquire) {
return true;
}
let mut h = lock(&self.health);
// Passive-only monitoring has no active probes to recover an ejected
// backend. After a bounded quarantine, allow one new traffic attempt.
if !h.passive_healthy && h.active_healthy && !h.checked {
if h.passive_ejected_at
.is_some_and(|at| at.elapsed() >= h.passive_cooldown)
@@ -173,8 +169,6 @@ impl BackendState {
.saturating_add(self.bytes_out.load(Ordering::Relaxed));
let delta = total.saturating_sub(sample.total);
if delta == 0 {
// No bytes moved in this sampling window: report an actual idle backend
// instead of keeping a decaying EWMA tail that looks like live traffic.
sample.rate = 0.0;
} else {
let rate = delta as f64 / elapsed;
@@ -367,8 +361,6 @@ impl BackendState {
self.observe_passive_with_detail(active, passive, reason, reason, reason, elapsed);
}
/// Returns the first failure in a consecutive counted streak only when
/// this observation actually removes the backend from the traffic pool.
fn observe_passive_with_detail(
&self,
active: &HealthCheck,
@@ -400,8 +392,6 @@ impl BackendState {
if !passive.counts(reason) {
return None;
}
// A fan-out of failures from one expired H2 connection must not count
// as independent failed backend observations in a single instant.
if reason == "io_error" {
let now = Instant::now();
if h.last_io_counted_at
@@ -459,8 +449,6 @@ pub struct Member {
pub backend: Arc<CompiledBackend>,
pub definition: PoolMember,
pub state: Arc<BackendState>,
// Immutable effective per-backend policies are compiled once per pool
// generation, not deep-cloned for every request/lease.
health_settings: Arc<HealthCheck>,
passive_settings: Arc<PassiveHealth>,
token: String,
-14
View File
@@ -76,8 +76,6 @@ impl Lease {
self.member.state.errors.fetch_add(1, Ordering::Relaxed);
}
self.member.state.record_failure(class, detail);
// GOAWAY(NO_ERROR) is a graceful H2 connection shutdown, not an
// origin failure. It can still be surfaced in access diagnostics.
if matches!(class, "h2_graceful_goaway" | "h2_graceful_close") {
return;
}
@@ -368,8 +366,6 @@ async fn probe(member: Arc<Member>, settings: Arc<HealthCheck>) {
tracing::info!(backend = %member.definition.id, healthy = after, reason = result, "backend health changed");
}
}
/// One supervisor, at most 16 probes, at most one outstanding probe per member.
/// DNS workers have their own shared bound and never resolve arbitrary URLs.
pub async fn health_loop(app: Weak<crate::runtime::App>, shutdown: CancellationToken) {
let mut interval = tokio::time::interval(Duration::from_millis(250));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
@@ -383,24 +379,14 @@ pub async fn health_loop(app: Weak<crate::runtime::App>, shutdown: CancellationT
_ = interval.tick() => {
let Some(app) = app.upgrade() else { break; };
let snapshot = app.config.load_full();
// Own an Arc for each candidate. JoinSet tasks require an
// owned, 'static member rather than a reference into snapshot.
// Health settings were already compiled into each member.
// Reuse the immutable Arc instead of cloning each HealthCheck
// and recalculating per-member overrides on every 250ms tick.
let members: Vec<_> = snapshot.pools.iter().flat_map(|pool|
pool.members.iter().map(|member|
(Arc::clone(member), Arc::clone(&member.health_settings)))).collect();
for (member, _) in &members {
member.state.sample_traffic();
// Time-based pool recycling must not depend on receiving a
// request; otherwise idle backends can stay on generation 0
// indefinitely after the configured recycle age elapses.
member.backend.client.maintain_rotation();
member.backend.websocket_client.maintain_rotation();
}
// Rotate the bounded probe window: slow first members must not
// starve later pools when all intervals equal their timeouts.
let start = cursor;
for offset in 0..members.len() {
if checks.len() >= 16 { break; }
-15
View File
@@ -1,4 +1,3 @@
// Core pool model and policies.
use crate::{config::CompiledBackend, vhost::PoolMember};
use anyhow::{ensure, Result};
use axum::body::Body;
@@ -54,15 +53,8 @@ pub struct Keepalive {
pub idle_timeout_seconds: u64,
pub max_idle_per_host: usize,
pub tcp_keepalive_seconds: u64,
/// Rebuild a backend Hyper client after this many requests. The counter is
/// per backend/client generation, not per TCP connection. Zero disables
/// request-count based recycling.
pub pool_recycle_requests: u64,
/// Maximum age of one backend Hyper client generation. Zero disables
/// time-based recycling.
pub pool_recycle_seconds: u64,
/// Bound Hyper's adaptive HTTP/1 read buffer per connection. Hyper requires
/// at least 8192 bytes. This affects upstream HTTP/1 only.
pub http1_max_buffer_bytes: usize,
}
impl Default for Keepalive {
@@ -70,9 +62,6 @@ impl Default for Keepalive {
Self {
enabled: true,
idle_timeout_seconds: 30,
// Retain enough reusable H1 upstream sockets under concurrent
// small-response traffic. The existing 1..=64 bound, idle
// timeout and pool recycling still limit resource retention.
max_idle_per_host: 64,
tcp_keepalive_seconds: 30,
pool_recycle_requests: 25_000,
@@ -219,7 +208,6 @@ impl Default for Settings {
}
}
impl Settings {
/// Resolve active health per member; a disabled monitor never probes.
pub fn health_for(&self, member: &PoolMember) -> HealthCheck {
let mut health = member
.health_check
@@ -233,7 +221,6 @@ impl Settings {
}
health
}
/// Monitoring off also excludes passive ejection; failures remain logged.
pub fn passive_for(&self, member: &PoolMember) -> PassiveHealth {
let mut passive = self.passive_health.clone();
if !member.health_check_enabled {
@@ -389,8 +376,6 @@ impl Default for FailureObservation {
}
#[derive(Debug)]
pub struct BackendState {
// Lock-free healthy lookups on the hot request-selection path. The mutex
// remains authoritative for transitions, probes and quarantine expiry.
healthy_cached: AtomicBool,
health: Mutex<Health>,
traffic: Mutex<Traffic>,
+18 -29
View File
@@ -72,13 +72,8 @@ impl Pool {
.backend
.websocket_client
.reusable_with(&old.backend.websocket_client);
// This pool has not been published or cloned into routes yet.
if let Some(member) = Arc::get_mut(member) {
if same_clients {
// Preserve the live Hyper pools and, with them, generation
// age/request/rotation state across unrelated snapshot
// rebuilds. Changed origins, address policy, keepalive or
// connect timeout still receive newly compiled clients.
member.backend = old.backend.clone();
}
if same_origin
@@ -215,9 +210,6 @@ impl Pool {
tls: bool,
excluded: &[String],
) -> Option<Selection> {
// Most deployments have one healthy backend. For non-cookie modes it
// needs neither candidate Vecs, weight scoring nor the shared scheduler
// mutex. Still use the same atomic capacity lease and health gate.
if self.members.len() == 1 && !self.cookie_affinity() {
let member = &self.members[0];
if !member.definition.pool_eligible
@@ -247,8 +239,6 @@ impl Pool {
} else {
None
};
// Drain stops new assignments while existing cookie-affine clients may
// finish their session. Disabled/unhealthy members never receive traffic.
let matched = existing.and_then(|token| {
self.members.iter().find(|m| {
m.token == token
@@ -269,8 +259,6 @@ impl Pool {
if self.settings.algorithm == Algorithm::RoundRobin
&& self.settings.sticky.mode != StickyMode::IpHash
{
// Smooth weighted round-robin keeps its historical state and
// global ordering. Only that algorithm takes the mutex.
let mut scheduling = lock(&self.scheduling);
let mut chosen = members[0];
let mut best = f64::NEG_INFINITY;
@@ -279,13 +267,14 @@ impl Pool {
let weight = member
.state
.weight(member.definition.weight, self.settings.slow_start_seconds);
let current = if let Some(current) =
scheduling.get_mut(member.definition.id.as_str())
{
current
} else {
scheduling.entry(member.definition.id.clone()).or_insert(0.0)
};
let current =
if let Some(current) = scheduling.get_mut(member.definition.id.as_str()) {
current
} else {
scheduling
.entry(member.definition.id.clone())
.or_insert(0.0)
};
*current += weight;
total += weight;
if *current > best {
@@ -305,8 +294,6 @@ impl Pool {
} else {
self.settings.algorithm
};
// Canonicalize the address once, not for every candidate. The
// SHA-256 input and rendezvous scoring remain bit-for-bit equal.
let hash_ip = (algorithm == Algorithm::IpHash)
.then(|| crate::vhost::canonical_ip(peer).to_string());
for offset in 0..members.len() {
@@ -330,16 +317,18 @@ impl Pool {
-unit.max(f64::MIN_POSITIVE).ln() / member.definition.weight as f64
}
_ => {
let weight = member.state.weight(
member.definition.weight,
self.settings.slow_start_seconds,
);
let weight = member
.state
.weight(member.definition.weight, self.settings.slow_start_seconds);
match algorithm {
Algorithm::LeastTraffic =>
(member.state.traffic_rate() + (active + 1.0) * 1024.0) / weight,
Algorithm::Adaptive =>
Algorithm::LeastTraffic => {
(member.state.traffic_rate() + (active + 1.0) * 1024.0) / weight
}
Algorithm::Adaptive => {
((active + 1.0) * member.state.latency()
+ member.state.traffic_rate() / 16_384.0) / weight,
+ member.state.traffic_rate() / 16_384.0)
/ weight
}
_ => (active + 1.0) / weight,
}
}
+16 -10
View File
@@ -19,10 +19,6 @@ impl Pool {
"down"
};
// Keep this as explicit Map construction instead of one very large
// json! invocation. serde_json's json_internal! macro is recursive
// and the diagnostics payload is large enough to hit rustc's default
// macro recursion limit on supported stable toolchains.
let mut item = serde_json::Map::new();
item.insert("id".into(), json!(m.definition.id));
item.insert("url".into(), json!(m.definition.url));
@@ -85,13 +81,23 @@ impl Pool {
"upstream_client_rotations_total".into(),
json!(client_rotation.rotations),
);
// Negotiated HTTP version from real backend response headers,
// never from the configured Auto/HTTP2 policy or health probes.
let observed = m.backend.client.protocol_stats();
item.insert("upstream_protocol_observed".into(), json!(observed.last_version));
item.insert("upstream_protocol_last_seen_ms".into(), json!(observed.observed_at_ms));
item.insert("upstream_protocol_http1_total".into(), json!(observed.http1_responses));
item.insert("upstream_protocol_http2_total".into(), json!(observed.http2_responses));
item.insert(
"upstream_protocol_observed".into(),
json!(observed.last_version),
);
item.insert(
"upstream_protocol_last_seen_ms".into(),
json!(observed.observed_at_ms),
);
item.insert(
"upstream_protocol_http1_total".into(),
json!(observed.http1_responses),
);
item.insert(
"upstream_protocol_http2_total".into(),
json!(observed.http2_responses),
);
item.insert("latency_ms".into(), json!(h.latency_us as f64 / 1000.0));
item.insert("ewma_latency_ms".into(), json!(h.ewma_us as f64 / 1000.0));
item.insert("last_probe_ms".into(), json!(h.last_probe_ms));
+21 -11
View File
@@ -66,7 +66,7 @@ mod tests {
defaults.health_check_enabled = false;
assert_eq!(settings.health_for(&defaults).mode, HealthMode::Off);
assert!(!settings.passive_for(&defaults).enabled);
assert!(!defaults.pool_eligible); // routing intent is independent
assert!(!defaults.pool_eligible);
defaults.health_check_enabled = true;
defaults.health_check = Some(HealthCheck {
mode: HealthMode::Http,
@@ -105,7 +105,8 @@ mod tests {
assert_eq!(parsed.keepalive.max_idle_per_host, 64);
let explicit: Settings = serde_json::from_value(serde_json::json!({
"keepalive": { "max_idle_per_host": 4 }
})).unwrap();
}))
.unwrap();
assert_eq!(explicit.keepalive.max_idle_per_host, 4);
assert_eq!(parsed.keepalive.pool_recycle_requests, 25_000);
assert_eq!(parsed.keepalive.pool_recycle_seconds, 3_600);
@@ -356,7 +357,7 @@ mod tests {
}
assert_eq!(counts, [30, 10]);
assert_eq!(pool.members[0].state.active.load(Ordering::Relaxed), 0);
assert_eq!(pool.members[0].state.requests.load(Ordering::Relaxed), 0); // Mere cache selection isn't origin traffic.
assert_eq!(pool.members[0].state.requests.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn least_connections_counts_a_lease_until_its_last_owner_drops() {
@@ -769,7 +770,6 @@ mod tests {
assert!(state.healthy());
}
#[test]
fn single_backend_fast_path_observes_capacity_and_health_without_affinity_cookie() {
let pool = capacity_pool(1, 0);
@@ -792,7 +792,10 @@ mod tests {
let first = select(&pool);
let second = select(&pool);
assert!(Arc::ptr_eq(&first.lease.health, &second.lease.health));
assert!(Arc::ptr_eq(&first.lease.passive_health, &second.lease.passive_health));
assert!(Arc::ptr_eq(
&first.lease.passive_health,
&second.lease.passive_health
));
}
#[test]
@@ -800,10 +803,14 @@ mod tests {
let state = BackendState::new(HealthMode::Off);
assert!(state.healthy());
let active = settings(HealthMode::Off);
let passive = PassiveHealth { enabled: true, fall: Some(1), rise: Some(1), ..Default::default() };
let passive = PassiveHealth {
enabled: true,
fall: Some(1),
rise: Some(1),
..Default::default()
};
state.observe_passive(&active, &passive, "connect_error", Duration::from_millis(2));
assert!(!state.healthy());
// Preserve quarantine rules; an active probe is not invented here.
state.observe_passive(&active, &passive, "ok", Duration::from_millis(2));
assert!(state.healthy());
}
@@ -812,14 +819,17 @@ mod tests {
fn successful_lease_response_keeps_passive_observability_and_failure_detail() {
let pool = capacity_pool(0, 0);
let selection = select(&pool);
selection.lease.response(200, Duration::from_millis(5), true);
selection
.lease
.response(200, Duration::from_millis(5), true);
let state = &selection.lease.member.state;
assert_eq!(state.last_status.load(Ordering::Relaxed), 200);
assert_eq!(lock(&state.health).passive_result, "ok");
assert!(lock(&state.health).latency_us > 0);
selection.lease.response(503, Duration::from_millis(5), true);
selection
.lease
.response(503, Duration::from_millis(5), true);
assert_eq!(state.last_status.load(Ordering::Relaxed), 503);
assert_eq!(lock(&state.last_failure).detail, "status=503");
}
} // mod tests
}