Skip to main content

ipfrs_network/
flow_control.rs

1//! Window-based flow control for peer data transfer.
2//!
3//! Provides per-peer sliding window flow control to prevent overwhelming
4//! peers with data faster than they can process. Each peer gets an independent
5//! window that grows on successful transfers and shrinks on congestion.
6
7use std::collections::HashMap;
8
9/// Configuration for the flow control system.
10#[derive(Debug, Clone)]
11pub struct FlowControlConfig {
12    /// Initial window size in bytes for new peers (default: 65536 = 64KB).
13    pub initial_window: u64,
14    /// Maximum window size in bytes (default: 1_048_576 = 1MB).
15    pub max_window: u64,
16    /// Minimum window size in bytes (default: 4096).
17    pub min_window: u64,
18    /// Multiplicative factor for growing the window (default: 1.5).
19    pub growth_factor: f64,
20    /// Multiplicative factor for shrinking the window (default: 0.5).
21    pub shrink_factor: f64,
22}
23
24impl Default for FlowControlConfig {
25    fn default() -> Self {
26        Self {
27            initial_window: 65_536,
28            max_window: 1_048_576,
29            min_window: 4096,
30            growth_factor: 1.5,
31            shrink_factor: 0.5,
32        }
33    }
34}
35
36/// Per-peer flow window tracking bytes in flight and acknowledgements.
37#[derive(Debug, Clone)]
38pub struct FlowWindow {
39    /// Peer identifier.
40    pub peer_id: String,
41    /// Maximum bytes allowed in flight for this peer.
42    pub window_size: u64,
43    /// Bytes currently in flight (sent but not yet acknowledged).
44    pub bytes_in_flight: u64,
45    /// Total bytes acknowledged by this peer.
46    pub bytes_acked: u64,
47    /// Total bytes sent to this peer.
48    pub bytes_sent: u64,
49    /// Number of times a send was rejected because the window was full.
50    pub stall_count: u64,
51}
52
53/// Aggregate statistics across all peers.
54#[derive(Debug, Clone)]
55pub struct FlowControlStats {
56    /// Number of peers with active flow windows.
57    pub peer_count: usize,
58    /// Total stalls across all peers.
59    pub total_stalls: u64,
60    /// Average utilization (bytes_in_flight / window_size) across all peers.
61    pub avg_utilization: f64,
62}
63
64/// Window-based flow control manager for multiple peers.
65///
66/// Tracks per-peer send windows, automatically creates windows for new peers,
67/// and provides grow/shrink operations for adaptive congestion control.
68pub struct PeerFlowControl {
69    config: FlowControlConfig,
70    windows: HashMap<String, FlowWindow>,
71    total_stalls: u64,
72}
73
74impl PeerFlowControl {
75    /// Create a new `PeerFlowControl` with the given configuration.
76    pub fn new(config: FlowControlConfig) -> Self {
77        Self {
78            config,
79            windows: HashMap::new(),
80            total_stalls: 0,
81        }
82    }
83
84    /// Check whether `bytes` can be sent to `peer_id` without exceeding
85    /// the window. Returns `true` even if the peer has no window yet
86    /// (it would be auto-created on `send`).
87    pub fn can_send(&self, peer_id: &str, bytes: u64) -> bool {
88        match self.windows.get(peer_id) {
89            Some(w) => w.bytes_in_flight.saturating_add(bytes) <= w.window_size,
90            None => bytes <= self.config.initial_window,
91        }
92    }
93
94    /// Record `bytes` as sent to `peer_id`.
95    ///
96    /// Auto-creates a window for new peers. Returns an error if sending
97    /// `bytes` would exceed the peer's window size and increments the
98    /// stall counter.
99    pub fn send(&mut self, peer_id: &str, bytes: u64) -> Result<(), String> {
100        let window = self
101            .windows
102            .entry(peer_id.to_string())
103            .or_insert_with(|| FlowWindow {
104                peer_id: peer_id.to_string(),
105                window_size: self.config.initial_window,
106                bytes_in_flight: 0,
107                bytes_acked: 0,
108                bytes_sent: 0,
109                stall_count: 0,
110            });
111
112        if window.bytes_in_flight.saturating_add(bytes) > window.window_size {
113            window.stall_count += 1;
114            self.total_stalls += 1;
115            return Err(format!(
116                "window full for peer {}: in_flight={} + bytes={} > window={}",
117                peer_id, window.bytes_in_flight, bytes, window.window_size
118            ));
119        }
120
121        window.bytes_in_flight = window.bytes_in_flight.saturating_add(bytes);
122        window.bytes_sent = window.bytes_sent.saturating_add(bytes);
123        Ok(())
124    }
125
126    /// Acknowledge `bytes` from `peer_id`, reducing bytes in flight.
127    ///
128    /// If the peer has no window this is a no-op. The in-flight counter
129    /// is clamped to zero (will not underflow).
130    pub fn ack(&mut self, peer_id: &str, bytes: u64) {
131        if let Some(w) = self.windows.get_mut(peer_id) {
132            w.bytes_in_flight = w.bytes_in_flight.saturating_sub(bytes);
133            w.bytes_acked = w.bytes_acked.saturating_add(bytes);
134        }
135    }
136
137    /// Grow the window for `peer_id` by `growth_factor`, clamped to `max_window`.
138    ///
139    /// No-op if the peer has no window.
140    pub fn grow_window(&mut self, peer_id: &str) {
141        if let Some(w) = self.windows.get_mut(peer_id) {
142            let new_size = (w.window_size as f64 * self.config.growth_factor) as u64;
143            w.window_size = new_size.min(self.config.max_window);
144        }
145    }
146
147    /// Shrink the window for `peer_id` by `shrink_factor`, clamped to `min_window`.
148    ///
149    /// No-op if the peer has no window.
150    pub fn shrink_window(&mut self, peer_id: &str) {
151        if let Some(w) = self.windows.get_mut(peer_id) {
152            let new_size = (w.window_size as f64 * self.config.shrink_factor) as u64;
153            w.window_size = new_size.max(self.config.min_window);
154        }
155    }
156
157    /// Get a reference to the flow window for `peer_id`, if it exists.
158    pub fn get_window(&self, peer_id: &str) -> Option<&FlowWindow> {
159        self.windows.get(peer_id)
160    }
161
162    /// Compute the utilization ratio (bytes_in_flight / window_size) for
163    /// `peer_id`. Returns `None` if the peer has no window.
164    pub fn utilization(&self, peer_id: &str) -> Option<f64> {
165        self.windows.get(peer_id).map(|w| {
166            if w.window_size == 0 {
167                0.0
168            } else {
169                w.bytes_in_flight as f64 / w.window_size as f64
170            }
171        })
172    }
173
174    /// Remove the flow window for `peer_id`. Returns `true` if the peer
175    /// was present.
176    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
177        self.windows.remove(peer_id).is_some()
178    }
179
180    /// Return the number of peers with active flow windows.
181    pub fn peer_count(&self) -> usize {
182        self.windows.len()
183    }
184
185    /// Compute aggregate statistics across all peers.
186    pub fn stats(&self) -> FlowControlStats {
187        let peer_count = self.windows.len();
188        let avg_utilization = if peer_count == 0 {
189            0.0
190        } else {
191            let total: f64 = self
192                .windows
193                .values()
194                .map(|w| {
195                    if w.window_size == 0 {
196                        0.0
197                    } else {
198                        w.bytes_in_flight as f64 / w.window_size as f64
199                    }
200                })
201                .sum();
202            total / peer_count as f64
203        };
204
205        FlowControlStats {
206            peer_count,
207            total_stalls: self.total_stalls,
208            avg_utilization,
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn default_ctrl() -> PeerFlowControl {
218        PeerFlowControl::new(FlowControlConfig::default())
219    }
220
221    // ---- Basic send / ack lifecycle ----
222
223    #[test]
224    fn test_send_ack_lifecycle() {
225        let mut fc = default_ctrl();
226        fc.send("peer1", 1000).expect("send should succeed");
227        let w = fc.get_window("peer1").expect("window should exist");
228        assert_eq!(w.bytes_in_flight, 1000);
229        assert_eq!(w.bytes_sent, 1000);
230
231        fc.ack("peer1", 500);
232        let w = fc.get_window("peer1").expect("window should exist");
233        assert_eq!(w.bytes_in_flight, 500);
234        assert_eq!(w.bytes_acked, 500);
235    }
236
237    #[test]
238    fn test_send_ack_full_cycle() {
239        let mut fc = default_ctrl();
240        fc.send("p", 2000).expect("ok");
241        fc.ack("p", 2000);
242        let w = fc.get_window("p").expect("exists");
243        assert_eq!(w.bytes_in_flight, 0);
244        assert_eq!(w.bytes_acked, 2000);
245        assert_eq!(w.bytes_sent, 2000);
246    }
247
248    // ---- can_send ----
249
250    #[test]
251    fn test_can_send_new_peer() {
252        let fc = default_ctrl();
253        assert!(fc.can_send("unknown", 65_536));
254        assert!(!fc.can_send("unknown", 65_537));
255    }
256
257    #[test]
258    fn test_can_send_existing_peer() {
259        let mut fc = default_ctrl();
260        fc.send("p", 60_000).expect("ok");
261        assert!(fc.can_send("p", 5_536));
262        assert!(!fc.can_send("p", 5_537));
263    }
264
265    #[test]
266    fn test_can_send_zero_bytes() {
267        let fc = default_ctrl();
268        assert!(fc.can_send("any", 0));
269    }
270
271    // ---- Window full / stall ----
272
273    #[test]
274    fn test_window_full_stall() {
275        let mut fc = default_ctrl();
276        fc.send("p", 65_536).expect("ok");
277        let res = fc.send("p", 1);
278        assert!(res.is_err());
279        let w = fc.get_window("p").expect("exists");
280        assert_eq!(w.stall_count, 1);
281        assert_eq!(fc.total_stalls, 1);
282    }
283
284    #[test]
285    fn test_multiple_stalls() {
286        let mut fc = default_ctrl();
287        fc.send("p", 65_536).expect("ok");
288        for _ in 0..5 {
289            let _ = fc.send("p", 1);
290        }
291        let w = fc.get_window("p").expect("exists");
292        assert_eq!(w.stall_count, 5);
293        assert_eq!(fc.total_stalls, 5);
294    }
295
296    // ---- Grow window ----
297
298    #[test]
299    fn test_grow_window() {
300        let mut fc = default_ctrl();
301        fc.send("p", 0).expect("ok");
302        fc.grow_window("p");
303        let w = fc.get_window("p").expect("exists");
304        assert_eq!(w.window_size, 98_304); // 65536 * 1.5
305    }
306
307    #[test]
308    fn test_grow_window_clamp_max() {
309        let mut fc = PeerFlowControl::new(FlowControlConfig {
310            initial_window: 900_000,
311            max_window: 1_000_000,
312            ..Default::default()
313        });
314        fc.send("p", 0).expect("ok");
315        fc.grow_window("p"); // 900000 * 1.5 = 1350000, clamped to 1000000
316        let w = fc.get_window("p").expect("exists");
317        assert_eq!(w.window_size, 1_000_000);
318    }
319
320    #[test]
321    fn test_grow_window_nonexistent_peer_noop() {
322        let mut fc = default_ctrl();
323        fc.grow_window("ghost"); // should not panic
324        assert!(fc.get_window("ghost").is_none());
325    }
326
327    // ---- Shrink window ----
328
329    #[test]
330    fn test_shrink_window() {
331        let mut fc = default_ctrl();
332        fc.send("p", 0).expect("ok");
333        fc.shrink_window("p");
334        let w = fc.get_window("p").expect("exists");
335        assert_eq!(w.window_size, 32_768); // 65536 * 0.5
336    }
337
338    #[test]
339    fn test_shrink_window_clamp_min() {
340        let mut fc = PeerFlowControl::new(FlowControlConfig {
341            initial_window: 5_000,
342            min_window: 4_096,
343            ..Default::default()
344        });
345        fc.send("p", 0).expect("ok");
346        fc.shrink_window("p"); // 5000 * 0.5 = 2500, clamped to 4096
347        let w = fc.get_window("p").expect("exists");
348        assert_eq!(w.window_size, 4_096);
349    }
350
351    #[test]
352    fn test_shrink_window_nonexistent_peer_noop() {
353        let mut fc = default_ctrl();
354        fc.shrink_window("ghost");
355        assert!(fc.get_window("ghost").is_none());
356    }
357
358    // ---- Utilization ----
359
360    #[test]
361    fn test_utilization_no_peer() {
362        let fc = default_ctrl();
363        assert!(fc.utilization("nope").is_none());
364    }
365
366    #[test]
367    fn test_utilization_zero_in_flight() {
368        let mut fc = default_ctrl();
369        fc.send("p", 0).expect("ok");
370        let u = fc.utilization("p").expect("exists");
371        assert!((u - 0.0).abs() < f64::EPSILON);
372    }
373
374    #[test]
375    fn test_utilization_half() {
376        let mut fc = default_ctrl();
377        fc.send("p", 32_768).expect("ok"); // half of 65536
378        let u = fc.utilization("p").expect("exists");
379        assert!((u - 0.5).abs() < f64::EPSILON);
380    }
381
382    #[test]
383    fn test_utilization_full() {
384        let mut fc = default_ctrl();
385        fc.send("p", 65_536).expect("ok");
386        let u = fc.utilization("p").expect("exists");
387        assert!((u - 1.0).abs() < f64::EPSILON);
388    }
389
390    // ---- Remove peer ----
391
392    #[test]
393    fn test_remove_peer_present() {
394        let mut fc = default_ctrl();
395        fc.send("p", 100).expect("ok");
396        assert!(fc.remove_peer("p"));
397        assert!(fc.get_window("p").is_none());
398        assert_eq!(fc.peer_count(), 0);
399    }
400
401    #[test]
402    fn test_remove_peer_absent() {
403        let mut fc = default_ctrl();
404        assert!(!fc.remove_peer("ghost"));
405    }
406
407    // ---- Auto-create window ----
408
409    #[test]
410    fn test_auto_create_window_on_send() {
411        let mut fc = default_ctrl();
412        assert!(fc.get_window("p").is_none());
413        fc.send("p", 10).expect("ok");
414        let w = fc.get_window("p").expect("exists");
415        assert_eq!(w.window_size, 65_536);
416        assert_eq!(w.peer_id, "p");
417    }
418
419    // ---- Peer count ----
420
421    #[test]
422    fn test_peer_count_empty() {
423        let fc = default_ctrl();
424        assert_eq!(fc.peer_count(), 0);
425    }
426
427    #[test]
428    fn test_peer_count_multiple() {
429        let mut fc = default_ctrl();
430        fc.send("a", 1).expect("ok");
431        fc.send("b", 1).expect("ok");
432        fc.send("c", 1).expect("ok");
433        assert_eq!(fc.peer_count(), 3);
434    }
435
436    // ---- Stats ----
437
438    #[test]
439    fn test_stats_empty() {
440        let fc = default_ctrl();
441        let s = fc.stats();
442        assert_eq!(s.peer_count, 0);
443        assert_eq!(s.total_stalls, 0);
444        assert!((s.avg_utilization - 0.0).abs() < f64::EPSILON);
445    }
446
447    #[test]
448    fn test_stats_with_peers() {
449        let mut fc = default_ctrl();
450        fc.send("a", 65_536).expect("ok"); // util = 1.0
451        fc.send("b", 0).expect("ok"); // util = 0.0
452        let _ = fc.send("a", 1); // stall
453        let s = fc.stats();
454        assert_eq!(s.peer_count, 2);
455        assert_eq!(s.total_stalls, 1);
456        assert!((s.avg_utilization - 0.5).abs() < f64::EPSILON);
457    }
458
459    // ---- Ack on nonexistent peer ----
460
461    #[test]
462    fn test_ack_nonexistent_peer_noop() {
463        let mut fc = default_ctrl();
464        fc.ack("ghost", 1000); // should not panic
465    }
466
467    // ---- Ack more than in flight (saturating) ----
468
469    #[test]
470    fn test_ack_more_than_in_flight() {
471        let mut fc = default_ctrl();
472        fc.send("p", 100).expect("ok");
473        fc.ack("p", 500); // over-ack
474        let w = fc.get_window("p").expect("exists");
475        assert_eq!(w.bytes_in_flight, 0);
476        assert_eq!(w.bytes_acked, 500);
477    }
478
479    // ---- Multiple sends ----
480
481    #[test]
482    fn test_multiple_sends_accumulate() {
483        let mut fc = default_ctrl();
484        fc.send("p", 10_000).expect("ok");
485        fc.send("p", 20_000).expect("ok");
486        fc.send("p", 30_000).expect("ok");
487        let w = fc.get_window("p").expect("exists");
488        assert_eq!(w.bytes_in_flight, 60_000);
489        assert_eq!(w.bytes_sent, 60_000);
490    }
491
492    // ---- Grow then send ----
493
494    #[test]
495    fn test_grow_allows_more_sends() {
496        let mut fc = default_ctrl();
497        fc.send("p", 65_536).expect("fill window");
498        assert!(fc.send("p", 1).is_err());
499        fc.ack("p", 65_536);
500        fc.grow_window("p"); // now 98304
501        fc.send("p", 98_304).expect("ok with grown window");
502        let w = fc.get_window("p").expect("exists");
503        assert_eq!(w.bytes_in_flight, 98_304);
504    }
505
506    // ---- Shrink then send fails ----
507
508    #[test]
509    fn test_shrink_reduces_capacity() {
510        let mut fc = default_ctrl();
511        fc.send("p", 0).expect("ok");
512        fc.shrink_window("p"); // 32768
513        assert!(fc.can_send("p", 32_768));
514        assert!(!fc.can_send("p", 32_769));
515    }
516
517    // ---- Custom config ----
518
519    #[test]
520    fn test_custom_config() {
521        let cfg = FlowControlConfig {
522            initial_window: 1000,
523            max_window: 5000,
524            min_window: 100,
525            growth_factor: 2.0,
526            shrink_factor: 0.25,
527        };
528        let mut fc = PeerFlowControl::new(cfg);
529        fc.send("p", 0).expect("ok");
530        assert_eq!(fc.get_window("p").expect("e").window_size, 1000);
531
532        fc.grow_window("p");
533        assert_eq!(fc.get_window("p").expect("e").window_size, 2000);
534
535        fc.grow_window("p");
536        assert_eq!(fc.get_window("p").expect("e").window_size, 4000);
537
538        fc.grow_window("p"); // 8000, clamped to 5000
539        assert_eq!(fc.get_window("p").expect("e").window_size, 5000);
540
541        fc.shrink_window("p"); // 5000 * 0.25 = 1250
542        assert_eq!(fc.get_window("p").expect("e").window_size, 1250);
543
544        fc.shrink_window("p"); // 1250 * 0.25 = 312
545        fc.shrink_window("p"); // 312 * 0.25 = 78, clamped to 100
546        assert_eq!(fc.get_window("p").expect("e").window_size, 100);
547    }
548
549    // ---- Stats avg_utilization across many peers ----
550
551    #[test]
552    fn test_stats_avg_utilization_three_peers() {
553        let mut fc = default_ctrl();
554        // 65536 window each
555        fc.send("a", 65_536).expect("ok"); // util=1.0
556        fc.send("b", 32_768).expect("ok"); // util=0.5
557        fc.send("c", 16_384).expect("ok"); // util=0.25
558        let s = fc.stats();
559        let expected = (1.0 + 0.5 + 0.25) / 3.0;
560        assert!((s.avg_utilization - expected).abs() < 1e-10);
561    }
562
563    // ---- Stalls across multiple peers ----
564
565    #[test]
566    fn test_stalls_across_multiple_peers() {
567        let mut fc = default_ctrl();
568        fc.send("a", 65_536).expect("ok");
569        fc.send("b", 65_536).expect("ok");
570        let _ = fc.send("a", 1);
571        let _ = fc.send("b", 1);
572        let _ = fc.send("a", 1);
573        assert_eq!(fc.total_stalls, 3);
574        assert_eq!(fc.stats().total_stalls, 3);
575    }
576
577    // ---- Default config values ----
578
579    #[test]
580    fn test_default_config() {
581        let cfg = FlowControlConfig::default();
582        assert_eq!(cfg.initial_window, 65_536);
583        assert_eq!(cfg.max_window, 1_048_576);
584        assert_eq!(cfg.min_window, 4096);
585        assert!((cfg.growth_factor - 1.5).abs() < f64::EPSILON);
586        assert!((cfg.shrink_factor - 0.5).abs() < f64::EPSILON);
587    }
588
589    // ---- Remove then re-add ----
590
591    #[test]
592    fn test_remove_and_readd_peer() {
593        let mut fc = default_ctrl();
594        fc.send("p", 100).expect("ok");
595        fc.grow_window("p");
596        fc.remove_peer("p");
597
598        fc.send("p", 50).expect("ok");
599        let w = fc.get_window("p").expect("exists");
600        // Fresh window after removal
601        assert_eq!(w.window_size, 65_536);
602        assert_eq!(w.bytes_in_flight, 50);
603        assert_eq!(w.bytes_sent, 50);
604    }
605}