1use std::collections::HashMap;
40
41#[derive(Debug, Clone)]
48pub struct BandwidthQuota {
49 pub upload_bytes_per_sec: u64,
51 pub download_bytes_per_sec: u64,
53 pub burst_factor: f64,
55}
56
57impl BandwidthQuota {
58 #[inline]
60 pub fn upload_burst_cap(&self) -> f64 {
61 self.upload_bytes_per_sec as f64 * self.burst_factor.max(1.0)
62 }
63
64 #[inline]
66 pub fn download_burst_cap(&self) -> f64 {
67 self.download_bytes_per_sec as f64 * self.burst_factor.max(1.0)
68 }
69}
70
71#[derive(Debug, Clone)]
79pub struct PeerBucket {
80 pub peer_id: String,
82 pub upload_tokens: f64,
84 pub download_tokens: f64,
86 pub last_refill: u64,
88 pub quota: BandwidthQuota,
90 pub bytes_used_up: u64,
92 pub bytes_used_down: u64,
94}
95
96impl PeerBucket {
97 fn new(peer_id: String, quota: BandwidthQuota, now: u64) -> Self {
100 let upload_tokens = quota.upload_burst_cap();
101 let download_tokens = quota.download_burst_cap();
102 Self {
103 peer_id,
104 upload_tokens,
105 download_tokens,
106 last_refill: now,
107 quota,
108 bytes_used_up: 0,
109 bytes_used_down: 0,
110 }
111 }
112
113 fn refill(&mut self, now: u64, min_tokens: f64) {
118 if now <= self.last_refill {
119 return;
120 }
121 let elapsed_ms = (now - self.last_refill) as f64;
122 let elapsed_secs = elapsed_ms / 1_000.0;
123
124 let up_cap = self.quota.upload_burst_cap();
125 let down_cap = self.quota.download_burst_cap();
126
127 self.upload_tokens = (self.upload_tokens
128 + elapsed_secs * self.quota.upload_bytes_per_sec as f64)
129 .min(up_cap)
130 .max(min_tokens);
131
132 self.download_tokens = (self.download_tokens
133 + elapsed_secs * self.quota.download_bytes_per_sec as f64)
134 .min(down_cap)
135 .max(min_tokens);
136
137 self.last_refill = now;
138 }
139}
140
141#[derive(Debug, Clone)]
145pub struct BudgetConfig {
146 pub default_quota: BandwidthQuota,
148 pub global_upload_cap: u64,
151 pub global_download_cap: u64,
153 pub min_tokens: f64,
155 pub gc_threshold: usize,
160}
161
162#[derive(Debug, Clone, Default)]
166pub struct BudgetStats {
167 pub total_granted_up: u64,
169 pub total_granted_down: u64,
171 pub total_rejected_up: u64,
173 pub total_rejected_down: u64,
175 pub active_peers: u64,
177}
178
179pub struct BandwidthBudgetManager {
192 config: BudgetConfig,
193 peers: HashMap<String, PeerBucket>,
194 global_upload_used: u64,
195 global_download_used: u64,
196 stats: BudgetStats,
197 op_count: usize,
199}
200
201impl BandwidthBudgetManager {
202 pub fn new(config: BudgetConfig) -> Self {
206 Self {
207 config,
208 peers: HashMap::new(),
209 global_upload_used: 0,
210 global_download_used: 0,
211 stats: BudgetStats::default(),
212 op_count: 0,
213 }
214 }
215
216 pub fn register_peer(&mut self, peer_id: &str, now: u64) {
222 if self.peers.contains_key(peer_id) {
223 return;
224 }
225 let quota = self.config.default_quota.clone();
226 self.register_peer_with_quota(peer_id, quota, now);
227 }
228
229 pub fn register_peer_with_quota(&mut self, peer_id: &str, quota: BandwidthQuota, now: u64) {
234 let bucket = PeerBucket::new(peer_id.to_owned(), quota, now);
235 self.peers.insert(peer_id.to_owned(), bucket);
236 self.stats.active_peers = self.peers.len() as u64;
237 }
238
239 pub fn refill(&mut self, peer_id: &str, now: u64) {
246 let min_tokens = self.config.min_tokens;
247 if let Some(bucket) = self.peers.get_mut(peer_id) {
248 bucket.refill(now, min_tokens);
249 }
250 }
251
252 pub fn refill_all(&mut self, now: u64) {
254 let min_tokens = self.config.min_tokens;
255 for bucket in self.peers.values_mut() {
256 bucket.refill(now, min_tokens);
257 }
258 }
259
260 pub fn try_consume_upload(&mut self, peer_id: &str, bytes: u64, now: u64) -> bool {
271 self.refill(peer_id, now);
273 self.bump_op_count();
274
275 let global_cap = self.config.global_upload_cap;
276 let global_used = self.global_upload_used;
277
278 let bucket = match self.peers.get_mut(peer_id) {
279 Some(b) => b,
280 None => {
281 self.stats.total_rejected_up = self.stats.total_rejected_up.saturating_add(1);
282 return false;
283 }
284 };
285
286 let bytes_f = bytes as f64;
287 if bucket.upload_tokens < bytes_f {
288 self.stats.total_rejected_up = self.stats.total_rejected_up.saturating_add(1);
289 return false;
290 }
291
292 if global_used.saturating_add(bytes) > global_cap {
293 self.stats.total_rejected_up = self.stats.total_rejected_up.saturating_add(1);
294 return false;
295 }
296
297 bucket.upload_tokens -= bytes_f;
298 bucket.bytes_used_up = bucket.bytes_used_up.saturating_add(bytes);
299 self.global_upload_used = self.global_upload_used.saturating_add(bytes);
300 self.stats.total_granted_up = self.stats.total_granted_up.saturating_add(bytes);
301 true
302 }
303
304 pub fn try_consume_download(&mut self, peer_id: &str, bytes: u64, now: u64) -> bool {
313 self.refill(peer_id, now);
314 self.bump_op_count();
315
316 let global_cap = self.config.global_download_cap;
317 let global_used = self.global_download_used;
318
319 let bucket = match self.peers.get_mut(peer_id) {
320 Some(b) => b,
321 None => {
322 self.stats.total_rejected_down = self.stats.total_rejected_down.saturating_add(1);
323 return false;
324 }
325 };
326
327 let bytes_f = bytes as f64;
328 if bucket.download_tokens < bytes_f {
329 self.stats.total_rejected_down = self.stats.total_rejected_down.saturating_add(1);
330 return false;
331 }
332
333 if global_used.saturating_add(bytes) > global_cap {
334 self.stats.total_rejected_down = self.stats.total_rejected_down.saturating_add(1);
335 return false;
336 }
337
338 bucket.download_tokens -= bytes_f;
339 bucket.bytes_used_down = bucket.bytes_used_down.saturating_add(bytes);
340 self.global_download_used = self.global_download_used.saturating_add(bytes);
341 self.stats.total_granted_down = self.stats.total_granted_down.saturating_add(bytes);
342 true
343 }
344
345 pub fn remaining_upload(&self, peer_id: &str) -> u64 {
351 self.peers
352 .get(peer_id)
353 .map(|b| b.upload_tokens.max(0.0) as u64)
354 .unwrap_or(0)
355 }
356
357 pub fn remaining_download(&self, peer_id: &str) -> u64 {
361 self.peers
362 .get(peer_id)
363 .map(|b| b.download_tokens.max(0.0) as u64)
364 .unwrap_or(0)
365 }
366
367 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
373 let removed = self.peers.remove(peer_id).is_some();
374 if removed {
375 self.stats.active_peers = self.peers.len() as u64;
376 }
377 removed
378 }
379
380 pub fn peer_count(&self) -> usize {
382 self.peers.len()
383 }
384
385 pub fn stats(&self) -> &BudgetStats {
387 &self.stats
388 }
389
390 fn bump_op_count(&mut self) {
394 self.op_count = self.op_count.wrapping_add(1);
395 if self.op_count >= self.config.gc_threshold {
396 self.op_count = 0;
397 self.gc_idle_peers();
398 }
399 }
400
401 fn gc_idle_peers(&mut self) {
407 self.peers.retain(|_, bucket| {
408 bucket.bytes_used_up > 0
409 || bucket.bytes_used_down > 0
410 || bucket.upload_tokens > 0.0
411 || bucket.download_tokens > 0.0
412 });
413 self.stats.active_peers = self.peers.len() as u64;
414 }
415}
416
417#[cfg(test)]
420mod tests {
421 use super::*;
422
423 fn default_quota() -> BandwidthQuota {
426 BandwidthQuota {
427 upload_bytes_per_sec: 1_000, download_bytes_per_sec: 2_000, burst_factor: 2.0,
430 }
431 }
432
433 fn make_manager() -> BandwidthBudgetManager {
434 let config = BudgetConfig {
435 default_quota: default_quota(),
436 global_upload_cap: 1_000_000,
437 global_download_cap: 2_000_000,
438 min_tokens: 0.0,
439 gc_threshold: 10_000,
440 };
441 BandwidthBudgetManager::new(config)
442 }
443
444 #[test]
447 fn t01_register_peer_adds_peer() {
448 let mut mgr = make_manager();
449 assert_eq!(mgr.peer_count(), 0);
450 mgr.register_peer("p1", 0);
451 assert_eq!(mgr.peer_count(), 1);
452 }
453
454 #[test]
457 fn t02_register_peer_idempotent() {
458 let mut mgr = make_manager();
459 mgr.register_peer("p1", 0);
460 mgr.register_peer("p1", 100); assert_eq!(mgr.peer_count(), 1);
462 }
463
464 #[test]
467 fn t03_register_peer_with_quota() {
468 let mut mgr = make_manager();
469 let quota = BandwidthQuota {
470 upload_bytes_per_sec: 9_999,
471 download_bytes_per_sec: 8_888,
472 burst_factor: 1.0,
473 };
474 mgr.register_peer_with_quota("p1", quota.clone(), 0);
475 assert_eq!(mgr.remaining_upload("p1"), 9_999);
477 assert_eq!(mgr.remaining_download("p1"), 8_888);
478 }
479
480 #[test]
483 fn t04_initial_tokens_equal_burst_cap() {
484 let mut mgr = make_manager();
485 mgr.register_peer("p1", 0);
486 assert_eq!(mgr.remaining_upload("p1"), 2_000);
488 assert_eq!(mgr.remaining_download("p1"), 4_000);
489 }
490
491 #[test]
494 fn t05_consume_upload_succeeds() {
495 let mut mgr = make_manager();
496 mgr.register_peer("p1", 0);
497 let ok = mgr.try_consume_upload("p1", 500, 0);
498 assert!(ok);
499 assert_eq!(mgr.remaining_upload("p1"), 1_500);
500 }
501
502 #[test]
505 fn t06_consume_download_succeeds() {
506 let mut mgr = make_manager();
507 mgr.register_peer("p1", 0);
508 let ok = mgr.try_consume_download("p1", 1_000, 0);
509 assert!(ok);
510 assert_eq!(mgr.remaining_download("p1"), 3_000);
511 }
512
513 #[test]
516 fn t07_consume_upload_rejected_no_tokens() {
517 let mut mgr = make_manager();
518 mgr.register_peer("p1", 0);
519 assert!(mgr.try_consume_upload("p1", 2_000, 0));
521 let ok = mgr.try_consume_upload("p1", 1, 0);
523 assert!(!ok);
524 }
525
526 #[test]
529 fn t08_consume_download_rejected_no_tokens() {
530 let mut mgr = make_manager();
531 mgr.register_peer("p1", 0);
532 assert!(mgr.try_consume_download("p1", 4_000, 0));
533 let ok = mgr.try_consume_download("p1", 1, 0);
534 assert!(!ok);
535 }
536
537 #[test]
540 fn t09_refill_adds_tokens() {
541 let mut mgr = make_manager();
542 mgr.register_peer("p1", 0);
543 assert!(mgr.try_consume_upload("p1", 2_000, 0));
545 assert_eq!(mgr.remaining_upload("p1"), 0);
546 mgr.refill("p1", 1_000);
548 assert_eq!(mgr.remaining_upload("p1"), 1_000);
549 }
550
551 #[test]
554 fn t10_refill_caps_at_burst() {
555 let mut mgr = make_manager();
556 mgr.register_peer("p1", 0);
557 assert!(mgr.try_consume_upload("p1", 1_000, 0));
559 mgr.refill("p1", 10_000);
561 assert_eq!(mgr.remaining_upload("p1"), 2_000);
562 }
563
564 #[test]
567 fn t11_burst_factor_allows_burst() {
568 let quota = BandwidthQuota {
569 upload_bytes_per_sec: 1_000,
570 download_bytes_per_sec: 1_000,
571 burst_factor: 3.0,
572 };
573 let config = BudgetConfig {
574 default_quota: quota,
575 global_upload_cap: u64::MAX,
576 global_download_cap: u64::MAX,
577 min_tokens: 0.0,
578 gc_threshold: 10_000,
579 };
580 let mut mgr = BandwidthBudgetManager::new(config);
581 mgr.register_peer("p1", 0);
582 assert!(mgr.try_consume_upload("p1", 2_500, 0));
584 }
585
586 #[test]
589 fn t12_global_upload_cap_enforced() {
590 let config = BudgetConfig {
591 default_quota: BandwidthQuota {
592 upload_bytes_per_sec: 1_000_000,
593 download_bytes_per_sec: 1_000_000,
594 burst_factor: 10.0,
595 },
596 global_upload_cap: 100,
597 global_download_cap: u64::MAX,
598 min_tokens: 0.0,
599 gc_threshold: 10_000,
600 };
601 let mut mgr = BandwidthBudgetManager::new(config);
602 mgr.register_peer("p1", 0);
603 assert!(mgr.try_consume_upload("p1", 100, 0));
605 assert!(!mgr.try_consume_upload("p1", 1, 0));
607 }
608
609 #[test]
612 fn t13_global_download_cap_enforced() {
613 let config = BudgetConfig {
614 default_quota: BandwidthQuota {
615 upload_bytes_per_sec: 1_000_000,
616 download_bytes_per_sec: 1_000_000,
617 burst_factor: 10.0,
618 },
619 global_upload_cap: u64::MAX,
620 global_download_cap: 200,
621 min_tokens: 0.0,
622 gc_threshold: 10_000,
623 };
624 let mut mgr = BandwidthBudgetManager::new(config);
625 mgr.register_peer("p1", 0);
626 assert!(mgr.try_consume_download("p1", 200, 0));
627 assert!(!mgr.try_consume_download("p1", 1, 0));
628 }
629
630 #[test]
633 fn t14_per_peer_quota_override() {
634 let mut mgr = make_manager();
635 let custom = BandwidthQuota {
636 upload_bytes_per_sec: 50,
637 download_bytes_per_sec: 50,
638 burst_factor: 1.0,
639 };
640 mgr.register_peer_with_quota("slow", custom, 0);
641 assert!(mgr.try_consume_upload("slow", 50, 0));
643 assert!(!mgr.try_consume_upload("slow", 1, 0));
644 }
645
646 #[test]
649 fn t15_remove_peer_returns_true() {
650 let mut mgr = make_manager();
651 mgr.register_peer("p1", 0);
652 assert!(mgr.remove_peer("p1"));
653 }
654
655 #[test]
658 fn t16_remove_peer_returns_false_unknown() {
659 let mut mgr = make_manager();
660 assert!(!mgr.remove_peer("ghost"));
661 }
662
663 #[test]
666 fn t17_peer_count_after_remove() {
667 let mut mgr = make_manager();
668 mgr.register_peer("p1", 0);
669 mgr.register_peer("p2", 0);
670 assert_eq!(mgr.peer_count(), 2);
671 mgr.remove_peer("p1");
672 assert_eq!(mgr.peer_count(), 1);
673 }
674
675 #[test]
678 fn t18_refill_all_refreshes_all_peers() {
679 let mut mgr = make_manager();
680 mgr.register_peer("p1", 0);
681 mgr.register_peer("p2", 0);
682 assert!(mgr.try_consume_upload("p1", 2_000, 0));
684 assert!(mgr.try_consume_upload("p2", 2_000, 0));
685 assert_eq!(mgr.remaining_upload("p1"), 0);
686 assert_eq!(mgr.remaining_upload("p2"), 0);
687 mgr.refill_all(2_000);
689 assert_eq!(mgr.remaining_upload("p1"), 2_000);
690 assert_eq!(mgr.remaining_upload("p2"), 2_000);
691 }
692
693 #[test]
696 fn t19_stats_granted_bytes() {
697 let mut mgr = make_manager();
698 mgr.register_peer("p1", 0);
699 assert!(mgr.try_consume_upload("p1", 100, 0));
700 assert!(mgr.try_consume_download("p1", 200, 0));
701 let s = mgr.stats();
702 assert_eq!(s.total_granted_up, 100);
703 assert_eq!(s.total_granted_down, 200);
704 }
705
706 #[test]
709 fn t20_stats_rejected_counts() {
710 let mut mgr = make_manager();
711 mgr.register_peer("p1", 0);
712 assert!(mgr.try_consume_upload("p1", 2_000, 0));
714 assert!(!mgr.try_consume_upload("p1", 1, 0));
715 assert!(mgr.try_consume_download("p1", 4_000, 0));
716 assert!(!mgr.try_consume_download("p1", 1, 0));
717 let s = mgr.stats();
718 assert_eq!(s.total_rejected_up, 1);
719 assert_eq!(s.total_rejected_down, 1);
720 }
721
722 #[test]
725 fn t21_stats_active_peers() {
726 let mut mgr = make_manager();
727 mgr.register_peer("a", 0);
728 mgr.register_peer("b", 0);
729 assert_eq!(mgr.stats().active_peers, 2);
730 mgr.remove_peer("a");
731 assert_eq!(mgr.stats().active_peers, 1);
732 }
733
734 #[test]
737 fn t22_zero_byte_consume_always_succeeds() {
738 let mut mgr = make_manager();
739 mgr.register_peer("p1", 0);
740 assert!(mgr.try_consume_upload("p1", 2_000, 0));
742 assert!(mgr.try_consume_upload("p1", 0, 0));
744 assert!(mgr.try_consume_download("p1", 0, 0));
745 }
746
747 #[test]
750 fn t23_consume_unknown_peer_returns_false() {
751 let mut mgr = make_manager();
752 assert!(!mgr.try_consume_upload("nobody", 1, 0));
753 assert!(!mgr.try_consume_download("nobody", 1, 0));
754 assert_eq!(mgr.stats().total_rejected_up, 1);
755 assert_eq!(mgr.stats().total_rejected_down, 1);
756 }
757
758 #[test]
761 fn t24_remaining_upload_unknown_peer() {
762 let mgr = make_manager();
763 assert_eq!(mgr.remaining_upload("ghost"), 0);
764 assert_eq!(mgr.remaining_download("ghost"), 0);
765 }
766
767 #[test]
774 fn t25_rapid_fill_drain_cycle() {
775 let mut mgr = make_manager();
776 mgr.register_peer("p1", 0);
777
778 let mut now: u64 = 0;
779 for _ in 0..10 {
780 assert!(
782 mgr.try_consume_upload("p1", 500, now),
783 "consume failed at t={now}"
784 );
785 now += 1_000; mgr.refill("p1", now);
787 }
788 let remaining = mgr.remaining_upload("p1");
790 assert!(remaining <= 2_000, "tokens exceeded burst cap: {remaining}");
791 }
792
793 #[test]
796 fn t26_refill_no_rewind() {
797 let mut mgr = make_manager();
798 mgr.register_peer("p1", 1_000);
799 assert!(mgr.try_consume_upload("p1", 2_000, 1_000));
801 assert_eq!(mgr.remaining_upload("p1"), 0);
802 mgr.refill("p1", 500);
804 assert_eq!(mgr.remaining_upload("p1"), 0);
805 }
806
807 #[test]
810 fn t27_peers_independent_buckets() {
811 let mut mgr = make_manager();
812 mgr.register_peer("a", 0);
813 mgr.register_peer("b", 0);
814 assert!(mgr.try_consume_upload("a", 2_000, 0));
816 assert_eq!(mgr.remaining_upload("b"), 2_000);
818 assert!(mgr.try_consume_upload("b", 2_000, 0));
820 }
821
822 #[test]
825 fn t28_global_cap_shared_across_peers() {
826 let config = BudgetConfig {
827 default_quota: BandwidthQuota {
828 upload_bytes_per_sec: 1_000_000,
829 download_bytes_per_sec: 1_000_000,
830 burst_factor: 100.0,
831 },
832 global_upload_cap: 300,
833 global_download_cap: u64::MAX,
834 min_tokens: 0.0,
835 gc_threshold: 10_000,
836 };
837 let mut mgr = BandwidthBudgetManager::new(config);
838 mgr.register_peer("a", 0);
839 mgr.register_peer("b", 0);
840 assert!(mgr.try_consume_upload("a", 200, 0));
842 assert!(mgr.try_consume_upload("b", 100, 0));
843 assert!(!mgr.try_consume_upload("a", 1, 0));
845 assert!(!mgr.try_consume_upload("b", 1, 0));
846 }
847}