use std::sync::Arc;
use std::sync::atomic::Ordering;
use super::UpstreamGroup;
#[derive(Debug)]
pub struct RequestPermit {
group: Option<Arc<UpstreamGroup>>,
}
impl Drop for RequestPermit {
fn drop(&mut self) {
if let Some(group) = &self.group {
group.in_flight.fetch_sub(1, Ordering::Relaxed);
}
}
}
impl UpstreamGroup {
pub fn try_acquire(self: &Arc<Self>) -> Option<RequestPermit> {
if self.max_requests == 0 {
return Some(RequestPermit { group: None });
}
let prev = self.in_flight.fetch_add(1, Ordering::Relaxed);
if prev >= self.max_requests {
self.in_flight.fetch_sub(1, Ordering::Relaxed);
return None;
}
Some(RequestPermit {
group: Some(self.clone()),
})
}
}
#[cfg(test)]
mod tests {
use crate::manifest::{
AddressSpec, CircuitBreaker, HealthConfig, LbAlgorithm, OutlierDetection, Upstream,
};
use crate::upstream::UpstreamRegistry;
fn health(healthy_threshold: u32, unhealthy_threshold: u32) -> HealthConfig {
HealthConfig {
path: "/healthz".to_string(),
interval_ms: 100,
timeout_ms: 50,
healthy_threshold,
unhealthy_threshold,
port: None,
}
}
fn upstream(name: &str, addrs: &[&str], h: HealthConfig) -> Upstream {
Upstream {
name: name.to_string(),
addresses: addrs
.iter()
.map(|s| AddressSpec::Bare(s.to_string()))
.collect(),
lb_algorithm: LbAlgorithm::RoundRobin,
hash: None,
tls: None,
resolve_interval_ms: 0,
health: h,
request_timeout_ms: 30_000,
max_retries: 1,
overall_timeout_ms: 0,
circuit_breaker: CircuitBreaker::default(),
outlier_detection: OutlierDetection::default(),
}
}
#[test]
fn circuit_breaker_caps_concurrent_in_flight_and_releases_on_drop() {
let reg = UpstreamRegistry::new();
reg.reconcile(
&[Upstream {
name: "u".to_string(),
addresses: vec![AddressSpec::Bare("a:1".to_string())],
lb_algorithm: LbAlgorithm::RoundRobin,
hash: None,
tls: None,
resolve_interval_ms: 0,
health: health(1, 1),
request_timeout_ms: 30_000,
max_retries: 0,
overall_timeout_ms: 0,
circuit_breaker: CircuitBreaker { max_requests: 2 },
outlier_detection: OutlierDetection::default(),
}],
std::path::Path::new("."),
)
.unwrap();
let g = reg.group("u").unwrap();
let p1 = g.try_acquire().expect("1st slot is under the cap");
let _p2 = g.try_acquire().expect("2nd slot is under the cap");
assert!(
g.try_acquire().is_none(),
"the 3rd concurrent request is over the cap → rejected"
);
drop(p1);
let _p3 = g.try_acquire().expect("a freed slot is reusable");
assert!(
g.try_acquire().is_none(),
"still capped at 2 after reusing the freed slot"
);
}
#[test]
fn circuit_breaker_zero_is_unlimited() {
let reg = UpstreamRegistry::new();
reg.reconcile(
&[upstream("u", &["a:1"], health(1, 1))],
std::path::Path::new("."),
)
.unwrap();
let g = reg.group("u").unwrap();
let permits: Vec<_> = (0..1000)
.map(|_| g.try_acquire().expect("an unlimited breaker never rejects"))
.collect();
assert_eq!(permits.len(), 1000);
}
}