#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TokenBucket {
pub capacity: f64,
pub refill_rate: f64,
tokens: f64,
last_refill_ms: u64,
}
impl TokenBucket {
#[must_use]
pub fn new(capacity: f64, refill_rate: f64, now_ms: u64) -> Self {
Self {
capacity,
refill_rate,
tokens: capacity,
last_refill_ms: now_ms,
}
}
fn refill(&mut self, now_ms: u64) {
if now_ms <= self.last_refill_ms {
return;
}
let elapsed = (now_ms - self.last_refill_ms) as f64;
self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.capacity);
self.last_refill_ms = now_ms;
}
pub fn try_consume(&mut self, tokens: f64, now_ms: u64) -> bool {
self.refill(now_ms);
if self.tokens >= tokens {
self.tokens -= tokens;
true
} else {
false
}
}
#[must_use]
pub fn available(&mut self, now_ms: u64) -> f64 {
self.refill(now_ms);
self.tokens
}
#[must_use]
pub fn is_full(&mut self, now_ms: u64) -> bool {
self.refill(now_ms);
(self.tokens - self.capacity).abs() < f64::EPSILON
}
}
#[derive(Debug, Clone)]
pub struct CreditAccount {
credits: i64,
pub max_credits: i64,
}
impl CreditAccount {
#[must_use]
pub fn new(max_credits: i64) -> Self {
Self {
credits: 0,
max_credits,
}
}
pub fn grant(&mut self, n: i64) {
self.credits = (self.credits + n).min(self.max_credits);
}
pub fn consume(&mut self) -> bool {
if self.credits > 0 {
self.credits -= 1;
true
} else {
false
}
}
#[must_use]
pub fn balance(&self) -> i64 {
self.credits
}
#[must_use]
pub fn may_send(&self) -> bool {
self.credits > 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BackpressureLevel {
None,
Low,
Medium,
High,
Critical,
}
impl BackpressureLevel {
#[must_use]
pub fn rate_multiplier(&self) -> f64 {
match self {
Self::None => 1.0,
Self::Low => 0.75,
Self::Medium => 0.5,
Self::High => 0.2,
Self::Critical => 0.0,
}
}
#[must_use]
pub fn from_fill_ratio(ratio: f64) -> Self {
let ratio = ratio.clamp(0.0, 1.0);
if ratio < 0.5 {
Self::None
} else if ratio < 0.7 {
Self::Low
} else if ratio < 0.85 {
Self::Medium
} else if ratio < 0.95 {
Self::High
} else {
Self::Critical
}
}
}
#[derive(Debug, Clone)]
pub struct NodeBackpressure {
pub node_id: String,
pub level: BackpressureLevel,
pub fill_ratio: f64,
pub timestamp_ms: u64,
}
impl NodeBackpressure {
#[must_use]
pub fn from_fill(node_id: impl Into<String>, fill_ratio: f64, timestamp_ms: u64) -> Self {
Self {
node_id: node_id.into(),
level: BackpressureLevel::from_fill_ratio(fill_ratio),
fill_ratio,
timestamp_ms,
}
}
}
#[derive(Debug, Default)]
pub struct BackpressureAggregator {
signals: Vec<NodeBackpressure>,
}
impl BackpressureAggregator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, signal: NodeBackpressure) {
if let Some(existing) = self
.signals
.iter_mut()
.find(|s| s.node_id == signal.node_id)
{
*existing = signal;
} else {
self.signals.push(signal);
}
}
pub fn evict_stale(&mut self, now_ms: u64, ttl_ms: u64) {
let cutoff = now_ms.saturating_sub(ttl_ms);
self.signals.retain(|s| s.timestamp_ms >= cutoff);
}
#[must_use]
pub fn max_level(&self) -> BackpressureLevel {
self.signals
.iter()
.map(|s| s.level)
.max()
.unwrap_or(BackpressureLevel::None)
}
#[must_use]
pub fn mean_fill_ratio(&self) -> f64 {
if self.signals.is_empty() {
return 0.0;
}
self.signals.iter().map(|s| s.fill_ratio).sum::<f64>() / self.signals.len() as f64
}
#[must_use]
pub fn recommended_rate_multiplier(&self) -> f64 {
self.signals
.iter()
.map(|s| s.level.rate_multiplier())
.fold(1.0_f64, f64::min)
}
#[must_use]
pub fn count_at_or_above(&self, level: BackpressureLevel) -> usize {
self.signals.iter().filter(|s| s.level >= level).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_bucket_starts_full() {
let mut bucket = TokenBucket::new(100.0, 1.0, 0);
assert!((bucket.available(0) - 100.0).abs() < 1e-9);
}
#[test]
fn test_token_bucket_consume_success() {
let mut bucket = TokenBucket::new(100.0, 1.0, 0);
assert!(bucket.try_consume(50.0, 0));
assert!((bucket.available(0) - 50.0).abs() < 1e-9);
}
#[test]
fn test_token_bucket_consume_fail_insufficient() {
let mut bucket = TokenBucket::new(10.0, 0.0, 0);
assert!(!bucket.try_consume(20.0, 0));
assert!((bucket.available(0) - 10.0).abs() < 1e-9);
}
#[test]
fn test_token_bucket_refills_over_time() {
let mut bucket = TokenBucket::new(100.0, 10.0, 0); bucket.try_consume(100.0, 0); assert!(bucket.available(0) < 1.0);
let avail = bucket.available(5); assert!((avail - 50.0).abs() < 1e-6, "avail={avail}");
}
#[test]
fn test_token_bucket_does_not_exceed_capacity() {
let mut bucket = TokenBucket::new(50.0, 100.0, 0);
let avail = bucket.available(1000);
assert!((avail - 50.0).abs() < 1e-9, "avail={avail}");
}
#[test]
fn test_token_bucket_is_full_initially() {
let mut bucket = TokenBucket::new(10.0, 1.0, 0);
assert!(bucket.is_full(0));
}
#[test]
fn test_credit_account_starts_at_zero() {
let account = CreditAccount::new(100);
assert_eq!(account.balance(), 0);
}
#[test]
fn test_credit_account_grant_increases_balance() {
let mut account = CreditAccount::new(100);
account.grant(10);
assert_eq!(account.balance(), 10);
}
#[test]
fn test_credit_account_grant_capped_at_max() {
let mut account = CreditAccount::new(5);
account.grant(100);
assert_eq!(account.balance(), 5);
}
#[test]
fn test_credit_account_consume_success() {
let mut account = CreditAccount::new(10);
account.grant(3);
assert!(account.consume());
assert_eq!(account.balance(), 2);
}
#[test]
fn test_credit_account_consume_fail_when_empty() {
let mut account = CreditAccount::new(10);
assert!(!account.consume());
}
#[test]
fn test_credit_account_may_send() {
let mut account = CreditAccount::new(10);
assert!(!account.may_send());
account.grant(1);
assert!(account.may_send());
}
#[test]
fn test_backpressure_from_fill_ratio_none() {
assert_eq!(
BackpressureLevel::from_fill_ratio(0.0),
BackpressureLevel::None
);
assert_eq!(
BackpressureLevel::from_fill_ratio(0.49),
BackpressureLevel::None
);
}
#[test]
fn test_backpressure_from_fill_ratio_low() {
assert_eq!(
BackpressureLevel::from_fill_ratio(0.6),
BackpressureLevel::Low
);
}
#[test]
fn test_backpressure_from_fill_ratio_medium() {
assert_eq!(
BackpressureLevel::from_fill_ratio(0.75),
BackpressureLevel::Medium
);
}
#[test]
fn test_backpressure_from_fill_ratio_high() {
assert_eq!(
BackpressureLevel::from_fill_ratio(0.9),
BackpressureLevel::High
);
}
#[test]
fn test_backpressure_from_fill_ratio_critical() {
assert_eq!(
BackpressureLevel::from_fill_ratio(1.0),
BackpressureLevel::Critical
);
}
#[test]
fn test_backpressure_rate_multiplier_none() {
assert!((BackpressureLevel::None.rate_multiplier() - 1.0).abs() < 1e-9);
}
#[test]
fn test_backpressure_rate_multiplier_critical() {
assert!(BackpressureLevel::Critical.rate_multiplier() < f64::EPSILON);
}
#[test]
fn test_aggregator_empty_max_level_is_none() {
let agg = BackpressureAggregator::new();
assert_eq!(agg.max_level(), BackpressureLevel::None);
}
#[test]
fn test_aggregator_max_level_selects_worst() {
let mut agg = BackpressureAggregator::new();
agg.update(NodeBackpressure::from_fill("n0", 0.3, 1000));
agg.update(NodeBackpressure::from_fill("n1", 0.9, 1000));
assert_eq!(agg.max_level(), BackpressureLevel::High);
}
#[test]
fn test_aggregator_mean_fill_ratio() {
let mut agg = BackpressureAggregator::new();
agg.update(NodeBackpressure::from_fill("n0", 0.4, 1000));
agg.update(NodeBackpressure::from_fill("n1", 0.6, 1000));
let mean = agg.mean_fill_ratio();
assert!((mean - 0.5).abs() < 1e-9, "mean={mean}");
}
#[test]
fn test_aggregator_recommended_rate_minimum() {
let mut agg = BackpressureAggregator::new();
agg.update(NodeBackpressure::from_fill("n0", 0.2, 1000)); agg.update(NodeBackpressure::from_fill("n1", 0.8, 1000)); let rate = agg.recommended_rate_multiplier();
assert!((rate - 0.5).abs() < 1e-6, "rate={rate}");
}
#[test]
fn test_aggregator_evict_stale() {
let mut agg = BackpressureAggregator::new();
agg.update(NodeBackpressure::from_fill("n0", 0.5, 100));
agg.update(NodeBackpressure::from_fill("n1", 0.5, 5000));
agg.evict_stale(5000, 1000); assert_eq!(agg.count_at_or_above(BackpressureLevel::None), 1);
}
#[test]
fn test_aggregator_count_at_or_above() {
let mut agg = BackpressureAggregator::new();
agg.update(NodeBackpressure::from_fill("n0", 0.2, 1000)); agg.update(NodeBackpressure::from_fill("n1", 0.6, 1000)); agg.update(NodeBackpressure::from_fill("n2", 0.9, 1000)); assert_eq!(agg.count_at_or_above(BackpressureLevel::Low), 2);
assert_eq!(agg.count_at_or_above(BackpressureLevel::High), 1);
}
}