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

+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,