1use std::collections::HashMap;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
13pub enum AllocationStrategy {
14 EqualShare,
16 WeightedFair,
18 MaxMinFair,
21}
22
23#[derive(Clone, Debug)]
27pub struct PeerAllocation {
28 pub peer_id: String,
30 pub weight: f64,
32 pub min_bps: u64,
34 pub max_bps: u64,
36 pub allocated_bps: u64,
38}
39
40impl PeerAllocation {
41 fn new(peer_id: String, weight: f64, min_bps: u64, max_bps: u64) -> Self {
42 Self {
43 peer_id,
44 weight,
45 min_bps,
46 max_bps,
47 allocated_bps: 0,
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
56pub struct AllocationStats {
57 pub total_bps: u64,
59 pub allocated_bps: u64,
61 pub unallocated_bps: u64,
63 pub peer_count: usize,
65}
66
67impl AllocationStats {
68 pub fn utilization(&self) -> f64 {
71 if self.total_bps == 0 {
72 return 0.0;
73 }
74 self.allocated_bps as f64 / self.total_bps as f64
75 }
76}
77
78pub struct PeerBandwidthAllocator {
82 pub peers: HashMap<String, PeerAllocation>,
84 pub total_bps: u64,
86 pub strategy: AllocationStrategy,
88}
89
90impl PeerBandwidthAllocator {
91 pub fn new(total_bps: u64, strategy: AllocationStrategy) -> Self {
93 Self {
94 peers: HashMap::new(),
95 total_bps,
96 strategy,
97 }
98 }
99
100 pub fn add_peer(&mut self, peer_id: String, weight: f64, min_bps: u64, max_bps: u64) {
102 self.peers
103 .entry(peer_id.clone())
104 .and_modify(|p| {
105 p.weight = weight;
106 p.min_bps = min_bps;
107 p.max_bps = max_bps;
108 })
109 .or_insert_with(|| PeerAllocation::new(peer_id, weight, min_bps, max_bps));
110 }
111
112 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
114 self.peers.remove(peer_id).is_some()
115 }
116
117 pub fn run_allocation(&mut self) {
120 let count = self.peers.len();
121 if count == 0 {
122 return;
123 }
124
125 match self.strategy {
126 AllocationStrategy::EqualShare => self.run_equal_share(count),
127 AllocationStrategy::WeightedFair => self.run_weighted_fair(),
128 AllocationStrategy::MaxMinFair => self.run_max_min_fair(count),
129 }
130 }
131
132 fn run_equal_share(&mut self, count: usize) {
135 let share = self.total_bps / count as u64;
136 for peer in self.peers.values_mut() {
137 peer.allocated_bps = share.clamp(peer.min_bps, peer.max_bps);
138 }
139 }
140
141 fn run_weighted_fair(&mut self) {
144 let sum_weights: f64 = self.peers.values().map(|p| p.weight.max(0.0)).sum();
145
146 if sum_weights <= 0.0 {
147 for peer in self.peers.values_mut() {
149 peer.allocated_bps = peer.min_bps.min(peer.max_bps);
150 }
151 return;
152 }
153
154 let total = self.total_bps as f64;
155 for peer in self.peers.values_mut() {
156 let w = peer.weight.max(0.0);
157 let proportional = ((w / sum_weights) * total) as u64;
158 peer.allocated_bps = proportional.clamp(peer.min_bps, peer.max_bps);
159 }
160 }
161
162 fn run_max_min_fair(&mut self, count: usize) {
165 let total_min: u64 = self.peers.values().map(|p| p.min_bps.min(p.max_bps)).sum();
168
169 if total_min >= self.total_bps {
172 let total_bps = self.total_bps;
173 let total_min_f = total_min as f64;
174 for peer in self.peers.values_mut() {
175 let effective_min = peer.min_bps.min(peer.max_bps) as f64;
176 let share = if total_min_f > 0.0 {
177 ((effective_min / total_min_f) * total_bps as f64) as u64
178 } else {
179 0
180 };
181 peer.allocated_bps = share.min(peer.max_bps);
182 }
183 return;
184 }
185
186 for peer in self.peers.values_mut() {
188 peer.allocated_bps = peer.min_bps.min(peer.max_bps);
189 }
190
191 let mut remaining = self.total_bps.saturating_sub(total_min);
194 let mut uncapped_count = count;
196
197 loop {
198 if remaining == 0 || uncapped_count == 0 {
199 break;
200 }
201
202 let fair_share = remaining / uncapped_count as u64;
203 if fair_share == 0 {
204 break;
205 }
206
207 let mut newly_capped = 0u64; let mut still_uncapped = 0usize;
209
210 for peer in self.peers.values_mut() {
211 if peer.allocated_bps >= peer.max_bps {
212 continue;
214 }
215 let candidate = peer.allocated_bps + fair_share;
216 if candidate >= peer.max_bps {
217 newly_capped += candidate - peer.max_bps;
218 peer.allocated_bps = peer.max_bps;
219 } else {
220 peer.allocated_bps = candidate;
221 still_uncapped += 1;
222 }
223 }
224
225 remaining = newly_capped;
226 uncapped_count = still_uncapped;
227 }
228
229 if remaining > 0 {
233 if let Some(peer) = self
234 .peers
235 .values_mut()
236 .find(|p| p.allocated_bps < p.max_bps)
237 {
238 let extra = remaining.min(peer.max_bps - peer.allocated_bps);
239 peer.allocated_bps += extra;
240 }
241 }
242 }
243
244 pub fn allocation_for(&self, peer_id: &str) -> Option<u64> {
248 self.peers.get(peer_id).map(|p| p.allocated_bps)
249 }
250
251 pub fn stats(&self) -> AllocationStats {
253 let allocated_bps: u64 = self.peers.values().map(|p| p.allocated_bps).sum();
254 let unallocated_bps = self.total_bps.saturating_sub(allocated_bps);
255 AllocationStats {
256 total_bps: self.total_bps,
257 allocated_bps,
258 unallocated_bps,
259 peer_count: self.peers.len(),
260 }
261 }
262}
263
264#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn alloc_equal_peers(n: usize, total_bps: u64) -> PeerBandwidthAllocator {
272 let mut alloc = PeerBandwidthAllocator::new(total_bps, AllocationStrategy::EqualShare);
273 for i in 0..n {
274 alloc.add_peer(format!("peer{i}"), 1.0, 0, u64::MAX);
275 }
276 alloc
277 }
278
279 #[test]
283 fn t01_equal_share_distributes_evenly() {
284 let mut alloc = alloc_equal_peers(4, 1000);
285 alloc.run_allocation();
286 for peer in alloc.peers.values() {
287 assert_eq!(
288 peer.allocated_bps, 250,
289 "peer {} got unexpected share",
290 peer.peer_id
291 );
292 }
293 }
294
295 #[test]
297 fn t02_equal_share_respects_max_cap() {
298 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::EqualShare);
299 alloc.add_peer("p0".into(), 1.0, 0, 100); alloc.add_peer("p1".into(), 1.0, 0, u64::MAX);
301 alloc.run_allocation();
302 assert_eq!(alloc.allocation_for("p0"), Some(100));
303 assert_eq!(alloc.allocation_for("p1"), Some(500));
304 }
305
306 #[test]
308 fn t03_equal_share_respects_min_guarantee() {
309 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::EqualShare);
310 alloc.add_peer("p0".into(), 1.0, 600, u64::MAX); alloc.add_peer("p1".into(), 1.0, 0, u64::MAX);
312 alloc.run_allocation();
313 assert_eq!(alloc.allocation_for("p0"), Some(600));
314 assert_eq!(alloc.allocation_for("p1"), Some(500));
315 }
316
317 #[test]
321 fn t04_weighted_fair_proportional() {
322 let mut alloc = PeerBandwidthAllocator::new(1200, AllocationStrategy::WeightedFair);
323 alloc.add_peer("p0".into(), 1.0, 0, u64::MAX);
324 alloc.add_peer("p1".into(), 2.0, 0, u64::MAX);
325 alloc.add_peer("p2".into(), 3.0, 0, u64::MAX);
326 alloc.run_allocation();
327 assert_eq!(alloc.allocation_for("p0"), Some(200));
329 assert_eq!(alloc.allocation_for("p1"), Some(400));
330 assert_eq!(alloc.allocation_for("p2"), Some(600));
331 }
332
333 #[test]
335 fn t05_weighted_fair_respects_max_cap() {
336 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::WeightedFair);
337 alloc.add_peer("heavy".into(), 9.0, 0, 200); alloc.add_peer("light".into(), 1.0, 0, u64::MAX);
339 alloc.run_allocation();
340 assert_eq!(alloc.allocation_for("heavy"), Some(200));
341 assert_eq!(alloc.allocation_for("light"), Some(100));
342 }
343
344 #[test]
346 fn t06_weight_zero_peer_gets_min() {
347 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::WeightedFair);
348 alloc.add_peer("zero".into(), 0.0, 50, u64::MAX);
349 alloc.add_peer("normal".into(), 1.0, 0, u64::MAX);
350 alloc.run_allocation();
351 assert_eq!(alloc.allocation_for("zero"), Some(50));
353 assert_eq!(alloc.allocation_for("normal"), Some(1000));
355 }
356
357 #[test]
361 fn t07_max_min_fair_satisfies_minimums() {
362 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::MaxMinFair);
363 alloc.add_peer("p0".into(), 1.0, 100, u64::MAX);
364 alloc.add_peer("p1".into(), 1.0, 200, u64::MAX);
365 alloc.add_peer("p2".into(), 1.0, 50, u64::MAX);
366 alloc.run_allocation();
367 assert!(
369 alloc
370 .allocation_for("p0")
371 .expect("test: peer should be registered and have an allocation")
372 >= 100
373 );
374 assert!(
375 alloc
376 .allocation_for("p1")
377 .expect("test: peer should be registered and have an allocation")
378 >= 200
379 );
380 assert!(
381 alloc
382 .allocation_for("p2")
383 .expect("test: peer should be registered and have an allocation")
384 >= 50
385 );
386 }
387
388 #[test]
390 fn t08_max_min_fair_respects_max_cap() {
391 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::MaxMinFair);
392 alloc.add_peer("capped".into(), 1.0, 0, 100);
393 alloc.add_peer("free".into(), 1.0, 0, u64::MAX);
394 alloc.run_allocation();
395 assert!(
396 alloc
397 .allocation_for("capped")
398 .expect("test: peer should be registered and have an allocation")
399 <= 100
400 );
401 }
402
403 #[test]
405 fn t09_max_min_fair_equal_remainder() {
406 let mut alloc = PeerBandwidthAllocator::new(900, AllocationStrategy::MaxMinFair);
408 alloc.add_peer("a".into(), 1.0, 0, u64::MAX);
409 alloc.add_peer("b".into(), 1.0, 0, u64::MAX);
410 alloc.add_peer("c".into(), 1.0, 0, u64::MAX);
411 alloc.run_allocation();
412 let a = alloc
413 .allocation_for("a")
414 .expect("test: peer should be registered and have an allocation");
415 let b = alloc
416 .allocation_for("b")
417 .expect("test: peer should be registered and have an allocation");
418 let c = alloc
419 .allocation_for("c")
420 .expect("test: peer should be registered and have an allocation");
421 assert_eq!(a, 300);
422 assert_eq!(b, 300);
423 assert_eq!(c, 300);
424 }
425
426 #[test]
428 fn t10_max_min_fair_budget_below_minimums() {
429 let mut alloc = PeerBandwidthAllocator::new(100, AllocationStrategy::MaxMinFair);
430 alloc.add_peer("x".into(), 1.0, 200, u64::MAX); alloc.add_peer("y".into(), 1.0, 200, u64::MAX);
432 alloc.run_allocation();
433 let x = alloc
434 .allocation_for("x")
435 .expect("test: allocation_for x should return Some after run_allocation");
436 let y = alloc
437 .allocation_for("y")
438 .expect("test: allocation_for y should return Some after run_allocation");
439 assert!(x + y <= 100, "allocated more than total: x={x} y={y}");
441 }
442
443 #[test]
447 fn t11_remove_peer_returns_correct_bool() {
448 let mut alloc = alloc_equal_peers(2, 1000);
449 assert!(alloc.remove_peer("peer0"));
450 assert!(!alloc.remove_peer("peer0")); assert!(!alloc.remove_peer("nonexistent"));
452 }
453
454 #[test]
456 fn t12_remove_then_rerun() {
457 let mut alloc = alloc_equal_peers(3, 900);
458 alloc.run_allocation();
459 assert_eq!(alloc.allocation_for("peer0"), Some(300));
460 alloc.remove_peer("peer2");
461 alloc.run_allocation();
462 assert_eq!(alloc.allocation_for("peer0"), Some(450));
464 assert_eq!(alloc.allocation_for("peer1"), Some(450));
465 assert_eq!(alloc.allocation_for("peer2"), None);
466 }
467
468 #[test]
472 fn t13_allocation_for_some_and_none() {
473 let mut alloc = alloc_equal_peers(1, 1000);
474 alloc.run_allocation();
475 assert!(alloc.allocation_for("peer0").is_some());
476 assert!(alloc.allocation_for("ghost").is_none());
477 }
478
479 #[test]
483 fn t14_stats_utilization() {
484 let mut alloc = alloc_equal_peers(2, 1000);
485 alloc.run_allocation();
486 let stats = alloc.stats();
487 assert!((stats.utilization() - 1.0).abs() < f64::EPSILON);
489 }
490
491 #[test]
493 fn t15_stats_fields_consistent() {
494 let mut alloc = alloc_equal_peers(3, 900);
495 alloc.run_allocation();
496 let stats = alloc.stats();
497 assert_eq!(stats.total_bps, 900);
498 assert_eq!(stats.allocated_bps, 900);
499 assert_eq!(stats.unallocated_bps, 0);
500 assert_eq!(stats.peer_count, 3);
501 }
502
503 #[test]
507 fn t16_empty_peers_no_panic() {
508 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::MaxMinFair);
509 alloc.run_allocation(); let stats = alloc.stats();
511 assert_eq!(stats.peer_count, 0);
512 assert_eq!(stats.allocated_bps, 0);
513 assert_eq!(stats.utilization(), 0.0);
514 }
515
516 #[test]
518 fn t17_single_peer_gets_full_budget() {
519 let mut alloc = PeerBandwidthAllocator::new(5000, AllocationStrategy::WeightedFair);
520 alloc.add_peer("only".into(), 1.0, 0, u64::MAX);
521 alloc.run_allocation();
522 assert_eq!(alloc.allocation_for("only"), Some(5000));
523 }
524
525 #[test]
527 fn t18_add_peer_then_rerun() {
528 let mut alloc = PeerBandwidthAllocator::new(600, AllocationStrategy::EqualShare);
529 alloc.add_peer("a".into(), 1.0, 0, u64::MAX);
530 alloc.run_allocation();
531 assert_eq!(alloc.allocation_for("a"), Some(600));
532
533 alloc.add_peer("b".into(), 1.0, 0, u64::MAX);
534 alloc.run_allocation();
535 assert_eq!(alloc.allocation_for("a"), Some(300));
536 assert_eq!(alloc.allocation_for("b"), Some(300));
537 }
538
539 #[test]
541 fn t19_utilization_zero_total() {
542 let alloc = PeerBandwidthAllocator::new(0, AllocationStrategy::EqualShare);
543 assert_eq!(alloc.stats().utilization(), 0.0);
544 }
545
546 #[test]
548 fn t20_max_min_fair_surplus_redistributed() {
549 let mut alloc = PeerBandwidthAllocator::new(1000, AllocationStrategy::MaxMinFair);
552 alloc.add_peer("capped".into(), 1.0, 0, 100);
553 alloc.add_peer("free".into(), 1.0, 0, u64::MAX);
554 alloc.run_allocation();
555 assert_eq!(alloc.allocation_for("capped"), Some(100));
556 assert_eq!(alloc.allocation_for("free"), Some(900));
557 }
558}