use super::{BipServerBuilder, ServerBuilder, TransportPort, runtime_clock};
use bacnet_types::error::Error;
use std::sync::Mutex;
use std::time::Instant;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DccDisableRateLimit {
pub capacity: u32,
pub refill_interval_ms: u64,
}
impl Default for DccDisableRateLimit {
fn default() -> Self {
Self {
capacity: 3,
refill_interval_ms: 20_000,
}
}
}
impl DccDisableRateLimit {
pub fn validate(self) -> Result<(), Error> {
if !(1..=65535).contains(&self.capacity)
|| !(1..=86_400_000).contains(&self.refill_interval_ms)
{
return Err(Error::Encoding(
"DCC disable rate requires capacity 1..=65535 and refill_interval_ms 1..=86400000"
.into(),
));
}
Ok(())
}
}
pub(super) struct Bucket {
interval_ns: u128,
maximum: u128,
state: Mutex<State>,
}
struct State {
credit: u128,
last: Instant,
}
impl Bucket {
pub(super) fn new(config: DccDisableRateLimit) -> Result<Self, Error> {
config.validate()?;
let interval_ns = u128::from(config.refill_interval_ms) * 1_000_000;
let maximum = interval_ns * u128::from(config.capacity);
Ok(Self {
interval_ns,
maximum,
state: Mutex::new(State {
credit: maximum,
last: runtime_clock::now(),
}),
})
}
pub(super) fn admit(&self) -> bool {
let mut state = self.state.lock().unwrap();
let now = runtime_clock::now();
state.credit = state
.credit
.saturating_add(now.saturating_duration_since(state.last).as_nanos())
.min(self.maximum);
state.last = now;
if state.credit < self.interval_ns {
return false;
}
state.credit -= self.interval_ns;
true
}
}
impl<T: TransportPort + 'static> ServerBuilder<T> {
pub fn dcc_disable_rate_limit(mut self, limit: Option<DccDisableRateLimit>) -> Self {
self.config.dcc_disable_rate_limit = limit;
self
}
}
impl BipServerBuilder {
pub fn dcc_disable_rate_limit(mut self, limit: Option<DccDisableRateLimit>) -> Self {
self.config.dcc_disable_rate_limit = limit;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Barrier};
use tokio::time::{Duration, advance};
#[tokio::test(start_paused = true)]
async fn dcc_disable_rate_fractional_boundaries_and_idle_cap() {
let bucket = Bucket::new(DccDisableRateLimit::default()).unwrap();
for _ in 0..3 {
assert!(bucket.admit());
}
assert!(!bucket.admit());
for _ in 0..19 {
advance(Duration::from_secs(1)).await;
assert!(!bucket.admit());
}
advance(Duration::from_millis(999)).await;
assert!(!bucket.admit());
advance(Duration::from_micros(999)).await;
assert!(!bucket.admit());
advance(Duration::from_micros(1)).await;
assert!(bucket.admit());
assert!(!bucket.admit());
advance(Duration::from_millis(45_500)).await;
assert!(bucket.admit());
assert!(bucket.admit());
assert!(!bucket.admit());
advance(Duration::from_millis(14_500)).await;
assert!(bucket.admit());
assert!(!bucket.admit());
advance(Duration::from_secs(86400 * 365)).await;
for _ in 0..3 {
assert!(bucket.admit());
}
assert!(!bucket.admit());
}
#[test]
fn dcc_disable_rate_concurrent_global_burst() {
let bucket = Arc::new(
Bucket::new(DccDisableRateLimit {
capacity: 3,
refill_interval_ms: 86_400_000,
})
.unwrap(),
);
let barrier = Arc::new(Barrier::new(32));
let threads: Vec<_> = (0..32)
.map(|_| {
let bucket = bucket.clone();
let barrier = barrier.clone();
std::thread::spawn(move || {
barrier.wait();
bucket.admit()
})
})
.collect();
assert_eq!(
threads
.into_iter()
.map(|t| usize::from(t.join().unwrap()))
.sum::<usize>(),
3
);
assert!(!bucket.admit());
}
}