v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
+19
-2
@@ -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,
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user