use crate::bitfield::Bitfield;
use crate::consts::REQUEST_PIPELINE;
use crate::monitoring::DiscoverySource;
use crate::platform::{ConnId, NetAddr};
use crate::wire::{ExtHandshake, Message, MessageStream};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerPhase {
Connecting,
Handshake,
Ready,
Closed,
}
#[derive(Debug)]
pub struct Peer {
pub id: ConnId,
pub addr: NetAddr,
pub phase: PeerPhase,
pub peer_id: Option<[u8; 20]>,
pub reserved: [u8; 8],
pub am_choking: bool,
pub am_interested: bool,
pub peer_choking: bool,
pub peer_interested: bool,
pub have: Bitfield,
pub have_all: bool,
pub have_none: bool,
pub ext: Option<ExtHandshake>,
pub ext_metadata: Option<u8>,
pub ext_pex: Option<u8>,
pub is_seed: bool,
pub fast: bool,
pub supports_dht: bool,
pub supports_v2: bool,
pub down_total: u64,
pub up_total: u64,
pub down_rate: u32,
pub up_rate: u32,
pub last_active: u64,
pub last_data_in: u64,
pub connected_at: u64,
pub snubbed: bool,
pub optimistic: bool,
pub requests_in_flight: u32,
pub out: Vec<u8>,
pub msgs: MessageStream,
pub handshake_buf: Vec<u8>,
pub source: DiscoverySource,
pub pex_sent_at: u64,
pub window_down: u64,
pub window_up: u64,
pub window_started: u64,
}
impl Peer {
pub fn new(id: ConnId, addr: NetAddr, piece_count: u32, source: DiscoverySource) -> Self {
Peer {
id,
addr,
phase: PeerPhase::Connecting,
peer_id: None,
reserved: [0u8; 8],
am_choking: true,
am_interested: false,
peer_choking: true,
peer_interested: false,
have: Bitfield::new(piece_count),
have_all: false,
have_none: false,
ext: None,
ext_metadata: None,
ext_pex: None,
is_seed: false,
fast: false,
supports_dht: false,
supports_v2: false,
down_total: 0,
up_total: 0,
down_rate: 0,
up_rate: 0,
last_active: 0,
last_data_in: 0,
connected_at: 0,
snubbed: false,
optimistic: false,
requests_in_flight: 0,
out: Vec::with_capacity(16 * 1024),
msgs: MessageStream::new(),
handshake_buf: Vec::new(),
source,
pex_sent_at: 0,
window_down: 0,
window_up: 0,
window_started: 0,
}
}
pub fn has_piece(&self, p: u32) -> bool {
self.have_all || self.have.get(p)
}
pub fn should_be_interested(&self, our_have: &Bitfield) -> bool {
if self.peer_choking || self.have_none {
return false;
}
if self.have_all {
return !our_have.all_set();
}
let n = self.have.len().min(our_have.len());
let mut i = self.have.first_set();
while let Some(p) = i {
if p >= n {
break;
}
if !our_have.get(p) {
return true;
}
i = self.have.next_set_from(p + 1);
}
false
}
pub fn max_pipeline(&self) -> u32 {
REQUEST_PIPELINE
}
pub fn send(&mut self, m: &Message) {
self.out.extend_from_slice(&m.encode());
}
pub fn flushed(&self) -> bool {
self.out.is_empty()
}
pub fn roll_window(&mut self, now: u64) {
let span_ms = now.saturating_sub(self.window_started).max(1);
self.down_rate = ((self.window_down * 1000) / span_ms) as u32;
self.up_rate = ((self.window_up * 1000) / span_ms) as u32;
self.window_down = 0;
self.window_up = 0;
self.window_started = now;
}
pub fn on_data_in(&mut self, n: usize, now: u64) {
self.down_total += n as u64;
self.window_down += n as u64;
self.last_active = now;
self.last_data_in = now;
self.snubbed = false;
}
pub fn on_data_out(&mut self, n: usize, now: u64) {
self.up_total += n as u64;
self.window_up += n as u64;
self.last_active = now;
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChokeConfig {
pub seeding_slots: u32,
pub leeching_slots: u32,
pub optimistic_interval_ms: u64,
pub snub_timeout_ms: u64,
pub interval_ms: u64,
}
impl Default for ChokeConfig {
fn default() -> Self {
ChokeConfig {
seeding_slots: 8,
leeching_slots: 8,
optimistic_interval_ms: 30_000,
snub_timeout_ms: 60_000,
interval_ms: 10_000,
}
}
}
struct Candidate {
id: ConnId,
rate: u32,
optimistic: bool,
snubbed: bool,
}
pub fn compute_unchoke_set<F>(
peers: &[&Peer],
seeding: bool,
cfg: &ChokeConfig,
is_optimistic: F,
) -> Vec<ConnId>
where
F: Fn(ConnId) -> bool,
{
let slots = if seeding {
cfg.seeding_slots
} else {
cfg.leeching_slots
} as usize;
let mut candidates: Vec<Candidate> = peers
.iter()
.filter(|p| p.phase == PeerPhase::Ready)
.filter(|p| p.peer_interested || !seeding)
.map(|p| Candidate {
id: p.id,
rate: if seeding { p.up_rate } else { p.down_rate },
optimistic: is_optimistic(p.id),
snubbed: p.snubbed,
})
.collect();
candidates.sort_by(|a, b| b.rate.cmp(&a.rate).then_with(|| a.snubbed.cmp(&b.snubbed)));
let mut unchoked: Vec<ConnId> = Vec::with_capacity(slots);
for c in &candidates {
if unchoked.len() >= slots {
break;
}
if c.snubbed {
continue;
}
unchoked.push(c.id);
}
if unchoked.len() < slots {
for c in &candidates {
if c.optimistic && !unchoked.contains(&c.id) {
unchoked.push(c.id);
break;
}
}
}
unchoked
}
pub fn update_snubs<'a>(
peers: impl IntoIterator<Item = &'a mut Peer>,
now: u64,
cfg: &ChokeConfig,
) {
for p in peers {
if p.phase == PeerPhase::Ready
&& !p.peer_choking
&& p.requests_in_flight > 0
&& now.saturating_sub(p.last_data_in) > cfg.snub_timeout_ms
{
p.snubbed = true;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn peer(id: ConnId, down: u32, interested: bool) -> Peer {
let mut p = Peer::new(
id,
NetAddr::V4([127, 0, 0, 1], 6881),
64,
DiscoverySource::Tracker,
);
p.phase = PeerPhase::Ready;
p.down_rate = down;
p.peer_interested = interested;
p
}
#[test]
fn unchoke_picks_fastest() {
let peers: Vec<Peer> = vec![
peer(1, 100, true),
peer(2, 9000, true),
peer(3, 5000, false),
];
let refs: Vec<&Peer> = peers.iter().collect();
let cfg = ChokeConfig {
leeching_slots: 2,
..Default::default()
};
let set = compute_unchoke_set(&refs, false, &cfg, |_| false);
assert_eq!(set.len(), 2);
assert!(set.contains(&2));
assert!(set.contains(&3) || set.contains(&1));
}
#[test]
fn snub_detection() {
let cfg = ChokeConfig::default();
let mut p = peer(1, 100, true);
p.phase = PeerPhase::Ready;
p.peer_choking = false;
p.requests_in_flight = 2;
p.last_data_in = 0;
update_snubs(core::slice::from_mut(&mut p), cfg.snub_timeout_ms + 1, &cfg);
assert!(p.snubbed);
}
#[test]
fn interested_detection() {
let mut p = peer(1, 0, false);
p.peer_choking = false;
p.have.set(5);
let mut ours = Bitfield::new(64);
ours.set(5);
assert!(!p.should_be_interested(&ours));
ours.clear(5);
assert!(p.should_be_interested(&ours));
}
}