use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use super::request_peer::CanonicalRequester;
use bacnet_types::error::Error;
use bacnet_types::map_hash::BuildMapHasher;
use tokio::sync::Semaphore;
#[path = "recovery_classifier.rs"]
mod recovery_classifier;
#[cfg(test)]
#[path = "request_admission_slot_tests.rs"]
mod slot_tests;
pub(super) use recovery_classifier::confirmed_class;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RequestAdmissionPolicy {
pub max_confirmed_in_flight: usize,
pub max_unconfirmed_in_flight: usize,
pub max_confirmed_in_flight_per_peer: usize,
pub max_unconfirmed_in_flight_per_peer: usize,
pub confirmed_recovery_reserve: usize,
pub max_recovery_in_flight_per_peer: usize,
}
impl Default for RequestAdmissionPolicy {
fn default() -> Self {
Self {
max_confirmed_in_flight: 64,
max_unconfirmed_in_flight: 32,
max_confirmed_in_flight_per_peer: 16,
max_unconfirmed_in_flight_per_peer: 8,
confirmed_recovery_reserve: 4,
max_recovery_in_flight_per_peer: 1,
}
}
}
impl RequestAdmissionPolicy {
pub fn validate(&self) -> Result<(), Error> {
for (name, value) in [
("max_confirmed_in_flight", self.max_confirmed_in_flight),
("max_unconfirmed_in_flight", self.max_unconfirmed_in_flight),
(
"max_recovery_in_flight_per_peer",
self.max_recovery_in_flight_per_peer,
),
(
"max_confirmed_in_flight_per_peer",
self.max_confirmed_in_flight_per_peer,
),
(
"max_unconfirmed_in_flight_per_peer",
self.max_unconfirmed_in_flight_per_peer,
),
] {
if value == 0 || value > Semaphore::MAX_PERMITS {
return Err(Error::Encoding(format!(
"{name} must be in 1..={}",
Semaphore::MAX_PERMITS
)));
}
}
if self.confirmed_recovery_reserve >= self.max_confirmed_in_flight {
return Err(Error::Encoding(
"confirmed_recovery_reserve must be smaller than max_confirmed_in_flight".into(),
));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RequestAdmissionCounters {
pub recovery_active: usize,
pub recovery_admitted_total: u64,
pub recovery_overloaded_total: u64,
pub confirmed_active: usize,
pub confirmed_admitted_total: u64,
pub confirmed_overloaded_total: u64,
pub confirmed_global_overloaded_total: u64,
pub confirmed_peer_overloaded_total: u64,
pub confirmed_shutdown_rejected_total: u64,
pub unconfirmed_active: usize,
pub unconfirmed_admitted_total: u64,
pub unconfirmed_overloaded_total: u64,
pub unconfirmed_global_overloaded_total: u64,
pub unconfirmed_peer_overloaded_total: u64,
pub unconfirmed_shutdown_rejected_total: u64,
pub abort_active: usize,
pub abort_admitted_total: u64,
pub confirmed_fallback_dropped_total: u64,
pub abort_shutdown_rejected_total: u64,
}
#[derive(Clone, Copy)]
pub(super) enum Class {
Confirmed = 0,
Unconfirmed = 1,
Abort = 2,
Recovery = 3,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Rejection {
Closed,
Overloaded,
}
pub(super) struct Admission {
pools: [Arc<Pool>; 3],
recovery: Arc<Pool>,
recovery_enabled: bool,
}
struct Pool {
limit: usize,
held: AtomicUsize,
peer_limit: usize,
peers: Mutex<HashMap<CanonicalRequester, usize, BuildMapHasher>>,
active: AtomicUsize,
admitted: AtomicU64,
overloaded: AtomicU64,
global_overloaded: AtomicU64,
peer_overloaded: AtomicU64,
closed: AtomicU64,
}
impl Pool {
fn new(limit: usize, peer_limit: usize) -> Arc<Self> {
Arc::new(Self {
limit,
held: AtomicUsize::new(0),
peer_limit,
peers: Mutex::new(HashMap::default()),
active: AtomicUsize::new(0),
admitted: AtomicU64::new(0),
overloaded: AtomicU64::new(0),
global_overloaded: AtomicU64::new(0),
peer_overloaded: AtomicU64::new(0),
closed: AtomicU64::new(0),
})
}
fn take_slot(&self) -> bool {
let mut held = self.held.load(Ordering::Relaxed);
while held < self.limit {
match self.held.compare_exchange_weak(
held,
held + 1,
Ordering::Acquire,
Ordering::Relaxed,
) {
Ok(_) => return true,
Err(now) => held = now,
}
}
false
}
#[cfg(test)]
fn free_slots(&self) -> usize {
self.limit - self.held.load(Ordering::Relaxed)
}
}
pub(super) struct Guard {
pool: Arc<Pool>,
aggregate: Option<Arc<Pool>>,
peer: Option<CanonicalRequester>,
}
impl Drop for Guard {
fn drop(&mut self) {
if let Some(peer) = &self.peer {
let mut peers = self.pool.peers.lock().unwrap_or_else(|e| e.into_inner());
let count = peers.get_mut(peer).expect("live peer registration");
*count -= 1;
if *count == 0 {
peers.remove(peer);
}
}
for pool in std::iter::once(&self.pool).chain(self.aggregate.iter()) {
pool.active.fetch_sub(1, Ordering::Relaxed);
}
self.pool.held.fetch_sub(1, Ordering::Release);
}
}
impl Admission {
#[cfg(test)]
pub(super) fn peer_entries(&self) -> [usize; 3] {
let mut entries = std::array::from_fn(|i| self.pools[i].peers.lock().unwrap().len());
entries[0] += self.recovery.peers.lock().unwrap().len();
entries
}
pub(super) fn new(policy: RequestAdmissionPolicy) -> Result<Self, Error> {
policy.validate()?;
Ok(Self {
pools: [
Pool::new(
policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve,
policy
.max_confirmed_in_flight_per_peer
.min(policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve),
),
Pool::new(
policy.max_unconfirmed_in_flight,
policy
.max_unconfirmed_in_flight_per_peer
.min(policy.max_unconfirmed_in_flight),
),
Pool::new(8, 8),
],
recovery: Pool::new(
policy.confirmed_recovery_reserve,
policy
.max_recovery_in_flight_per_peer
.min(policy.confirmed_recovery_reserve),
),
recovery_enabled: policy.confirmed_recovery_reserve != 0,
})
}
pub(super) fn try_enter(
&self,
class: Class,
peer: CanonicalRequester,
closed: bool,
) -> Result<Guard, Rejection> {
let (pool, aggregate) = if matches!(class, Class::Recovery) {
if self.recovery_enabled {
(&self.recovery, Some(&self.pools[0]))
} else {
(&self.pools[0], None)
}
} else {
(&self.pools[class as usize], None)
};
if closed {
pool.closed.fetch_add(1, Ordering::Relaxed);
if let Some(total) = aggregate {
total.closed.fetch_add(1, Ordering::Relaxed);
}
return Err(Rejection::Closed);
}
if !pool.take_slot() {
pool.overloaded.fetch_add(1, Ordering::Relaxed);
pool.global_overloaded.fetch_add(1, Ordering::Relaxed);
if let Some(total) = aggregate {
total.overloaded.fetch_add(1, Ordering::Relaxed);
total.global_overloaded.fetch_add(1, Ordering::Relaxed);
}
return Err(Rejection::Overloaded);
}
let peer = if matches!(class, Class::Abort) {
None
} else {
let mut peers = pool.peers.lock().unwrap_or_else(|e| e.into_inner());
let count = peers.entry(peer.clone()).or_insert(0);
if *count >= pool.peer_limit {
if *count == 0 {
peers.remove(&peer);
}
pool.overloaded.fetch_add(1, Ordering::Relaxed);
pool.peer_overloaded.fetch_add(1, Ordering::Relaxed);
if let Some(total) = aggregate {
total.overloaded.fetch_add(1, Ordering::Relaxed);
total.peer_overloaded.fetch_add(1, Ordering::Relaxed);
}
pool.held.fetch_sub(1, Ordering::Release);
return Err(Rejection::Overloaded);
}
*count += 1;
Some(peer)
};
pool.active.fetch_add(1, Ordering::Relaxed);
pool.admitted.fetch_add(1, Ordering::Relaxed);
if let Some(total) = aggregate {
total.active.fetch_add(1, Ordering::Relaxed);
total.admitted.fetch_add(1, Ordering::Relaxed);
}
Ok(Guard {
pool: Arc::clone(pool),
aggregate: aggregate.map(Arc::clone),
peer,
})
}
pub(super) fn snapshot(&self) -> RequestAdmissionCounters {
let [c, u, a] = &self.pools;
RequestAdmissionCounters {
recovery_active: self.recovery.active.load(Ordering::Relaxed),
recovery_admitted_total: self.recovery.admitted.load(Ordering::Relaxed),
recovery_overloaded_total: self.recovery.overloaded.load(Ordering::Relaxed),
confirmed_active: c.active.load(Ordering::Relaxed),
confirmed_admitted_total: c.admitted.load(Ordering::Relaxed),
confirmed_overloaded_total: c.overloaded.load(Ordering::Relaxed),
confirmed_global_overloaded_total: c.global_overloaded.load(Ordering::Relaxed),
confirmed_peer_overloaded_total: c.peer_overloaded.load(Ordering::Relaxed),
confirmed_shutdown_rejected_total: c.closed.load(Ordering::Relaxed),
unconfirmed_active: u.active.load(Ordering::Relaxed),
unconfirmed_admitted_total: u.admitted.load(Ordering::Relaxed),
unconfirmed_overloaded_total: u.overloaded.load(Ordering::Relaxed),
unconfirmed_global_overloaded_total: u.global_overloaded.load(Ordering::Relaxed),
unconfirmed_peer_overloaded_total: u.peer_overloaded.load(Ordering::Relaxed),
unconfirmed_shutdown_rejected_total: u.closed.load(Ordering::Relaxed),
abort_active: a.active.load(Ordering::Relaxed),
abort_admitted_total: a.admitted.load(Ordering::Relaxed),
confirmed_fallback_dropped_total: a.overloaded.load(Ordering::Relaxed),
abort_shutdown_rejected_total: a.closed.load(Ordering::Relaxed),
}
}
}
impl<T: super::TransportPort + 'static> super::ServerBuilder<T> {
pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
self.config.request_admission_policy = policy;
self
}
}
impl super::BipServerBuilder {
pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
self.config.request_admission_policy = policy;
self
}
}
#[cfg(feature = "sc-tls")]
impl super::ScServerBuilder {
pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
self.config.request_admission_policy = policy;
self
}
}
impl<T: super::TransportPort + 'static> super::BACnetServer<T> {
pub fn request_admission_counters(&self) -> RequestAdmissionCounters {
self.request_tasks.counters()
}
}