Skip to main content

ipfrs_network/
bandwidth_budget.rs

1//! Per-peer bandwidth budget allocation and enforcement with token-bucket per peer.
2//!
3//! Each peer gets an independent token bucket for upload and download.  Tokens
4//! refill over time at the configured rate (bytes/sec) and may briefly exceed
5//! the nominal quota up to `quota * burst_factor` to allow short bursts.
6//!
7//! A global upload and download cap prevents any single peer's activity from
8//! starving the whole node.
9//!
10//! # Example
11//!
12//! ```rust
13//! use ipfrs_network::bandwidth_budget::{
14//!     BandwidthBudgetManager, BandwidthQuota, BudgetConfig,
15//! };
16//!
17//! let quota = BandwidthQuota {
18//!     upload_bytes_per_sec: 1_000_000,
19//!     download_bytes_per_sec: 2_000_000,
20//!     burst_factor: 1.5,
21//! };
22//! let config = BudgetConfig {
23//!     default_quota: quota,
24//!     global_upload_cap: 100_000_000,
25//!     global_download_cap: 200_000_000,
26//!     min_tokens: 0.0,
27//!     gc_threshold: 1000,
28//! };
29//! let mut mgr = BandwidthBudgetManager::new(config);
30//!
31//! // Register a peer with timestamp 0 (ms since epoch or any monotonic counter).
32//! mgr.register_peer("peer-1", 0);
33//!
34//! // Try to use 512 KiB of upload bandwidth at t=1000 ms.
35//! let allowed = mgr.try_consume_upload("peer-1", 512 * 1024, 1000);
36//! println!("upload allowed: {allowed}");
37//! ```
38
39use std::collections::HashMap;
40
41// ── BandwidthQuota ────────────────────────────────────────────────────────────
42
43/// Steady-state rate limits for a single peer.
44///
45/// `burst_factor` must be ≥ 1.0; values below 1.0 are clamped to 1.0 at
46/// token-bucket initialisation time.
47#[derive(Debug, Clone)]
48pub struct BandwidthQuota {
49    /// Maximum outbound bytes per second for this peer.
50    pub upload_bytes_per_sec: u64,
51    /// Maximum inbound bytes per second for this peer.
52    pub download_bytes_per_sec: u64,
53    /// Burst multiplier: tokens can accumulate up to `quota * burst_factor`.
54    pub burst_factor: f64,
55}
56
57impl BandwidthQuota {
58    /// Returns the upload burst cap in bytes.
59    #[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    /// Returns the download burst cap in bytes.
65    #[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// ── PeerBucket ────────────────────────────────────────────────────────────────
72
73/// Token-bucket state for a single peer.
74///
75/// Timestamps (`last_refill`) are in **milliseconds** and can be any
76/// monotonically-increasing integer (e.g. milliseconds since UNIX epoch, or
77/// from a test counter).
78#[derive(Debug, Clone)]
79pub struct PeerBucket {
80    /// Unique peer identifier.
81    pub peer_id: String,
82    /// Available upload tokens (bytes).  May not exceed the burst cap.
83    pub upload_tokens: f64,
84    /// Available download tokens (bytes).  May not exceed the burst cap.
85    pub download_tokens: f64,
86    /// Timestamp (ms) of the last token-refill.
87    pub last_refill: u64,
88    /// Per-peer quota configuration.
89    pub quota: BandwidthQuota,
90    /// Cumulative upload bytes consumed (granted) since registration.
91    pub bytes_used_up: u64,
92    /// Cumulative download bytes consumed (granted) since registration.
93    pub bytes_used_down: u64,
94}
95
96impl PeerBucket {
97    /// Creates a new bucket initialised with one second's worth of tokens (capped
98    /// at the burst cap).
99    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    /// Refills tokens based on elapsed time since `last_refill`.
114    ///
115    /// Tokens are added proportionally to the elapsed milliseconds and capped at
116    /// the per-quota burst ceiling.
117    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// ── BudgetConfig ──────────────────────────────────────────────────────────────
142
143/// Global configuration for [`BandwidthBudgetManager`].
144#[derive(Debug, Clone)]
145pub struct BudgetConfig {
146    /// Quota applied to newly registered peers (unless overridden).
147    pub default_quota: BandwidthQuota,
148    /// Maximum total upload bytes that can be granted across **all** peers.
149    /// `try_consume_upload` returns `false` once this is reached.
150    pub global_upload_cap: u64,
151    /// Maximum total download bytes across all peers.
152    pub global_download_cap: u64,
153    /// Floor value for token buckets; prevents tokens going below this.
154    pub min_tokens: f64,
155    /// Remove peers with no active tokens after this many accounting operations.
156    /// Currently used as a hint for future GC; the manager tracks an internal
157    /// operation counter and prunes fully-drained peers when the counter exceeds
158    /// this threshold.
159    pub gc_threshold: usize,
160}
161
162// ── BudgetStats ───────────────────────────────────────────────────────────────
163
164/// Aggregate statistics collected by [`BandwidthBudgetManager`].
165#[derive(Debug, Clone, Default)]
166pub struct BudgetStats {
167    /// Total upload bytes granted across all peers and all time.
168    pub total_granted_up: u64,
169    /// Total download bytes granted across all peers and all time.
170    pub total_granted_down: u64,
171    /// Total upload requests rejected (insufficient tokens or global cap).
172    pub total_rejected_up: u64,
173    /// Total download requests rejected.
174    pub total_rejected_down: u64,
175    /// Number of peers currently registered.
176    pub active_peers: u64,
177}
178
179// ── BandwidthBudgetManager ────────────────────────────────────────────────────
180
181/// Per-peer token-bucket bandwidth manager.
182///
183/// Call [`refill`] or [`refill_all`] periodically (or lazily before each
184/// consume call) to add tokens, then use [`try_consume_upload`] /
185/// [`try_consume_download`] to request bandwidth.
186///
187/// [`refill`]: BandwidthBudgetManager::refill
188/// [`refill_all`]: BandwidthBudgetManager::refill_all
189/// [`try_consume_upload`]: BandwidthBudgetManager::try_consume_upload
190/// [`try_consume_download`]: BandwidthBudgetManager::try_consume_download
191pub struct BandwidthBudgetManager {
192    config: BudgetConfig,
193    peers: HashMap<String, PeerBucket>,
194    global_upload_used: u64,
195    global_download_used: u64,
196    stats: BudgetStats,
197    /// Internal operation counter for GC triggering.
198    op_count: usize,
199}
200
201impl BandwidthBudgetManager {
202    // ── Construction ──────────────────────────────────────────────────────────
203
204    /// Creates a new manager with the supplied configuration.
205    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    // ── Peer registration ─────────────────────────────────────────────────────
217
218    /// Registers a peer using the default quota.
219    ///
220    /// If the peer is already registered this is a no-op.
221    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    /// Registers a peer with a custom quota, overriding the default.
230    ///
231    /// If the peer already exists its quota is **replaced** and tokens are
232    /// re-initialised.
233    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    // ── Token refill ──────────────────────────────────────────────────────────
240
241    /// Refills the token bucket for a single peer based on elapsed time.
242    ///
243    /// Does nothing if the peer is not registered or `now` is not later than
244    /// the peer's `last_refill` timestamp.
245    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    /// Refills token buckets for **all** registered peers.
253    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    // ── Bandwidth consumption ─────────────────────────────────────────────────
261
262    /// Attempts to consume `bytes` of upload bandwidth for `peer_id`.
263    ///
264    /// Returns `true` and deducts tokens when:
265    /// 1. The peer is registered,
266    /// 2. The peer has enough upload tokens,
267    /// 3. The global upload cap has not been reached.
268    ///
269    /// Returns `false` (and does **not** deduct tokens) in all other cases.
270    pub fn try_consume_upload(&mut self, peer_id: &str, bytes: u64, now: u64) -> bool {
271        // Lazy refill before consumption.
272        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    /// Attempts to consume `bytes` of download bandwidth for `peer_id`.
305    ///
306    /// Returns `true` and deducts tokens when:
307    /// 1. The peer is registered,
308    /// 2. The peer has enough download tokens,
309    /// 3. The global download cap has not been reached.
310    ///
311    /// Returns `false` (and does **not** deduct tokens) in all other cases.
312    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    // ── Queries ───────────────────────────────────────────────────────────────
346
347    /// Returns the number of remaining upload tokens for `peer_id`, floored to 0.
348    ///
349    /// Returns `0` for unknown peers.
350    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    /// Returns the number of remaining download tokens for `peer_id`, floored to 0.
358    ///
359    /// Returns `0` for unknown peers.
360    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    // ── Peer management ───────────────────────────────────────────────────────
368
369    /// Removes a peer from the manager.
370    ///
371    /// Returns `true` if the peer existed, `false` otherwise.
372    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    /// Returns the number of currently registered peers.
381    pub fn peer_count(&self) -> usize {
382        self.peers.len()
383    }
384
385    /// Returns a reference to the aggregate statistics.
386    pub fn stats(&self) -> &BudgetStats {
387        &self.stats
388    }
389
390    // ── Internal helpers ──────────────────────────────────────────────────────
391
392    /// Increments the operation counter and runs GC when the threshold is met.
393    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    /// Removes peers whose token buckets are completely empty (both upload and
402    /// download tokens at or below zero) and who have no accumulated usage.
403    ///
404    /// This is a heuristic GC pass meant to reclaim memory for ephemeral peers
405    /// that were registered but never granted any bandwidth.
406    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// ── Tests ─────────────────────────────────────────────────────────────────────
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    // ── Helpers ───────────────────────────────────────────────────────────────
424
425    fn default_quota() -> BandwidthQuota {
426        BandwidthQuota {
427            upload_bytes_per_sec: 1_000,   // 1 KB/s
428            download_bytes_per_sec: 2_000, // 2 KB/s
429            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    // T01 ── register_peer adds the peer ──────────────────────────────────────
445
446    #[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    // T02 ── register_peer is idempotent ──────────────────────────────────────
455
456    #[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); // duplicate — should not add again
461        assert_eq!(mgr.peer_count(), 1);
462    }
463
464    // T03 ── register_peer_with_quota sets custom quota ───────────────────────
465
466    #[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        // Tokens should be initialised at burst cap = quota (since burst_factor=1.0)
476        assert_eq!(mgr.remaining_upload("p1"), 9_999);
477        assert_eq!(mgr.remaining_download("p1"), 8_888);
478    }
479
480    // T04 ── initial tokens equal burst cap ───────────────────────────────────
481
482    #[test]
483    fn t04_initial_tokens_equal_burst_cap() {
484        let mut mgr = make_manager();
485        mgr.register_peer("p1", 0);
486        // quota: upload=1000, burst_factor=2.0 → cap=2000
487        assert_eq!(mgr.remaining_upload("p1"), 2_000);
488        assert_eq!(mgr.remaining_download("p1"), 4_000);
489    }
490
491    // T05 ── try_consume_upload succeeds when tokens are available ─────────────
492
493    #[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    // T06 ── try_consume_download succeeds when tokens are available ───────────
503
504    #[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    // T07 ── try_consume_upload fails when tokens are exhausted ───────────────
514
515    #[test]
516    fn t07_consume_upload_rejected_no_tokens() {
517        let mut mgr = make_manager();
518        mgr.register_peer("p1", 0);
519        // Drain all upload tokens (burst cap = 2000)
520        assert!(mgr.try_consume_upload("p1", 2_000, 0));
521        // Now should fail
522        let ok = mgr.try_consume_upload("p1", 1, 0);
523        assert!(!ok);
524    }
525
526    // T08 ── try_consume_download fails when tokens are exhausted ─────────────
527
528    #[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    // T09 ── refill adds tokens over elapsed time ──────────────────────────────
538
539    #[test]
540    fn t09_refill_adds_tokens() {
541        let mut mgr = make_manager();
542        mgr.register_peer("p1", 0);
543        // Drain all tokens
544        assert!(mgr.try_consume_upload("p1", 2_000, 0));
545        assert_eq!(mgr.remaining_upload("p1"), 0);
546        // After 1000 ms: 1 KB/s × 1 s = 1000 tokens added (capped at burst=2000)
547        mgr.refill("p1", 1_000);
548        assert_eq!(mgr.remaining_upload("p1"), 1_000);
549    }
550
551    // T10 ── refill caps tokens at burst ceiling ───────────────────────────────
552
553    #[test]
554    fn t10_refill_caps_at_burst() {
555        let mut mgr = make_manager();
556        mgr.register_peer("p1", 0);
557        // Partially drain: 1000 tokens remaining (out of 2000 burst cap)
558        assert!(mgr.try_consume_upload("p1", 1_000, 0));
559        // Refill 10 s → would add 10,000 tokens, but burst cap is 2000
560        mgr.refill("p1", 10_000);
561        assert_eq!(mgr.remaining_upload("p1"), 2_000);
562    }
563
564    // T11 ── burst_factor allows consuming beyond steady rate ─────────────────
565
566    #[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        // Burst cap = 1000 × 3 = 3000; consuming 2500 should succeed.
583        assert!(mgr.try_consume_upload("p1", 2_500, 0));
584    }
585
586    // T12 ── global_upload_cap blocks consumption ─────────────────────────────
587
588    #[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        // First consume uses 100 bytes (exactly at the cap).
604        assert!(mgr.try_consume_upload("p1", 100, 0));
605        // Second consume must fail because global cap is exhausted.
606        assert!(!mgr.try_consume_upload("p1", 1, 0));
607    }
608
609    // T13 ── global_download_cap blocks consumption ────────────────────────────
610
611    #[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    // T14 ── per-peer quota override is respected ──────────────────────────────
631
632    #[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        // Default quota allows 2000 upload, but "slow" can only do 50.
642        assert!(mgr.try_consume_upload("slow", 50, 0));
643        assert!(!mgr.try_consume_upload("slow", 1, 0));
644    }
645
646    // T15 ── remove_peer returns true for existing peer ────────────────────────
647
648    #[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    // T16 ── remove_peer returns false for unknown peer ────────────────────────
656
657    #[test]
658    fn t16_remove_peer_returns_false_unknown() {
659        let mut mgr = make_manager();
660        assert!(!mgr.remove_peer("ghost"));
661    }
662
663    // T17 ── peer_count decrements after remove ───────────────────────────────
664
665    #[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    // T18 ── refill_all refreshes all peers ───────────────────────────────────
676
677    #[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        // Drain both
683        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        // Refill all at t=2000 ms → 2 s × 1000 B/s = 2000 tokens (= burst cap)
688        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    // T19 ── stats track granted bytes ────────────────────────────────────────
694
695    #[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    // T20 ── stats track rejected counts ──────────────────────────────────────
707
708    #[test]
709    fn t20_stats_rejected_counts() {
710        let mut mgr = make_manager();
711        mgr.register_peer("p1", 0);
712        // Exhaust tokens then attempt another consume
713        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    // T21 ── stats active_peers reflects real count ────────────────────────────
723
724    #[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    // T22 ── zero-byte consume always succeeds ─────────────────────────────────
735
736    #[test]
737    fn t22_zero_byte_consume_always_succeeds() {
738        let mut mgr = make_manager();
739        mgr.register_peer("p1", 0);
740        // Drain all tokens first
741        assert!(mgr.try_consume_upload("p1", 2_000, 0));
742        // Zero-byte consume must succeed even with empty bucket
743        assert!(mgr.try_consume_upload("p1", 0, 0));
744        assert!(mgr.try_consume_download("p1", 0, 0));
745    }
746
747    // T23 ── consume for unregistered peer returns false ───────────────────────
748
749    #[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    // T24 ── remaining_upload returns 0 for unknown peer ──────────────────────
759
760    #[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    // T25 ── rapid fill-drain cycle maintains correctness ─────────────────────
768    //
769    // Quota: 1000 B/s, burst_factor=2.0 → burst cap = 2000 bytes.
770    // Cycle: drain 500 bytes, then advance 1000 ms (refilling 1000 bytes).
771    // Net gain per cycle = 1000 − 500 = +500, so tokens grow until they hit the cap.
772
773    #[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            // Consume 500 bytes — always affordable since refill >= 1000 B/s per second.
781            assert!(
782                mgr.try_consume_upload("p1", 500, now),
783                "consume failed at t={now}"
784            );
785            now += 1_000; // advance 1 s → refill 1000 tokens (lazy refill on next consume)
786            mgr.refill("p1", now);
787        }
788        // After 10 cycles tokens must be ≤ burst cap and ≥ 0.
789        let remaining = mgr.remaining_upload("p1");
790        assert!(remaining <= 2_000, "tokens exceeded burst cap: {remaining}");
791    }
792
793    // T26 ── refill does not rewind time (now <= last_refill is no-op) ─────────
794
795    #[test]
796    fn t26_refill_no_rewind() {
797        let mut mgr = make_manager();
798        mgr.register_peer("p1", 1_000);
799        // Drain fully at t=1000
800        assert!(mgr.try_consume_upload("p1", 2_000, 1_000));
801        assert_eq!(mgr.remaining_upload("p1"), 0);
802        // Refill with an older timestamp — should be a no-op
803        mgr.refill("p1", 500);
804        assert_eq!(mgr.remaining_upload("p1"), 0);
805    }
806
807    // T27 ── multiple peers do not share tokens ────────────────────────────────
808
809    #[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        // Drain all of "a"
815        assert!(mgr.try_consume_upload("a", 2_000, 0));
816        // "b" should still have full tokens
817        assert_eq!(mgr.remaining_upload("b"), 2_000);
818        // "b" can still consume
819        assert!(mgr.try_consume_upload("b", 2_000, 0));
820    }
821
822    // T28 ── global cap shared across peers ───────────────────────────────────
823
824    #[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        // "a" consumes 200, "b" consumes 100 → total = 300 = cap
841        assert!(mgr.try_consume_upload("a", 200, 0));
842        assert!(mgr.try_consume_upload("b", 100, 0));
843        // Next consume (either peer) must fail
844        assert!(!mgr.try_consume_upload("a", 1, 0));
845        assert!(!mgr.try_consume_upload("b", 1, 0));
846    }
847}