use crate::Condition;
use crate::windowed::{
WindowedMinF32Raw, WindowedMinF64Raw, WindowedMinI32Raw, WindowedMinI64Raw, WindowedMinI128Raw,
};
macro_rules! impl_codel_raw {
($name:ident, $builder:ident, $ty:ty, $windowed_min_raw:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
windowed_min: $windowed_min_raw,
target: $ty,
min_samples: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
target: Option<$ty>,
window: Option<u64>,
min_samples: u64,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
target: Option::None,
window: Option::None,
min_samples: 1,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, timestamp: u64, sojourn: $ty) -> Option<Condition> {
let min = self.windowed_min.update(timestamp, sojourn);
if self.windowed_min.count() < self.min_samples {
return Option::None;
}
if min > self.target {
Option::Some(Condition::Degraded)
} else {
Option::Some(Condition::Normal)
}
}
#[inline]
#[must_use]
pub fn update_i64(&mut self, timestamp: i64, sojourn: $ty) -> Option<Condition> {
debug_assert!(timestamp >= 0, "negative timestamp: {timestamp}");
self.update(timestamp as u64, sojourn)
}
#[inline]
#[must_use]
pub fn min_sojourn(&self) -> Option<$ty> {
self.windowed_min.min()
}
#[inline]
#[must_use]
pub fn is_elevated(&self) -> bool {
if let Some(min) = self.windowed_min.min() {
min > self.target
} else {
false
}
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.windowed_min.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.windowed_min.count() >= self.min_samples
}
#[inline]
pub fn reset(&mut self) {
self.windowed_min.reset();
}
}
impl $builder {
#[inline]
#[must_use]
pub fn target(mut self, target: $ty) -> Self {
self.target = Option::Some(target);
self
}
#[inline]
#[must_use]
pub fn window(mut self, window: u64) -> Self {
self.window = Option::Some(window);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
pub fn build(self) -> Result<$name, crate::ConfigError> {
let target = self.target.ok_or(crate::ConfigError::Missing("target"))?;
let window = self.window.ok_or(crate::ConfigError::Missing("window"))?;
if window == 0 {
return Err(crate::ConfigError::Invalid("CoDel window must be positive"));
}
Ok($name {
windowed_min: <$windowed_min_raw>::new(window)?,
target,
min_samples: self.min_samples,
})
}
}
};
}
impl_codel_raw!(CoDelI64Raw, CoDelI64RawBuilder, i64, WindowedMinI64Raw);
impl_codel_raw!(CoDelI32Raw, CoDelI32RawBuilder, i32, WindowedMinI32Raw);
impl_codel_raw!(CoDelI128Raw, CoDelI128RawBuilder, i128, WindowedMinI128Raw);
impl_codel_raw!(CoDelF64Raw, CoDelF64RawBuilder, f64, WindowedMinF64Raw);
impl_codel_raw!(CoDelF32Raw, CoDelF32RawBuilder, f32, WindowedMinF32Raw);
#[cfg(feature = "std")]
use std::time::{Duration, Instant};
#[cfg(feature = "std")]
use crate::windowed::{
WindowedMinF32, WindowedMinF64, WindowedMinI32, WindowedMinI64, WindowedMinI128,
};
#[cfg(feature = "std")]
macro_rules! impl_codel {
($name:ident, $builder:ident, $ty:ty, $windowed_min:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
windowed_min: $windowed_min,
target: $ty,
min_samples: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
target: Option<$ty>,
window: Option<Duration>,
min_samples: u64,
base: Option<Instant>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
target: Option::None,
window: Option::None,
min_samples: 1,
base: Option::None,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, now: Instant, sojourn: $ty) -> Option<Condition> {
let min = self.windowed_min.update(now, sojourn);
if self.windowed_min.count() < self.min_samples {
return Option::None;
}
if min > self.target {
Option::Some(Condition::Degraded)
} else {
Option::Some(Condition::Normal)
}
}
#[inline]
#[must_use]
pub fn min_sojourn(&self) -> Option<$ty> {
self.windowed_min.min()
}
#[inline]
#[must_use]
pub fn is_elevated(&self) -> bool {
if let Some(min) = self.windowed_min.min() {
min > self.target
} else {
false
}
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.windowed_min.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.windowed_min.count() >= self.min_samples
}
#[inline]
pub fn reset(&mut self, now: Instant) {
self.windowed_min.reset(now);
}
}
impl $builder {
#[inline]
#[must_use]
pub fn target(mut self, target: $ty) -> Self {
self.target = Option::Some(target);
self
}
#[inline]
#[must_use]
pub fn window(mut self, window: Duration) -> Self {
self.window = Option::Some(window);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
#[must_use]
pub fn base(mut self, base: Instant) -> Self {
self.base = Option::Some(base);
self
}
#[inline]
pub fn build(self) -> Result<$name, crate::ConfigError> {
let target = self.target.ok_or(crate::ConfigError::Missing("target"))?;
let window = self.window.ok_or(crate::ConfigError::Missing("window"))?;
if window.is_zero() {
return Err(crate::ConfigError::Invalid("CoDel window must be positive"));
}
let base = self.base.unwrap_or_else(Instant::now);
Ok($name {
windowed_min: <$windowed_min>::with_base(window, base)?,
target,
min_samples: self.min_samples,
})
}
}
};
}
#[cfg(feature = "std")]
impl_codel!(CoDelI64, CoDelI64Builder, i64, WindowedMinI64);
#[cfg(feature = "std")]
impl_codel!(CoDelI32, CoDelI32Builder, i32, WindowedMinI32);
#[cfg(feature = "std")]
impl_codel!(CoDelI128, CoDelI128Builder, i128, WindowedMinI128);
#[cfg(feature = "std")]
impl_codel!(CoDelF64, CoDelF64Builder, f64, WindowedMinF64);
#[cfg(feature = "std")]
impl_codel!(CoDelF32, CoDelF32Builder, f32, WindowedMinF32);
#[cfg(test)]
mod raw_tests {
use super::*;
use crate::Condition;
#[test]
fn raw_codel_normal() {
let mut cd = CoDelI64Raw::builder()
.target(100)
.window(1000)
.build()
.unwrap();
assert_eq!(cd.update(0, 50), Some(Condition::Normal));
}
#[test]
fn raw_codel_degraded() {
let mut cd = CoDelI64Raw::builder()
.target(50)
.window(1000)
.build()
.unwrap();
for t in 0..10 {
let _ = cd.update(t * 100, 200);
}
assert!(cd.is_elevated());
}
#[test]
fn raw_codel_f64() {
let mut cd = CoDelF64Raw::builder()
.target(0.5)
.window(1000)
.build()
.unwrap();
assert_eq!(cd.update(0, 0.1), Some(Condition::Normal));
}
}
#[cfg(test)]
#[cfg(feature = "std")]
mod tests {
use super::*;
use std::time::{Duration, Instant};
fn t(base: Instant, ns: u64) -> Instant {
base + Duration::from_nanos(ns)
}
#[test]
fn healthy_queue() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(1000))
.base(base)
.build()
.unwrap();
for ts in 0..100 {
let result = qd.update(t(base, ts * 10), 50);
assert_eq!(result, Some(Condition::Normal));
}
assert!(!qd.is_elevated());
}
#[test]
fn elevated_detection() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(1000))
.base(base)
.build()
.unwrap();
for ts in 0..100 {
let _ = qd.update(t(base, ts * 10), 200);
}
assert!(qd.is_elevated());
}
#[test]
fn recovery_after_drain() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(10))
.base(base)
.build()
.unwrap();
for ts in 0..10 {
let _ = qd.update(t(base, ts), 200);
}
assert!(qd.is_elevated());
for ts in 10..30 {
let _ = qd.update(t(base, ts), 10);
}
assert!(!qd.is_elevated(), "should recover after drain");
}
#[test]
fn burst_vs_standing_queue() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(10))
.base(base)
.build()
.unwrap();
for ts in 0..10 {
let _ = qd.update(t(base, ts), 10);
}
let _ = qd.update(t(base, 10), 500); assert!(
!qd.is_elevated(),
"single burst should not trigger — min is still low"
);
}
#[test]
fn priming() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(1000))
.min_samples(5)
.base(base)
.build()
.unwrap();
for ts in 0..4 {
assert_eq!(qd.update(t(base, ts), 200), None);
}
assert!(qd.update(t(base, 4), 200).is_some());
}
#[test]
fn reset_clears() {
let base = Instant::now();
let mut qd = CoDelI64::builder()
.target(100)
.window(Duration::from_nanos(1000))
.base(base)
.build()
.unwrap();
for ts in 0..10 {
let _ = qd.update(t(base, ts), 200);
}
qd.reset(base);
assert_eq!(qd.count(), 0);
assert!(qd.min_sojourn().is_none());
}
#[test]
fn i32_basic() {
let base = Instant::now();
let mut qd = CoDelI32::builder()
.target(50)
.window(Duration::from_nanos(100))
.base(base)
.build()
.unwrap();
let result = qd.update(t(base, 0), 30);
assert_eq!(result, Some(Condition::Normal));
}
#[test]
fn errors_without_target() {
let base = Instant::now();
let result = CoDelI64::builder()
.window(Duration::from_nanos(100))
.base(base)
.build();
assert!(matches!(result, Err(crate::ConfigError::Missing("target"))));
}
#[test]
fn errors_without_window() {
let base = Instant::now();
let result = CoDelI64::builder().target(100).base(base).build();
assert!(matches!(result, Err(crate::ConfigError::Missing("window"))));
}
#[test]
fn i128_basic() {
let base = Instant::now();
let mut qd = CoDelI128::builder()
.target(50)
.window(Duration::from_nanos(100))
.base(base)
.build()
.unwrap();
let result = qd.update(t(base, 0), 30);
assert_eq!(result, Some(Condition::Normal));
}
}