v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 20:34:13 +02:00
1 parent 9b3d6731cc
commit 00f29f92c8
68 files changed
+2599 -513

No files matched your search

+19 -2
View File
@@ -2,6 +2,7 @@ impl BackendState {
fn new(mode: HealthMode) -> Self {
let now = Instant::now();
Self {
healthy_cached: AtomicBool::new(mode == HealthMode::Off),
health: Mutex::new(Health {
healthy: mode == HealthMode::Off,
active_healthy: mode == HealthMode::Off,
@@ -111,6 +112,11 @@ 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.
@@ -125,6 +131,12 @@ impl BackendState {
}
h.healthy
}
#[cfg(test)]
fn set_healthy_for_test(&self, healthy: bool) {
lock(&self.health).healthy = healthy;
self.healthy_cached.store(healthy, Ordering::Release);
}
fn clear_passive_ejection(h: &mut Health) {
h.passive_healthy = true;
h.passive_failures = 0;
@@ -195,6 +207,7 @@ impl BackendState {
fn finish_transition(&self, h: &mut Health, previous: bool) {
h.healthy = h.active_healthy && h.passive_healthy;
self.healthy_cached.store(h.healthy, Ordering::Release);
if previous != h.healthy {
h.transitions = h.transitions.saturating_add(1);
if h.healthy {
@@ -446,6 +459,10 @@ 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,
}
#[derive(Debug)]
@@ -461,7 +478,7 @@ pub struct Pool {
#[derive(Debug)]
pub struct Lease {
pub member: Arc<Member>,
health: HealthCheck,
passive_health: PassiveHealth,
health: Arc<HealthCheck>,
passive_health: Arc<PassiveHealth>,
error_recorded: AtomicBool,
}
+3
View File
@@ -386,6 +386,9 @@ 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>,
last_failure: Mutex<FailureObservation>,
+78 -46
View File
@@ -20,10 +20,14 @@ impl Pool {
.cloned()
.zip(backends)
.map(|(definition, backend)| {
let health_settings = Arc::new(settings.health_for(&definition));
let passive_settings = Arc::new(settings.passive_for(&definition));
Arc::new(Member {
state: Arc::new(BackendState::new(settings.health_for(&definition).mode)),
state: Arc::new(BackendState::new(health_settings.mode)),
definition,
backend,
health_settings,
passive_settings,
token: rand::rng()
.sample_iter(Alphanumeric)
.take(32)
@@ -88,7 +92,7 @@ impl Pool {
}
}
fn candidates_excluding(&self, excluded: &[String]) -> Vec<&Arc<Member>> {
let all: Vec<_> = self
let mut eligible: Vec<_> = self
.members
.iter()
.filter(|m| {
@@ -100,10 +104,10 @@ impl Pool {
&& !excluded.iter().any(|id| id == &m.definition.id)
})
.collect();
let primary = all.iter().any(|m| !m.definition.backup);
all.into_iter()
.filter(|m| !primary || !m.definition.backup)
.collect()
if eligible.iter().any(|m| !m.definition.backup) {
eligible.retain(|m| !m.definition.backup);
}
eligible
}
pub fn ready(&self) -> bool {
!self.eligible_healthy_excluding(&[]).is_empty()
@@ -197,8 +201,8 @@ impl Pool {
Some(Selection {
lease: Arc::new(Lease {
member: member.clone(),
health: self.settings.health_for(&member.definition),
passive_health: self.settings.passive_for(&member.definition),
health: member.health_settings.clone(),
passive_health: member.passive_settings.clone(),
error_recorded: AtomicBool::new(false),
}),
cookie: None,
@@ -211,7 +215,32 @@ impl Pool {
tls: bool,
excluded: &[String],
) -> Option<Selection> {
let mut scheduling = lock(&self.scheduling);
// 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
|| !member.definition.enabled
|| member.definition.drain
|| !member.state.healthy()
|| excluded.iter().any(|id| id == &member.definition.id)
|| !member
.state
.try_acquire_active(member.definition.max_concurrent_requests)
{
return None;
}
return Some(Selection {
lease: Arc::new(Lease {
member: member.clone(),
health: member.health_settings.clone(),
passive_health: member.passive_settings.clone(),
error_recorded: AtomicBool::new(false),
}),
cookie: None,
});
}
let members = self.candidates_excluding(excluded);
let existing = if self.cookie_affinity() {
self.affinity(headers)
@@ -237,27 +266,24 @@ impl Pool {
let selected = if let Some(member) = matched {
member
} else {
let weights: Vec<f64> = members
.iter()
.map(|m| {
m.state
.weight(m.definition.weight, self.settings.slow_start_seconds)
})
.collect();
if self.settings.algorithm == Algorithm::RoundRobin
&& self.settings.sticky.mode != StickyMode::IpHash
{
// Smooth weighted round-robin: distribute weighted requests,
// rather than sending long consecutive bursts to one member.
// 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;
let mut total = 0.0;
for (member, weight) in members.iter().zip(&weights) {
for member in &members {
let weight = member
.state
.weight(member.definition.weight, self.settings.slow_start_seconds);
let current = scheduling
.entry(member.definition.id.clone())
.or_insert(0.0);
*current += *weight;
total += *weight;
*current += weight;
total += weight;
if *current > best {
best = *current;
chosen = *member;
@@ -270,43 +296,49 @@ impl Pool {
} else {
let mut chosen = members[(cursor as usize) % members.len()];
let mut best = f64::INFINITY;
let algorithm = if self.settings.sticky.mode == StickyMode::IpHash {
Algorithm::IpHash
} 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() {
let index = ((cursor as usize) % members.len() + offset) % members.len();
let member = members[index];
let active = member.state.active.load(Ordering::Relaxed) as f64;
let algorithm = if self.settings.sticky.mode == StickyMode::IpHash {
Algorithm::IpHash
} else {
self.settings.algorithm
};
let score = match algorithm {
Algorithm::IpHash => {
let digest = Sha256::digest(
format!(
"{}|{}|{}",
self.id,
crate::vhost::canonical_ip(peer),
member.definition.id
)
.as_bytes(),
);
let mut hash = Sha256::new();
hash.update(self.id.as_bytes());
hash.update(b"|");
hash.update(hash_ip.as_deref().expect("hash address").as_bytes());
hash.update(b"|");
hash.update(member.definition.id.as_bytes());
let digest = hash.finalize();
let n = u64::from_be_bytes([
digest[0], digest[1], digest[2], digest[3], digest[4], digest[5],
digest[6], digest[7],
]);
// Weighted rendezvous: stable across requests, no client-controlled forwarding headers.
let unit = (n as f64 + 1.0) / (u64::MAX as f64 + 2.0);
-unit.max(f64::MIN_POSITIVE).ln() / member.definition.weight as f64
}
Algorithm::LeastTraffic => {
(member.state.traffic_rate() + (active + 1.0) * 1024.0) / weights[index]
_ => {
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 =>
((active + 1.0) * member.state.latency()
+ member.state.traffic_rate() / 16_384.0) / weight,
_ => (active + 1.0) / weight,
}
}
Algorithm::Adaptive => {
((active + 1.0) * member.state.latency()
+ member.state.traffic_rate() / 16_384.0)
/ weights[index]
}
_ => (active + 1.0) / weights[index],
};
if score < best {
best = score;
@@ -338,8 +370,8 @@ impl Pool {
Some(Selection {
lease: Arc::new(Lease {
member: selected.clone(),
health: self.settings.health_for(&selected.definition),
passive_health: self.settings.passive_for(&selected.definition),
health: selected.health_settings.clone(),
passive_health: selected.passive_settings.clone(),
error_recorded: AtomicBool::new(false),
}),
cookie,
+43 -5
View File
@@ -533,7 +533,7 @@ mod tests {
headers.insert("cookie", format!("{cookie}; {cookie}").parse().unwrap());
assert!(pool.affinity(&headers).is_none());
headers.insert("cookie", cookie.parse().unwrap());
lock(&first.lease.member.state.health).healthy = false;
first.lease.member.state.set_healthy_for_test(false);
let next = pool
.select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap();
@@ -542,7 +542,7 @@ mod tests {
first.lease.member.definition.id
);
assert!(next.cookie.is_some());
lock(&next.lease.member.state.health).healthy = false;
next.lease.member.state.set_healthy_for_test(false);
assert!(pool
.select("127.0.0.1".parse().unwrap(), &headers, false)
.is_none());
@@ -611,9 +611,9 @@ mod tests {
for _ in 0..8 {
assert_eq!(select(&pool).lease.member.definition.id, "a");
}
lock(&pool.members[0].state.health).healthy = false;
pool.members[0].state.set_healthy_for_test(false);
assert_eq!(select(&pool).lease.member.definition.id, "b");
lock(&pool.members[0].state.health).healthy = true;
pool.members[0].state.set_healthy_for_test(true);
assert_eq!(select(&pool).lease.member.definition.id, "a");
}
#[tokio::test]
@@ -635,7 +635,7 @@ mod tests {
.select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap();
assert_eq!(existing.lease.member.definition.id, "a");
lock(&pool.members[0].state.health).healthy = false;
pool.members[0].state.set_healthy_for_test(false);
assert_eq!(
pool.select("127.0.0.1".parse().unwrap(), &headers, false)
.unwrap()
@@ -764,4 +764,42 @@ mod tests {
assert!(state.healthy());
}
#[test]
fn single_backend_fast_path_observes_capacity_and_health_without_affinity_cookie() {
let pool = capacity_pool(1, 0);
assert_eq!(pool.members.len(), 1);
let peer = "127.0.0.1".parse().unwrap();
let headers = HeaderMap::new();
let first = pool.select(peer, &headers, false).expect("healthy backend");
assert!(first.cookie.is_none());
assert!(pool.select(peer, &headers, false).is_none());
drop(first);
pool.members[0].state.set_healthy_for_test(false);
assert!(pool.select(peer, &headers, false).is_none());
pool.members[0].state.set_healthy_for_test(true);
assert!(pool.select(peer, &headers, false).is_some());
}
#[test]
fn leases_reuse_immutable_backend_health_policies() {
let pool = capacity_pool(0, 0);
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));
}
#[test]
fn healthy_fast_path_tracks_active_and_passive_ejections() {
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() };
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());
}
} // mod tests