1use std::collections::HashSet;
2
3use super::peer_stats::PeerStats;
4use crate::constants;
5
6#[derive(Debug, Clone, PartialEq)]
8pub enum ChokeAction {
9 Unchoke(usize),
11 Choke(usize),
13 NoChange(usize),
15}
16
17#[derive(Debug, Clone)]
19pub struct ChokingConfig {
20 pub max_upload_slots: usize,
22 pub optimistic_unchoke_interval_secs: u64,
24 pub snubbed_timeout_secs: u64,
26 pub choke_rotation_interval_secs: u64,
28}
29
30impl Default for ChokingConfig {
31 fn default() -> Self {
32 Self {
33 max_upload_slots: 4,
34 optimistic_unchoke_interval_secs: 30,
35 snubbed_timeout_secs: 60,
36 choke_rotation_interval_secs: 10,
37 }
38 }
39}
40
41pub struct ChokingAlgorithm {
50 peers: Vec<PeerStats>,
51 config: ChokingConfig,
52 snubbed_peers: HashSet<usize>,
55 current_optimistic_peer: Option<usize>,
57 optimistic_rotation_counter: usize,
59}
60
61impl ChokingAlgorithm {
62 pub fn new(config: ChokingConfig) -> Self {
64 Self {
65 peers: Vec::new(),
66 config,
67 snubbed_peers: HashSet::new(),
68 current_optimistic_peer: None,
69 optimistic_rotation_counter: 0,
70 }
71 }
72
73 pub fn add_peer(&mut self, stats: PeerStats) {
75 self.peers.push(stats);
76 }
77
78 pub fn remove_peer(&mut self, idx: usize) {
80 if idx < self.peers.len() {
81 self.peers.remove(idx);
82 }
83 }
84
85 pub fn len(&self) -> usize {
87 self.peers.len()
88 }
89
90 pub fn is_empty(&self) -> bool {
92 self.peers.is_empty()
93 }
94
95 pub fn rotate_choke(&mut self) -> Vec<ChokeAction> {
106 self.check_snubbed_peers_internal();
108
109 if self.peers.is_empty() {
110 return vec![];
111 }
112
113 let max_slots = self.config.max_upload_slots;
114
115 let mut scored_peers: Vec<(usize, f64)> = self
117 .peers
118 .iter()
119 .enumerate()
120 .map(|(i, peer)| {
121 let is_snubbed = self.snubbed_peers.contains(&i);
122 (i, Self::calculate_peer_score(peer, is_snubbed))
123 })
124 .collect();
125
126 scored_peers.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
128
129 let mut actions = Vec::new();
131 let mut new_unchoked_indices = std::collections::HashSet::new();
132
133 for (rank, &(idx, _)) in scored_peers.iter().enumerate() {
135 if rank < max_slots {
136 if self.peers[idx].am_choking {
138 actions.push(ChokeAction::Unchoke(idx));
139 self.peers[idx].record_unchoke();
140 } else {
141 actions.push(ChokeAction::NoChange(idx));
142 }
143 new_unchoked_indices.insert(idx);
144 } else {
145 if !self.peers[idx].am_choking {
147 actions.push(ChokeAction::Choke(idx));
148 self.peers[idx].record_choke();
149 } else {
150 actions.push(ChokeAction::NoChange(idx));
151 }
152 }
153 }
154
155 actions
156 }
157
158 pub fn optimistically_unchoke(&mut self) -> Option<usize> {
168 let candidates: Vec<usize> = self
174 .peers
175 .iter()
176 .enumerate()
177 .filter(|(i, peer)| {
178 peer.am_choking
179 && peer.peer_interested
180 && !peer.is_snubbed
181 && !self.snubbed_peers.contains(i)
182 && peer.time_since_last_optimistic_unchoke().as_secs()
183 >= self.config.optimistic_unchoke_interval_secs
184 })
185 .map(|(i, _)| i)
186 .collect();
187
188 if candidates.is_empty() {
189 return None;
190 }
191
192 let selected = self.rotate_optimistic_unchoked(&candidates);
194
195 if let Some(peer) = self.peers.get_mut(selected) {
197 peer.record_optimistic_unchoke();
198 }
199 self.current_optimistic_peer = Some(selected);
200
201 Some(selected)
202 }
203
204 pub fn rotate_optimistic_unchoked(&mut self, eligible_peers: &[usize]) -> usize {
215 if eligible_peers.is_empty() {
216 panic!("rotate_optimistic_unchoked called with empty eligible list");
217 }
218
219 if eligible_peers.len() == 1 {
220 return eligible_peers[0];
221 }
222
223 let current_pos = self
225 .current_optimistic_peer
226 .and_then(|curr| eligible_peers.iter().position(|&x| x == curr));
227
228 let next_pos = match current_pos {
230 Some(pos) => (pos + 1) % eligible_peers.len(),
231 None => self.optimistic_rotation_counter % eligible_peers.len(),
232 };
233
234 self.optimistic_rotation_counter = self.optimistic_rotation_counter.wrapping_add(1);
235 eligible_peers[next_pos]
236 }
237
238 pub fn on_data_received(&mut self, peer_idx: usize, bytes: u64) {
241 if let Some(peer) = self.peers.get_mut(peer_idx) {
242 peer.on_data_received(bytes);
243 }
244 self.unsnub_peer(peer_idx);
246 }
247
248 pub fn mark_peer_snubbed(&mut self, peer_id: usize) {
254 if self.snubbed_peers.insert(peer_id) {
255 tracing::debug!("[BT] Peer {} explicitly marked as snubbed", peer_id);
256 }
257 }
258
259 pub fn unsnub_peer(&mut self, peer_id: usize) -> bool {
264 if self.snubbed_peers.remove(&peer_id) {
265 tracing::debug!("[BT] Peer {} un-snubbed (data received)", peer_id);
266 true
267 } else {
268 false
269 }
270 }
271
272 pub fn is_explicitly_snubbed(&self, peer_id: usize) -> bool {
274 self.snubbed_peers.contains(&peer_id)
275 }
276
277 pub fn snubbed_count(&self) -> usize {
279 self.snubbed_peers.len()
280 }
281
282 pub fn check_snubbed_peers(&mut self) -> Vec<usize> {
285 self.check_snubbed_peers_internal()
286 }
287
288 fn check_snubbed_peers_internal(&mut self) -> Vec<usize> {
290 let mut snubbed = vec![];
291 for (i, peer) in self.peers.iter_mut().enumerate() {
292 if peer.check_snubbed(self.config.snubbed_timeout_secs) {
293 snubbed.push(i);
294 }
295 }
296 snubbed
297 }
298
299 fn calculate_peer_score(peer: &PeerStats, is_explicitly_snubbed: bool) -> f64 {
308 let mut score = 0.0;
309
310 score += peer.download_speed * constants::CHOKING_DOWNLOAD_SPEED_WEIGHT;
313
314 score += peer.upload_speed * constants::CHOKING_UPLOAD_SPEED_WEIGHT;
316
317 if peer.is_snubbed || is_explicitly_snubbed {
320 score -= constants::CHOKING_SNUBBED_PENALTY;
321 }
322
323 if peer.peer_interested {
325 score += constants::CHOKING_INTEREST_BONUS;
326 }
327
328 if !peer.am_choking
330 && peer.time_since_last_unchoke().as_secs()
331 < constants::CHOKING_ANTI_CHURN_THRESHOLD_SECS
332 {
333 score += constants::CHOKING_ANTI_CHURN_BONUS;
334 }
335
336 score
337 }
338
339 pub fn get_peer_mut(&mut self, idx: usize) -> Option<&mut PeerStats> {
341 self.peers.get_mut(idx)
342 }
343
344 pub fn get_peer(&self, idx: usize) -> Option<&PeerStats> {
346 self.peers.get(idx)
347 }
348
349 pub fn peers(&self) -> &[PeerStats] {
351 &self.peers
352 }
353
354 pub fn config(&self) -> &ChokingConfig {
356 &self.config
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use std::net::SocketAddr;
364
365 fn create_test_peer(
367 download_speed: f64,
368 upload_speed: f64,
369 am_choking: bool,
370 peer_interested: bool,
371 ) -> PeerStats {
372 let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
373 let mut peer = PeerStats::new([0u8; 20], addr);
374 peer.download_speed = download_speed;
375 peer.upload_speed = upload_speed;
376 peer.am_choking = am_choking;
377 peer.peer_interested = peer_interested;
378 peer
379 }
380
381 #[test]
382 fn test_new_algorithm_empty() {
383 let config = ChokingConfig::default();
384 let algo = ChokingAlgorithm::new(config);
385
386 assert!(algo.is_empty());
387 assert_eq!(algo.len(), 0);
388 }
389
390 #[test]
391 fn test_add_remove_peers() {
392 let config = ChokingConfig::default();
393 let mut algo = ChokingAlgorithm::new(config);
394
395 assert_eq!(algo.len(), 0);
397 let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
398 algo.add_peer(PeerStats::new([0u8; 20], addr));
399 assert_eq!(algo.len(), 1);
400 algo.add_peer(PeerStats::new([0u8; 20], addr));
401 assert_eq!(algo.len(), 2);
402 algo.add_peer(PeerStats::new([0u8; 20], addr));
403 assert_eq!(algo.len(), 3);
404
405 algo.remove_peer(1);
407 assert_eq!(algo.len(), 2);
408
409 algo.remove_peer(0);
411 assert_eq!(algo.len(), 1);
412
413 algo.remove_peer(0);
415 assert!(algo.is_empty());
416 }
417
418 #[test]
419 fn test_rotate_choke_selects_top_k() {
420 let config = ChokingConfig {
421 max_upload_slots: 3,
422 ..Default::default()
423 };
424 let mut algo = ChokingAlgorithm::new(config);
425
426 algo.add_peer(create_test_peer(100000.0, 1000.0, true, true));
429 algo.add_peer(create_test_peer(80000.0, 800.0, true, true));
431 algo.add_peer(create_test_peer(60000.0, 600.0, true, true));
433 algo.add_peer(create_test_peer(40000.0, 400.0, true, true));
435 algo.add_peer(create_test_peer(20000.0, 200.0, true, true));
437 algo.add_peer(create_test_peer(10000.0, 100.0, true, true));
439
440 let actions = algo.rotate_choke();
441
442 let unchoke_count = actions
444 .iter()
445 .filter(|a| matches!(a, ChokeAction::Unchoke(_)))
446 .count();
447
448 assert_eq!(unchoke_count, 3);
450
451 assert_eq!(actions.len(), 6); }
454
455 #[test]
456 fn test_rotate_choke_minimizes_changes() {
457 let config = ChokingConfig {
458 max_upload_slots: 2,
459 ..Default::default()
460 };
461 let mut algo = ChokingAlgorithm::new(config);
462
463 algo.add_peer(create_test_peer(100000.0, 1000.0, false, true)); algo.add_peer(create_test_peer(80000.0, 800.0, false, true)); algo.add_peer(create_test_peer(60000.0, 600.0, true, true)); algo.add_peer(create_test_peer(40000.0, 400.0, true, true)); let actions = algo.rotate_choke();
471
472 let no_change_count = actions
474 .iter()
475 .filter(|a| matches!(a, ChokeAction::NoChange(_)))
476 .count();
477
478 assert!(
480 no_change_count >= 2,
481 "Expected at least 2 NoChange actions, got {}",
482 no_change_count
483 );
484
485 let actions2 = algo.rotate_choke();
487 let no_change_count2 = actions2
488 .iter()
489 .filter(|a| matches!(a, ChokeAction::NoChange(_)))
490 .count();
491
492 assert_eq!(no_change_count2, 4, "Expected all NoChange on second call");
494 }
495
496 #[test]
497 fn test_optimistically_unchoke_selects_choked_peer() {
498 let config = ChokingConfig {
499 optimistic_unchoke_interval_secs: 0, ..Default::default()
501 };
502 let mut algo = ChokingAlgorithm::new(config);
503
504 algo.add_peer(create_test_peer(1000.0, 100.0, true, true)); algo.add_peer(create_test_peer(2000.0, 200.0, false, true)); algo.add_peer(create_test_peer(3000.0, 300.0, true, false)); let result = algo.optimistically_unchoke();
510
511 assert!(
513 result.is_some(),
514 "Expected to select a peer for optimistic unchoke"
515 );
516 assert_eq!(result.unwrap(), 0);
517 }
518
519 #[test]
520 fn test_optimistically_avoids_recent() {
521 let config = ChokingConfig {
522 optimistic_unchoke_interval_secs: 30,
523 ..Default::default()
524 };
525 let mut algo = ChokingAlgorithm::new(config);
526
527 let mut peer = create_test_peer(1000.0, 100.0, true, true);
529 peer.record_optimistic_unchoke(); algo.add_peer(peer);
531
532 let result = algo.optimistically_unchoke();
533
534 assert!(result.is_none());
536 }
537
538 #[test]
539 fn test_snubbed_peers_get_lowered_score() {
540 let normal_peer = create_test_peer(50000.0, 500.0, true, true);
542 let mut snubbed_peer = create_test_peer(50000.0, 500.0, true, true);
543 snubbed_peer.is_snubbed = true;
544
545 let normal_score = ChokingAlgorithm::calculate_peer_score(&normal_peer, false);
546 let snubbed_score_stats = ChokingAlgorithm::calculate_peer_score(&snubbed_peer, false);
547 let snubbed_score_explicit = ChokingAlgorithm::calculate_peer_score(&normal_peer, true);
548
549 assert!(snubbed_score_stats < normal_score);
551 assert!(
552 (normal_score - snubbed_score_stats) > 900.0,
553 "Expected large score difference due to PeerStats snubbed penalty"
554 );
555
556 assert!(snubbed_score_explicit < normal_score);
558 assert!(
559 (normal_score - snubbed_score_explicit) > 900.0,
560 "Expected large score difference due to explicit snubbed penalty"
561 );
562 }
563
564 #[test]
565 fn test_check_snubbed_returns_timed_out_peers() {
566 let config = ChokingConfig {
567 snubbed_timeout_secs: 1, ..Default::default()
569 };
570 let mut algo = ChokingAlgorithm::new(config);
571
572 let _addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
574 let peer = PeerStats::new([0u8; 20], "127.0.0.1:6882".parse().unwrap());
575 algo.add_peer(peer);
576
577 std::thread::sleep(std::time::Duration::from_millis(1100));
579
580 let snubbed_indices = algo.check_snubbed_peers();
582
583 assert_eq!(snubbed_indices.len(), 1);
585 assert_eq!(snubbed_indices[0], 0);
586 assert!(algo.get_peer(0).unwrap().is_snubbed);
587 }
588
589 #[test]
590 fn test_on_data_received_resets_snubbed_status() {
591 let config = ChokingConfig {
592 snubbed_timeout_secs: 1,
593 ..Default::default()
594 };
595 let mut algo = ChokingAlgorithm::new(config);
596
597 let peer = PeerStats::new([0u8; 20], "127.0.0.1:6883".parse().unwrap());
598 algo.add_peer(peer);
599
600 std::thread::sleep(std::time::Duration::from_millis(1100));
602
603 let snubbed = algo.check_snubbed_peers();
605 assert_eq!(snubbed.len(), 1);
606 assert!(algo.get_peer(0).unwrap().is_snubbed);
607
608 algo.on_data_received(0, 1024);
610
611 assert!(!algo.get_peer(0).unwrap().is_snubbed);
613 }
614
615 #[test]
616 fn test_get_peer_accessors() {
617 let config = ChokingConfig::default();
618 let mut algo = ChokingAlgorithm::new(config);
619
620 let addr: SocketAddr = "127.0.0.1:6881".parse().unwrap();
621 let peer = PeerStats::new([0u8; 20], addr);
622 algo.add_peer(peer);
623
624 assert!(algo.get_peer(0).is_some());
626 assert!(algo.get_peer(1).is_none());
627
628 {
630 let p = algo.get_peer_mut(0).unwrap();
631 p.download_speed = 9999.0;
632 }
633
634 assert!((algo.get_peer(0).unwrap().download_speed - 9999.0).abs() < f64::EPSILON);
635 }
636
637 #[test]
638 fn test_config_defaults() {
639 let config = ChokingConfig::default();
640
641 assert_eq!(config.max_upload_slots, 4);
642 assert_eq!(config.optimistic_unchoke_interval_secs, 30);
643 assert_eq!(config.snubbed_timeout_secs, 60);
644 assert_eq!(config.choke_rotation_interval_secs, 10);
645 }
646
647 #[test]
650 fn test_snub_detection_after_timeout() {
651 let config = ChokingConfig {
653 snubbed_timeout_secs: 1,
654 ..Default::default()
655 };
656 let mut algo = ChokingAlgorithm::new(config);
657
658 let peer = PeerStats::new([0u8; 20], "127.0.0.1:6882".parse().unwrap());
659 algo.add_peer(peer);
660
661 std::thread::sleep(std::time::Duration::from_millis(1100));
663
664 let snubbed_indices = algo.check_snubbed_peers();
666 assert_eq!(snubbed_indices.len(), 1);
667 assert!(algo.get_peer(0).unwrap().is_snubbed);
668 }
669
670 #[test]
671 fn test_snubbed_peer_always_choked() {
672 let config = ChokingConfig {
674 max_upload_slots: 2,
675 ..Default::default()
676 };
677 let mut algo = ChokingAlgorithm::new(config);
678
679 algo.add_peer(create_test_peer(100000.0, 1000.0, true, true)); algo.add_peer(create_test_peer(50000.0, 500.0, true, true)); algo.add_peer(create_test_peer(30000.0, 300.0, true, true)); algo.mark_peer_snubbed(0);
686 assert!(algo.is_explicitly_snubbed(0));
687 assert_eq!(algo.snubbed_count(), 1);
688
689 let actions = algo.rotate_choke();
691
692 let peer0_action = actions
694 .iter()
695 .find(|a| matches!(a, ChokeAction::NoChange(0) | ChokeAction::Choke(0)));
696 assert!(
697 peer0_action.is_some(),
698 "Peer 0 should have an action in results"
699 );
700 match peer0_action.unwrap() {
702 ChokeAction::Choke(_) | ChokeAction::NoChange(_) => {} ChokeAction::Unchoke(_) => panic!("Snubbed peer 0 should NEVER be unchoked"),
704 }
705 }
706
707 #[test]
708 fn test_unsnub_on_data_received() {
709 let config = ChokingConfig::default();
711 let mut algo = ChokingAlgorithm::new(config);
712
713 let peer = PeerStats::new([0u8; 20], "127.0.0.1:6883".parse().unwrap());
714 algo.add_peer(peer);
715
716 algo.mark_peer_snubbed(0);
718 assert!(algo.is_explicitly_snubbed(0));
719 assert_eq!(algo.snubbed_count(), 1);
720
721 algo.on_data_received(0, 1024);
723 assert!(
724 !algo.is_explicitly_snubbed(0),
725 "Peer should be un-snubbed after data received"
726 );
727 assert_eq!(algo.snubbed_count(), 0);
728 }
729
730 #[test]
731 fn test_opt_unchoking_rotation_changes_peer() {
732 let config = ChokingConfig {
734 optimistic_unchoke_interval_secs: 0, ..Default::default()
736 };
737 let mut algo = ChokingAlgorithm::new(config);
738
739 algo.add_peer(create_test_peer(1000.0, 100.0, true, true));
741 algo.add_peer(create_test_peer(2000.0, 200.0, true, true));
742 algo.add_peer(create_test_peer(3000.0, 300.0, true, true));
743
744 let first = algo.optimistically_unchoke();
746 assert!(first.is_some());
747 let first_idx = first.unwrap();
748
749 for i in 0..3 {
752 if let Some(p) = algo.get_peer_mut(i) {
753 p.last_optimistic_unchoke_at =
754 std::time::Instant::now() - std::time::Duration::from_secs(1);
755 }
756 }
757
758 let second = algo.optimistically_unchoke();
759 assert!(second.is_some());
760 let second_idx = second.unwrap();
761
762 assert_ne!(
765 first_idx, second_idx,
766 "Optimistic unchoke should rotate to a different peer"
767 );
768 }
769
770 #[test]
771 fn test_opt_unchoking_excludes_snubbed_peers() {
772 let config = ChokingConfig {
774 optimistic_unchoke_interval_secs: 0,
775 ..Default::default()
776 };
777 let mut algo = ChokingAlgorithm::new(config);
778
779 algo.add_peer(create_test_peer(5000.0, 500.0, true, true));
781 algo.add_peer(create_test_peer(3000.0, 300.0, true, true));
783
784 algo.mark_peer_snubbed(0);
786
787 let result = algo.optimistically_unchoke();
789 assert!(result.is_some());
790 assert_eq!(
791 result.unwrap(),
792 1,
793 "Should select non-snubbed peer for optimistic unchoke"
794 );
795 }
796
797 #[test]
798 fn test_mark_snubbed_idempotent() {
799 let config = ChokingConfig::default();
800 let mut algo = ChokingAlgorithm::new(config);
801
802 algo.add_peer(create_test_peer(100.0, 10.0, true, true));
803
804 algo.mark_peer_snubbed(0);
806 assert_eq!(algo.snubbed_count(), 1);
807 algo.mark_peer_snubbed(0); assert_eq!(
809 algo.snubbed_count(),
810 1,
811 "Duplicate mark should not increase count"
812 );
813 }
814
815 #[test]
816 fn test_unsnub_non_snubbed_peer_returns_false() {
817 let config = ChokingConfig::default();
818 let mut algo = ChokingAlgorithm::new(config);
819
820 algo.add_peer(create_test_peer(100.0, 10.0, true, true));
821
822 let result = algo.unsnub_peer(0);
824 assert!(!result, "Unsnubbing non-snubbed peer should return false");
825 }
826}