0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user