diff --git a/packages/primitives/src/peer.rs b/packages/primitives/src/peer.rs index f5b009f2a..ab7559508 100644 --- a/packages/primitives/src/peer.rs +++ b/packages/primitives/src/peer.rs @@ -362,6 +362,38 @@ pub mod fixture { } impl PeerBuilder { + #[allow(dead_code)] + #[must_use] + pub fn seeder() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000001"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Completed, + }; + + Self { peer } + } + + #[allow(dead_code)] + #[must_use] + pub fn leecher() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000002"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(10), + event: AnnounceEvent::Started, + }; + + Self { peer } + } + #[allow(dead_code)] #[must_use] pub fn with_peer_id(mut self, peer_id: &Id) -> Self { @@ -390,6 +422,13 @@ pub mod fixture { self } + #[allow(dead_code)] + #[must_use] + pub fn last_updated_on(mut self, updated: DurationSinceUnixEpoch) -> Self { + self.peer.updated = updated; + self + } + #[allow(dead_code)] #[must_use] pub fn build(self) -> Peer { diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index d72ff254b..40fa4efd5 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -2,13 +2,15 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; -//use serde::{Deserialize, Serialize}; use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; +use self::peer_list::PeerList; + pub mod mutex_std; pub mod mutex_tokio; +pub mod peer_list; pub mod single; pub trait Entry { @@ -81,9 +83,8 @@ pub trait EntryAsync { /// The tracker keeps one entry like this for every torrent. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Torrent { - /// The swarm: a network of peers that are all trying to download the torrent associated to this entry - // #[serde(skip)] - pub(crate) peers: std::collections::BTreeMap>, + /// A network of peers that are all trying to download the torrent associated to this entry + pub(crate) swarm: PeerList, /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } diff --git a/packages/torrent-repository/src/entry/peer_list.rs b/packages/torrent-repository/src/entry/peer_list.rs new file mode 100644 index 000000000..3f69edbb5 --- /dev/null +++ b/packages/torrent-repository/src/entry/peer_list.rs @@ -0,0 +1,289 @@ +//! A peer list. +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +// code-review: the current implementation uses the peer Id as the ``BTreeMap`` +// key. That would allow adding two identical peers except for the Id. +// For example, two peers with the same socket address but a different peer Id +// would be allowed. That would lead to duplicated peers in the tracker responses. + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PeerList { + peers: std::collections::BTreeMap>, +} + +impl PeerList { + #[must_use] + pub fn len(&self) -> usize { + self.peers.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + pub fn upsert(&mut self, value: Arc) -> Option> { + self.peers.insert(value.peer_id, value) + } + + pub fn remove(&mut self, key: &peer::Id) -> Option> { + self.peers.remove(key) + } + + pub fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { + self.peers + .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); + } + + #[must_use] + pub fn get(&self, peer_id: &peer::Id) -> Option<&Arc> { + self.peers.get(peer_id) + } + + #[must_use] + pub fn get_all(&self, limit: Option) -> Vec> { + match limit { + Some(limit) => self.peers.values().take(limit).cloned().collect(), + None => self.peers.values().cloned().collect(), + } + } + + #[must_use] + pub fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + #[must_use] + pub fn get_peers_excluding_addr(&self, peer_addr: &SocketAddr, limit: Option) -> Vec> { + match limit { + Some(limit) => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) + // Limit the number of peers on the result + .take(limit) + .cloned() + .collect(), + None => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) + .cloned() + .collect(), + } + } +} + +#[cfg(test)] +mod tests { + + mod it_should { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::Arc; + + use torrust_tracker_primitives::peer::fixture::PeerBuilder; + use torrust_tracker_primitives::peer::{self}; + use torrust_tracker_primitives::DurationSinceUnixEpoch; + + use crate::entry::peer_list::PeerList; + + #[test] + fn be_empty_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert!(peer_list.is_empty()); + } + + #[test] + fn have_zero_length_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn allow_inserting_a_new_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + assert_eq!(peer_list.upsert(peer.into()), None); + } + + #[test] + fn allow_updating_a_preexisting_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.upsert(peer.into()), Some(Arc::new(peer))); + } + + #[test] + fn allow_getting_all_peers() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get_all(None), [Arc::new(peer)]); + } + + #[test] + fn allow_getting_one_peer_by_id() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get(&peer.peer_id), Some(Arc::new(peer)).as_ref()); + } + + #[test] + fn increase_the_number_of_peers_after_inserting_a_new_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn decrease_the_number_of_peers_after_removing_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert!(peer_list.is_empty()); + } + + #[test] + fn allow_removing_an_existing_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert_eq!(peer_list.get(&peer.peer_id), None); + } + + #[test] + fn allow_getting_all_peers_excluding_peers_with_a_given_address() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969)) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 6969)) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.get_peers_excluding_addr(&peer2.peer_addr, None), [Arc::new(peer1)]); + } + + #[test] + fn return_the_number_of_seeders_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (seeders, _leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(seeders, 1); + } + + #[test] + fn return_the_number_of_leechers_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (_seeders, leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(leechers, 1); + } + + #[test] + fn remove_inactive_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second after inserting the peer + peer_list.remove_inactive_peers(last_update_time + one_second); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn not_remove_active_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second before inserting the peer. + peer_list.remove_inactive_peers(last_update_time - one_second); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn allow_inserting_two_identical_peers_except_for_the_id() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.len(), 2); + } + } +} diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index a38b54023..a01124454 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -13,13 +13,12 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] fn get_swarm_metadata(&self) -> SwarmMetadata { - let complete: u32 = self.peers.values().filter(|peer| peer.is_seeder()).count() as u32; - let incomplete: u32 = self.peers.len() as u32 - complete; + let (seeders, leechers) = self.swarm.seeders_and_leechers(); SwarmMetadata { downloaded: self.downloaded, - complete, - incomplete, + complete: seeders as u32, + incomplete: leechers as u32, } } @@ -28,7 +27,7 @@ impl Entry for EntrySingle { return true; } - if policy.remove_peerless_torrents && self.peers.is_empty() { + if policy.remove_peerless_torrents && self.swarm.is_empty() { return false; } @@ -36,38 +35,19 @@ impl Entry for EntrySingle { } fn peers_is_empty(&self) -> bool { - self.peers.is_empty() + self.swarm.is_empty() } fn get_peers_len(&self) -> usize { - self.peers.len() + self.swarm.len() } + fn get_peers(&self, limit: Option) -> Vec> { - match limit { - Some(limit) => self.peers.values().take(limit).cloned().collect(), - None => self.peers.values().cloned().collect(), - } + self.swarm.get_all(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - match limit { - Some(limit) => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - // Limit the number of peers on the result - .take(limit) - .cloned() - .collect(), - None => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - .cloned() - .collect(), - } + self.swarm.get_peers_excluding_addr(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { @@ -75,10 +55,10 @@ impl Entry for EntrySingle { match peer::ReadInfo::get_event(peer) { AnnounceEvent::Stopped => { - drop(self.peers.remove(&peer::ReadInfo::get_id(peer))); + drop(self.swarm.remove(&peer::ReadInfo::get_id(peer))); } AnnounceEvent::Completed => { - let previous = self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); + let previous = self.swarm.upsert(Arc::new(*peer)); // Don't count if peer was not previously known and not already completed. if previous.is_some_and(|p| p.event != AnnounceEvent::Completed) { self.downloaded += 1; @@ -86,7 +66,7 @@ impl Entry for EntrySingle { } } _ => { - drop(self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); + drop(self.swarm.upsert(Arc::new(*peer))); } } @@ -94,7 +74,6 @@ impl Entry for EntrySingle { } fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { - self.peers - .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); + self.swarm.remove_inactive_peers(current_cutoff); } } diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index b398b09dc..a38205205 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use dashmap::DashMap; @@ -10,6 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; @@ -82,7 +82,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index af48428e4..0d96a2375 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,6 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; +use crate::entry::peer_list::PeerList; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockStd}; @@ -102,7 +101,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *downloaded, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 74cdc4475..76d5e8f1e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,6 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockStdMutexStd}; @@ -97,7 +97,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 83ac02c91..e527d6b59 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::iter::zip; use std::pin::Pin; use std::sync::Arc; @@ -13,6 +12,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockStdMutexTokio}; @@ -106,7 +106,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index b95f1e31e..c360106b8 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,6 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; +use crate::entry::peer_list::PeerList; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockTokio}; @@ -106,7 +105,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index bde959940..9fce79b44 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,6 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockTokioMutexStd}; @@ -97,7 +97,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index 1d002e317..c7e0d4054 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,6 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockTokioMutexTokio}; @@ -100,7 +100,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index ef3e7e478..bc9ecd066 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use crossbeam_skiplist::SkipMap; @@ -10,6 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; +use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; @@ -76,7 +76,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(),