use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WorkPriority {
Notification,
Normal,
Control,
Emergency,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderLimits {
pub concurrency: usize,
pub reserved_control_concurrency: usize,
pub tenant_concurrency: usize,
pub failure_threshold: u32,
pub recovery_after: Duration,
pub max_elapsed: Duration,
pub initial_backoff: Duration,
pub max_backoff: Duration,
}
impl ProviderLimits {
pub fn validate(&self) -> Result<(), String> {
if self.concurrency == 0
|| self.tenant_concurrency == 0
|| self.failure_threshold == 0
|| self.recovery_after.is_zero()
|| self.max_elapsed.is_zero()
{
return Err("provider limits must be positive".into());
}
if self.tenant_concurrency > self.concurrency {
return Err("tenant concurrency cannot exceed provider concurrency".into());
}
if self.reserved_control_concurrency >= self.concurrency {
return Err("reserved control concurrency must be below provider concurrency".into());
}
if self.initial_backoff.is_zero() || self.initial_backoff > self.max_backoff {
return Err("provider backoff range is invalid".into());
}
Ok(())
}
pub fn backoff(&self, attempt: u32, jitter_seed: u64) -> Duration {
let factor = 1_u32.checked_shl(attempt.min(20)).unwrap_or(u32::MAX);
let base = self
.initial_backoff
.saturating_mul(factor)
.min(self.max_backoff);
let jitter_ceiling = (base.as_millis() / 4).max(1) as u64;
base.saturating_add(Duration::from_millis(jitter_seed % jitter_ceiling))
.min(self.max_backoff)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderBlock {
CircuitOpen {
retry_after: Duration,
},
Saturated,
}
struct Circuit {
consecutive_failures: u32,
opened_at: Option<Instant>,
probe_in_flight: bool,
}
const TENANT_SWEEP_THRESHOLD: usize = 1_024;
pub struct ProviderGate {
limits: ProviderLimits,
global: Arc<Semaphore>,
normal: Arc<Semaphore>,
tenants: Mutex<HashMap<String, Arc<Semaphore>>>,
circuit: Arc<Mutex<Circuit>>,
}
pub struct ProviderPermit {
_global: OwnedSemaphorePermit,
_normal: Option<OwnedSemaphorePermit>,
_tenant: Option<OwnedSemaphorePermit>,
is_probe: bool,
circuit: Arc<Mutex<Circuit>>,
}
impl Drop for ProviderPermit {
fn drop(&mut self) {
if self.is_probe {
self.circuit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.probe_in_flight = false;
}
}
}
impl ProviderGate {
pub fn new(limits: ProviderLimits) -> Result<Self, String> {
limits.validate()?;
Ok(Self {
global: Arc::new(Semaphore::new(limits.concurrency)),
normal: Arc::new(Semaphore::new(
limits.concurrency - limits.reserved_control_concurrency,
)),
limits,
tenants: Mutex::new(HashMap::new()),
circuit: Arc::new(Mutex::new(Circuit {
consecutive_failures: 0,
opened_at: None,
probe_in_flight: false,
})),
})
}
pub fn try_acquire(
&self,
tenant_id: &str,
priority: WorkPriority,
now: Instant,
) -> Result<ProviderPermit, ProviderBlock> {
let is_probe = {
let mut circuit = self
.circuit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match circuit.opened_at {
None => false,
Some(opened_at) => {
let elapsed = now.saturating_duration_since(opened_at);
if elapsed < self.limits.recovery_after || circuit.probe_in_flight {
return Err(ProviderBlock::CircuitOpen {
retry_after: self.limits.recovery_after.saturating_sub(elapsed),
});
}
circuit.probe_in_flight = true;
true
}
}
};
let normal = if matches!(priority, WorkPriority::Notification | WorkPriority::Normal) {
match self.normal.clone().try_acquire_owned() {
Ok(permit) => Some(permit),
Err(_) => {
self.release_probe(is_probe);
return Err(ProviderBlock::Saturated);
}
}
} else {
None
};
let global = match self.global.clone().try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
self.release_probe(is_probe);
return Err(ProviderBlock::Saturated);
}
};
let tenant = if priority == WorkPriority::Emergency {
None
} else {
let tenant = {
let mut tenants = self
.tenants
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if tenants.len() >= TENANT_SWEEP_THRESHOLD {
let full = self.limits.tenant_concurrency;
tenants.retain(|_, semaphore| semaphore.available_permits() < full);
}
tenants
.entry(tenant_id.to_owned())
.or_insert_with(|| Arc::new(Semaphore::new(self.limits.tenant_concurrency)))
.clone()
};
match tenant.try_acquire_owned() {
Ok(permit) => Some(permit),
Err(_) => {
self.release_probe(is_probe);
return Err(ProviderBlock::Saturated);
}
}
};
Ok(ProviderPermit {
_global: global,
_normal: normal,
_tenant: tenant,
is_probe,
circuit: self.circuit.clone(),
})
}
pub fn record_success(&self, permit: ProviderPermit) {
let mut circuit = self
.circuit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
circuit.consecutive_failures = 0;
circuit.opened_at = None;
drop(circuit);
drop(permit);
}
pub fn record_failure(&self, permit: ProviderPermit, now: Instant) {
let mut circuit = self
.circuit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
circuit.consecutive_failures = circuit.consecutive_failures.saturating_add(1);
if permit.is_probe || circuit.consecutive_failures >= self.limits.failure_threshold {
circuit.opened_at = Some(now);
}
drop(circuit);
drop(permit);
}
pub fn release(&self, permit: ProviderPermit) {
drop(permit);
}
fn release_probe(&self, is_probe: bool) {
if is_probe {
self.circuit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.probe_in_flight = false;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn limits() -> ProviderLimits {
ProviderLimits {
concurrency: 2,
reserved_control_concurrency: 1,
tenant_concurrency: 1,
failure_threshold: 2,
recovery_after: Duration::from_secs(10),
max_elapsed: Duration::from_secs(30),
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(1),
}
}
#[test]
fn tenant_bulkhead_and_circuit_recovery_are_bounded() {
let gate = ProviderGate::new(limits()).unwrap();
let now = Instant::now();
let first = gate.try_acquire("one", WorkPriority::Normal, now).unwrap();
let emergency = gate
.try_acquire("one", WorkPriority::Emergency, now)
.expect("reserved control capacity must remain available");
gate.record_success(emergency);
assert_eq!(
gate.try_acquire("two", WorkPriority::Normal, now).err(),
Some(ProviderBlock::Saturated)
);
gate.record_failure(first, now);
let second = gate.try_acquire("two", WorkPriority::Normal, now).unwrap();
gate.record_failure(second, now);
assert!(matches!(
gate.try_acquire("one", WorkPriority::Normal, now),
Err(ProviderBlock::CircuitOpen { .. })
));
let probe = gate
.try_acquire("one", WorkPriority::Normal, now + Duration::from_secs(10))
.unwrap();
gate.record_success(probe);
assert!(gate
.try_acquire("one", WorkPriority::Normal, now + Duration::from_secs(10))
.is_ok());
}
#[test]
fn dropped_probe_permit_does_not_wedge_the_circuit() {
let gate = ProviderGate::new(limits()).unwrap();
let now = Instant::now();
for tenant in ["one", "two"] {
let permit = gate.try_acquire(tenant, WorkPriority::Normal, now).unwrap();
gate.record_failure(permit, now);
}
let later = now + Duration::from_secs(10);
let probe = gate
.try_acquire("one", WorkPriority::Normal, later)
.unwrap();
assert!(matches!(
gate.try_acquire("two", WorkPriority::Normal, later),
Err(ProviderBlock::CircuitOpen { .. })
));
gate.release(probe);
let probe = gate
.try_acquire("two", WorkPriority::Normal, later)
.expect("released probe must allow the next probe");
gate.record_success(probe);
assert!(gate.try_acquire("one", WorkPriority::Normal, later).is_ok());
}
#[test]
fn tenant_map_is_swept_once_it_grows() {
let gate = ProviderGate::new(ProviderLimits {
concurrency: 4,
tenant_concurrency: 1,
..limits()
})
.unwrap();
let now = Instant::now();
for index in 0..TENANT_SWEEP_THRESHOLD + 5 {
let permit = gate
.try_acquire(&format!("tenant-{index}"), WorkPriority::Normal, now)
.unwrap();
gate.record_success(permit);
}
assert!(gate.tenants.lock().unwrap().len() <= TENANT_SWEEP_THRESHOLD + 1);
}
#[test]
fn backoff_is_capped() {
let limits = limits();
assert_eq!(limits.backoff(30, 99), Duration::from_secs(1));
}
}