1#![allow(dead_code)]
8
9#[derive(Debug, Clone)]
19pub struct TokenBucket {
20 pub capacity: f64,
22 pub refill_rate: f64,
24 tokens: f64,
26 last_refill_ms: u64,
28}
29
30impl TokenBucket {
31 #[must_use]
33 pub fn new(capacity: f64, refill_rate: f64, now_ms: u64) -> Self {
34 Self {
35 capacity,
36 refill_rate,
37 tokens: capacity,
38 last_refill_ms: now_ms,
39 }
40 }
41
42 fn refill(&mut self, now_ms: u64) {
44 if now_ms <= self.last_refill_ms {
45 return;
46 }
47 let elapsed = (now_ms - self.last_refill_ms) as f64;
48 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.capacity);
49 self.last_refill_ms = now_ms;
50 }
51
52 pub fn try_consume(&mut self, tokens: f64, now_ms: u64) -> bool {
57 self.refill(now_ms);
58 if self.tokens >= tokens {
59 self.tokens -= tokens;
60 true
61 } else {
62 false
63 }
64 }
65
66 #[must_use]
68 pub fn available(&mut self, now_ms: u64) -> f64 {
69 self.refill(now_ms);
70 self.tokens
71 }
72
73 #[must_use]
75 pub fn is_full(&mut self, now_ms: u64) -> bool {
76 self.refill(now_ms);
77 (self.tokens - self.capacity).abs() < f64::EPSILON
78 }
79}
80
81#[derive(Debug, Clone)]
90pub struct CreditAccount {
91 credits: i64,
93 pub max_credits: i64,
95}
96
97impl CreditAccount {
98 #[must_use]
100 pub fn new(max_credits: i64) -> Self {
101 Self {
102 credits: 0,
103 max_credits,
104 }
105 }
106
107 pub fn grant(&mut self, n: i64) {
109 self.credits = (self.credits + n).min(self.max_credits);
110 }
111
112 pub fn consume(&mut self) -> bool {
117 if self.credits > 0 {
118 self.credits -= 1;
119 true
120 } else {
121 false
122 }
123 }
124
125 #[must_use]
127 pub fn balance(&self) -> i64 {
128 self.credits
129 }
130
131 #[must_use]
133 pub fn may_send(&self) -> bool {
134 self.credits > 0
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
144pub enum BackpressureLevel {
145 None,
147 Low,
149 Medium,
151 High,
153 Critical,
155}
156
157impl BackpressureLevel {
158 #[must_use]
162 pub fn rate_multiplier(&self) -> f64 {
163 match self {
164 Self::None => 1.0,
165 Self::Low => 0.75,
166 Self::Medium => 0.5,
167 Self::High => 0.2,
168 Self::Critical => 0.0,
169 }
170 }
171
172 #[must_use]
174 pub fn from_fill_ratio(ratio: f64) -> Self {
175 let ratio = ratio.clamp(0.0, 1.0);
176 if ratio < 0.5 {
177 Self::None
178 } else if ratio < 0.7 {
179 Self::Low
180 } else if ratio < 0.85 {
181 Self::Medium
182 } else if ratio < 0.95 {
183 Self::High
184 } else {
185 Self::Critical
186 }
187 }
188}
189
190#[derive(Debug, Clone)]
196pub struct NodeBackpressure {
197 pub node_id: String,
199 pub level: BackpressureLevel,
201 pub fill_ratio: f64,
203 pub timestamp_ms: u64,
205}
206
207impl NodeBackpressure {
208 #[must_use]
210 pub fn from_fill(node_id: impl Into<String>, fill_ratio: f64, timestamp_ms: u64) -> Self {
211 Self {
212 node_id: node_id.into(),
213 level: BackpressureLevel::from_fill_ratio(fill_ratio),
214 fill_ratio,
215 timestamp_ms,
216 }
217 }
218}
219
220#[derive(Debug, Default)]
227pub struct BackpressureAggregator {
228 signals: Vec<NodeBackpressure>,
229}
230
231impl BackpressureAggregator {
232 #[must_use]
234 pub fn new() -> Self {
235 Self::default()
236 }
237
238 pub fn update(&mut self, signal: NodeBackpressure) {
240 if let Some(existing) = self
241 .signals
242 .iter_mut()
243 .find(|s| s.node_id == signal.node_id)
244 {
245 *existing = signal;
246 } else {
247 self.signals.push(signal);
248 }
249 }
250
251 pub fn evict_stale(&mut self, now_ms: u64, ttl_ms: u64) {
253 let cutoff = now_ms.saturating_sub(ttl_ms);
254 self.signals.retain(|s| s.timestamp_ms >= cutoff);
255 }
256
257 #[must_use]
259 pub fn max_level(&self) -> BackpressureLevel {
260 self.signals
261 .iter()
262 .map(|s| s.level)
263 .max()
264 .unwrap_or(BackpressureLevel::None)
265 }
266
267 #[must_use]
269 pub fn mean_fill_ratio(&self) -> f64 {
270 if self.signals.is_empty() {
271 return 0.0;
272 }
273 self.signals.iter().map(|s| s.fill_ratio).sum::<f64>() / self.signals.len() as f64
274 }
275
276 #[must_use]
278 pub fn recommended_rate_multiplier(&self) -> f64 {
279 self.signals
280 .iter()
281 .map(|s| s.level.rate_multiplier())
282 .fold(1.0_f64, f64::min)
283 }
284
285 #[must_use]
287 pub fn count_at_or_above(&self, level: BackpressureLevel) -> usize {
288 self.signals.iter().filter(|s| s.level >= level).count()
289 }
290}
291
292#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
303 fn test_token_bucket_starts_full() {
304 let mut bucket = TokenBucket::new(100.0, 1.0, 0);
305 assert!((bucket.available(0) - 100.0).abs() < 1e-9);
306 }
307
308 #[test]
309 fn test_token_bucket_consume_success() {
310 let mut bucket = TokenBucket::new(100.0, 1.0, 0);
311 assert!(bucket.try_consume(50.0, 0));
312 assert!((bucket.available(0) - 50.0).abs() < 1e-9);
313 }
314
315 #[test]
316 fn test_token_bucket_consume_fail_insufficient() {
317 let mut bucket = TokenBucket::new(10.0, 0.0, 0);
318 assert!(!bucket.try_consume(20.0, 0));
319 assert!((bucket.available(0) - 10.0).abs() < 1e-9);
321 }
322
323 #[test]
324 fn test_token_bucket_refills_over_time() {
325 let mut bucket = TokenBucket::new(100.0, 10.0, 0); bucket.try_consume(100.0, 0); assert!(bucket.available(0) < 1.0);
328 let avail = bucket.available(5); assert!((avail - 50.0).abs() < 1e-6, "avail={avail}");
330 }
331
332 #[test]
333 fn test_token_bucket_does_not_exceed_capacity() {
334 let mut bucket = TokenBucket::new(50.0, 100.0, 0);
335 let avail = bucket.available(1000);
336 assert!((avail - 50.0).abs() < 1e-9, "avail={avail}");
337 }
338
339 #[test]
340 fn test_token_bucket_is_full_initially() {
341 let mut bucket = TokenBucket::new(10.0, 1.0, 0);
342 assert!(bucket.is_full(0));
343 }
344
345 #[test]
348 fn test_credit_account_starts_at_zero() {
349 let account = CreditAccount::new(100);
350 assert_eq!(account.balance(), 0);
351 }
352
353 #[test]
354 fn test_credit_account_grant_increases_balance() {
355 let mut account = CreditAccount::new(100);
356 account.grant(10);
357 assert_eq!(account.balance(), 10);
358 }
359
360 #[test]
361 fn test_credit_account_grant_capped_at_max() {
362 let mut account = CreditAccount::new(5);
363 account.grant(100);
364 assert_eq!(account.balance(), 5);
365 }
366
367 #[test]
368 fn test_credit_account_consume_success() {
369 let mut account = CreditAccount::new(10);
370 account.grant(3);
371 assert!(account.consume());
372 assert_eq!(account.balance(), 2);
373 }
374
375 #[test]
376 fn test_credit_account_consume_fail_when_empty() {
377 let mut account = CreditAccount::new(10);
378 assert!(!account.consume());
379 }
380
381 #[test]
382 fn test_credit_account_may_send() {
383 let mut account = CreditAccount::new(10);
384 assert!(!account.may_send());
385 account.grant(1);
386 assert!(account.may_send());
387 }
388
389 #[test]
392 fn test_backpressure_from_fill_ratio_none() {
393 assert_eq!(
394 BackpressureLevel::from_fill_ratio(0.0),
395 BackpressureLevel::None
396 );
397 assert_eq!(
398 BackpressureLevel::from_fill_ratio(0.49),
399 BackpressureLevel::None
400 );
401 }
402
403 #[test]
404 fn test_backpressure_from_fill_ratio_low() {
405 assert_eq!(
406 BackpressureLevel::from_fill_ratio(0.6),
407 BackpressureLevel::Low
408 );
409 }
410
411 #[test]
412 fn test_backpressure_from_fill_ratio_medium() {
413 assert_eq!(
414 BackpressureLevel::from_fill_ratio(0.75),
415 BackpressureLevel::Medium
416 );
417 }
418
419 #[test]
420 fn test_backpressure_from_fill_ratio_high() {
421 assert_eq!(
422 BackpressureLevel::from_fill_ratio(0.9),
423 BackpressureLevel::High
424 );
425 }
426
427 #[test]
428 fn test_backpressure_from_fill_ratio_critical() {
429 assert_eq!(
430 BackpressureLevel::from_fill_ratio(1.0),
431 BackpressureLevel::Critical
432 );
433 }
434
435 #[test]
436 fn test_backpressure_rate_multiplier_none() {
437 assert!((BackpressureLevel::None.rate_multiplier() - 1.0).abs() < 1e-9);
438 }
439
440 #[test]
441 fn test_backpressure_rate_multiplier_critical() {
442 assert!(BackpressureLevel::Critical.rate_multiplier() < f64::EPSILON);
443 }
444
445 #[test]
448 fn test_aggregator_empty_max_level_is_none() {
449 let agg = BackpressureAggregator::new();
450 assert_eq!(agg.max_level(), BackpressureLevel::None);
451 }
452
453 #[test]
454 fn test_aggregator_max_level_selects_worst() {
455 let mut agg = BackpressureAggregator::new();
456 agg.update(NodeBackpressure::from_fill("n0", 0.3, 1000));
457 agg.update(NodeBackpressure::from_fill("n1", 0.9, 1000));
458 assert_eq!(agg.max_level(), BackpressureLevel::High);
459 }
460
461 #[test]
462 fn test_aggregator_mean_fill_ratio() {
463 let mut agg = BackpressureAggregator::new();
464 agg.update(NodeBackpressure::from_fill("n0", 0.4, 1000));
465 agg.update(NodeBackpressure::from_fill("n1", 0.6, 1000));
466 let mean = agg.mean_fill_ratio();
467 assert!((mean - 0.5).abs() < 1e-9, "mean={mean}");
468 }
469
470 #[test]
471 fn test_aggregator_recommended_rate_minimum() {
472 let mut agg = BackpressureAggregator::new();
473 agg.update(NodeBackpressure::from_fill("n0", 0.2, 1000)); agg.update(NodeBackpressure::from_fill("n1", 0.8, 1000)); let rate = agg.recommended_rate_multiplier();
477 assert!((rate - 0.5).abs() < 1e-6, "rate={rate}");
478 }
479
480 #[test]
481 fn test_aggregator_evict_stale() {
482 let mut agg = BackpressureAggregator::new();
483 agg.update(NodeBackpressure::from_fill("n0", 0.5, 100));
484 agg.update(NodeBackpressure::from_fill("n1", 0.5, 5000));
485 agg.evict_stale(5000, 1000); assert_eq!(agg.count_at_or_above(BackpressureLevel::None), 1);
487 }
488
489 #[test]
490 fn test_aggregator_count_at_or_above() {
491 let mut agg = BackpressureAggregator::new();
492 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);
496 assert_eq!(agg.count_at_or_above(BackpressureLevel::High), 1);
497 }
498}