From 53497308f1932783647ee1b45e10a7be9dc21fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 10:01:45 +0100 Subject: [PATCH 01/26] udp: create file with thread-shared torrent map implementation --- Cargo.lock | 33 ++ crates/udp/Cargo.toml | 1 + crates/udp/src/lib.rs | 1 + crates/udp/src/swarm.rs | 700 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 735 insertions(+) create mode 100644 crates/udp/src/swarm.rs diff --git a/Cargo.lock b/Cargo.lock index bc446f5e..b82edb27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,6 +315,7 @@ dependencies = [ "mimalloc", "mio", "num-format", + "parking_lot", "quickcheck", "quickcheck_macros", "rand", @@ -2001,6 +2002,29 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2269,6 +2293,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "ref-cast" version = "1.0.22" diff --git a/crates/udp/Cargo.toml b/crates/udp/Cargo.toml index 72c1f923..47cfa06e 100644 --- a/crates/udp/Cargo.toml +++ b/crates/udp/Cargo.toml @@ -48,6 +48,7 @@ log = "0.4" mimalloc = { version = "0.1", default-features = false } mio = { version = "0.8", features = ["net", "os-poll"] } num-format = "0.4" +parking_lot = "0.12" rand = { version = "0.8", features = ["small_rng"] } serde = { version = "1", features = ["derive"] } signal-hook = { version = "0.3" } diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index cf6a588c..49c8aa1c 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -1,5 +1,6 @@ pub mod common; pub mod config; +pub mod swarm; pub mod workers; use std::collections::BTreeMap; diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs new file mode 100644 index 00000000..1c671e1b --- /dev/null +++ b/crates/udp/src/swarm.rs @@ -0,0 +1,700 @@ +use std::iter::repeat_with; +use std::net::IpAddr; +use std::ops::DerefMut; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use aquatic_common::SecondsSinceServerStart; +use aquatic_common::{ + access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, + ValidUntil, +}; +use aquatic_common::{CanonicalSocketAddr, IndexMap}; + +use aquatic_udp_protocol::*; +use arrayvec::ArrayVec; +use crossbeam_channel::Sender; +use hdrhistogram::Histogram; +use rand::prelude::SmallRng; +use rand::Rng; + +use crate::common::*; +use crate::config::Config; + +const SMALL_PEER_MAP_CAPACITY: usize = 2; + +use aquatic_udp_protocol::InfoHash; +use parking_lot::RwLock; + +type TorrentMapShard = IndexMap>>; + +#[derive(Clone)] +pub struct TorrentMaps { + ipv4: TorrentMapShards, + ipv6: TorrentMapShards, +} + +impl TorrentMaps { + pub fn new(config: &Config) -> Self { + let num_shards = 128usize; + + Self { + ipv4: TorrentMapShards::new(num_shards), + ipv6: TorrentMapShards::new(num_shards), + } + } + + pub fn announce( + &self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + ip_address: CanonicalSocketAddr, + valid_until: ValidUntil, + ) -> Response { + match ip_address.get().ip() { + IpAddr::V4(ip_address) => Response::AnnounceIpv4(self.ipv4.announce( + config, + statistics_sender, + rng, + request, + ip_address.into(), + valid_until, + )), + IpAddr::V6(ip_address) => Response::AnnounceIpv6(self.ipv6.announce( + config, + statistics_sender, + rng, + request, + ip_address.into(), + valid_until, + )), + } + } + + pub fn scrape(&self, ip_addr: CanonicalSocketAddr, request: ScrapeRequest) -> ScrapeResponse { + if ip_addr.is_ipv4() { + self.ipv4.scrape(request) + } else { + self.ipv6.scrape(request) + } + } + /// Remove forbidden or inactive torrents, reclaim space and update statistics + pub fn clean_and_update_statistics( + &mut self, + config: &Config, + state: &State, + statistics: &CachePaddedArc>, + statistics_sender: &Sender, + access_list: &Arc, + ) { + let mut cache = create_access_list_cache(access_list); + let mode = config.access_list.mode; + let now = state.server_start_instant.seconds_elapsed(); + + let ipv4 = + self.ipv4 + .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); + let ipv6 = + self.ipv6 + .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); + + if config.statistics.active() { + statistics.ipv4.peers.store(ipv4.0, Ordering::Relaxed); + statistics.ipv6.peers.store(ipv6.0, Ordering::Relaxed); + + if let Some(message) = ipv4.1.map(StatisticsMessage::Ipv4PeerHistogram) { + if let Err(err) = statistics_sender.try_send(message) { + ::log::error!("couldn't send statistics message: {:#}", err); + } + } + if let Some(message) = ipv6.1.map(StatisticsMessage::Ipv6PeerHistogram) { + if let Err(err) = statistics_sender.try_send(message) { + ::log::error!("couldn't send statistics message: {:#}", err); + } + } + } + } +} + +#[derive(Clone)] +pub struct TorrentMapShards(Arc<[RwLock>]>); + +impl TorrentMapShards { + fn new(num_shards: usize) -> Self { + Self( + repeat_with(Default::default) + .take(num_shards) + .collect::>() + .into_boxed_slice() + .into(), + ) + } + + fn announce( + &self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + ip_address: I, + valid_until: ValidUntil, + ) -> AnnounceResponse { + let torrent_map_shard = self.get_shard(&request.info_hash); + + // Clone Arc here to avoid keeping lock on whole shard + let torrent_data = + if let Some(torrent_data) = torrent_map_shard.read().get(&request.info_hash) { + torrent_data.clone() + } else { + // Don't overwrite entry if created in the meantime + torrent_map_shard + .write() + .entry(request.info_hash) + .or_default() + .clone() + }; + + let mut peer_map = torrent_data.peer_map.write(); + + peer_map.announce( + config, + statistics_sender, + rng, + request, + ip_address, + valid_until, + ) + } + + fn scrape(&self, request: ScrapeRequest) -> ScrapeResponse { + let mut response = ScrapeResponse { + transaction_id: request.transaction_id, + torrent_stats: Vec::with_capacity(request.info_hashes.len()), + }; + + for info_hash in request.info_hashes { + let torrent_map_shard = self.get_shard(&info_hash); + + let statistics = if let Some(torrent_data) = torrent_map_shard.read().get(&info_hash) { + torrent_data.peer_map.read().scrape_statistics() + } else { + TorrentScrapeStatistics { + seeders: NumberOfPeers::new(0), + leechers: NumberOfPeers::new(0), + completed: NumberOfDownloads::new(0), + } + }; + + response.torrent_stats.push(statistics); + } + + response + } + + fn clean_and_get_statistics( + &mut self, + config: &Config, + statistics_sender: &Sender, + access_list_cache: &mut AccessListCache, + access_list_mode: AccessListMode, + now: SecondsSinceServerStart, + ) -> (usize, Option>) { + let mut total_num_peers = 0; + + let mut opt_histogram: Option> = if config.statistics.torrent_peer_histograms + { + match Histogram::new(3) { + Ok(histogram) => Some(histogram), + Err(err) => { + ::log::error!("Couldn't create peer histogram: {:#}", err); + + None + } + } + } else { + None + }; + + for torrent_map_shard in self.0.iter() { + for torrent_data in torrent_map_shard.read().values() { + let mut peer_map = torrent_data.peer_map.write(); + + let num_peers = match peer_map.deref_mut() { + PeerMap::Small(small_peer_map) => { + small_peer_map.clean_and_get_num_peers(config, statistics_sender, now) + } + PeerMap::Large(large_peer_map) => { + let num_peers = + large_peer_map.clean_and_get_num_peers(config, statistics_sender, now); + + if let Some(small_peer_map) = large_peer_map.try_shrink() { + *peer_map = PeerMap::Small(small_peer_map); + } + + num_peers + } + }; + + drop(peer_map); + + match opt_histogram { + Some(ref mut histogram) if num_peers > 0 => { + let n = num_peers.try_into().expect("Couldn't fit usize into u64"); + + if let Err(err) = histogram.record(n) { + ::log::error!("Couldn't record {} to histogram: {:#}", n, err); + } + } + _ => (), + } + + torrent_data + .pending_removal + .store(num_peers == 0, Ordering::Release); + + total_num_peers += num_peers; + } + + let mut torrent_map_shard = torrent_map_shard.write(); + + torrent_map_shard.retain(|info_hash, torrent_data| { + if !access_list_cache + .load() + .allows(access_list_mode, &info_hash.0) + { + return false; + } + + // Only remove if no peers have been added since previous + // cleaning step + if torrent_data + .pending_removal + .fetch_and(false, Ordering::Acquire) + && torrent_data.peer_map.read().is_empty() + { + return false; + } + + true + }); + + torrent_map_shard.shrink_to_fit(); + } + + (total_num_peers, opt_histogram) + } + + fn get_shard(&self, info_hash: &InfoHash) -> &RwLock> { + self.0.get(info_hash.0[0] as usize % self.0.len()).unwrap() + } +} + +pub struct TorrentData { + peer_map: RwLock>, + pending_removal: AtomicBool, +} + +impl Default for TorrentData { + fn default() -> Self { + Self { + peer_map: Default::default(), + pending_removal: Default::default(), + } + } +} + +pub enum PeerMap { + Small(SmallPeerMap), + Large(LargePeerMap), +} + +impl PeerMap { + fn announce( + &mut self, + config: &Config, + statistics_sender: &Sender, + rng: &mut SmallRng, + request: &AnnounceRequest, + ip_address: I, + valid_until: ValidUntil, + ) -> AnnounceResponse { + let max_num_peers_to_take: usize = if request.peers_wanted.0.get() <= 0 { + config.protocol.max_response_peers + } else { + ::std::cmp::min( + config.protocol.max_response_peers, + request.peers_wanted.0.get().try_into().unwrap(), + ) + }; + + let status = + PeerStatus::from_event_and_bytes_left(request.event.into(), request.bytes_left); + + let peer_map_key = ResponsePeer { + ip_address, + port: request.port, + }; + + // Create the response before inserting the peer. This means that we + // don't have to filter it out from the response peers, and that the + // reported number of seeders/leechers will not include it + let (response, opt_removed_peer) = match self { + Self::Small(peer_map) => { + let opt_removed_peer = peer_map.remove(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + + let response = AnnounceResponse { + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval::new( + config.protocol.peer_announce_interval, + ), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + }, + peers: peer_map.extract_response_peers(max_num_peers_to_take), + }; + + // Convert peer map to large variant if it is full and + // announcing peer is not stopped and will therefore be + // inserted + if peer_map.is_full() && status != PeerStatus::Stopped { + *self = Self::Large(peer_map.to_large()); + } + + (response, opt_removed_peer) + } + Self::Large(peer_map) => { + let opt_removed_peer = peer_map.remove_peer(&peer_map_key); + + let (seeders, leechers) = peer_map.num_seeders_leechers(); + + let response = AnnounceResponse { + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval::new( + config.protocol.peer_announce_interval, + ), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + }, + peers: peer_map.extract_response_peers(rng, max_num_peers_to_take), + }; + + // Try shrinking the map if announcing peer is stopped and + // will therefore not be inserted + if status == PeerStatus::Stopped { + if let Some(peer_map) = peer_map.try_shrink() { + *self = Self::Small(peer_map); + } + } + + (response, opt_removed_peer) + } + }; + + match status { + PeerStatus::Leeching | PeerStatus::Seeding => { + let peer = Peer { + peer_id: request.peer_id, + is_seeder: status == PeerStatus::Seeding, + valid_until, + }; + + match self { + Self::Small(peer_map) => peer_map.insert(peer_map_key, peer), + Self::Large(peer_map) => peer_map.insert(peer_map_key, peer), + } + + if config.statistics.peer_clients && opt_removed_peer.is_none() { + statistics_sender + .try_send(StatisticsMessage::PeerAdded(request.peer_id)) + .expect("statistics channel should be unbounded"); + } + } + PeerStatus::Stopped => { + if config.statistics.peer_clients && opt_removed_peer.is_some() { + statistics_sender + .try_send(StatisticsMessage::PeerRemoved(request.peer_id)) + .expect("statistics channel should be unbounded"); + } + } + }; + + response + } + + fn scrape_statistics(&self) -> TorrentScrapeStatistics { + let (seeders, leechers) = match self { + Self::Small(peer_map) => peer_map.num_seeders_leechers(), + Self::Large(peer_map) => peer_map.num_seeders_leechers(), + }; + + TorrentScrapeStatistics { + seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), + leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), + completed: NumberOfDownloads::new(0), + } + } + + fn is_empty(&self) -> bool { + match self { + Self::Small(peer_map) => peer_map.0.is_empty(), + Self::Large(peer_map) => peer_map.peers.is_empty(), + } + } +} + +impl Default for PeerMap { + fn default() -> Self { + Self::Small(SmallPeerMap(ArrayVec::default())) + } +} + +/// Store torrents with up to two peers without an extra heap allocation +/// +/// On public open trackers, this is likely to be the majority of torrents. +#[derive(Default, Debug)] +pub struct SmallPeerMap(ArrayVec<(ResponsePeer, Peer), SMALL_PEER_MAP_CAPACITY>); + +impl SmallPeerMap { + fn is_full(&self) -> bool { + self.0.is_full() + } + + fn num_seeders_leechers(&self) -> (usize, usize) { + let seeders = self.0.iter().filter(|(_, p)| p.is_seeder).count(); + let leechers = self.0.len() - seeders; + + (seeders, leechers) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + self.0.push((key, peer)); + } + + fn remove(&mut self, key: &ResponsePeer) -> Option { + for (i, (k, _)) in self.0.iter().enumerate() { + if k == key { + return Some(self.0.remove(i).1); + } + } + + None + } + + fn extract_response_peers(&self, max_num_peers_to_take: usize) -> Vec> { + Vec::from_iter(self.0.iter().take(max_num_peers_to_take).map(|(k, _)| *k)) + } + + fn clean_and_get_num_peers( + &mut self, + config: &Config, + statistics_sender: &Sender, + now: SecondsSinceServerStart, + ) -> usize { + self.0.retain(|(_, peer)| { + let keep = peer.valid_until.valid(now); + + if !keep + && config.statistics.peer_clients + && statistics_sender + .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) + .is_err() + { + // Should never happen in practice + ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); + } + + keep + }); + + self.0.len() + } + + fn to_large(&self) -> LargePeerMap { + let (num_seeders, _) = self.num_seeders_leechers(); + let peers = self.0.iter().copied().collect(); + + LargePeerMap { peers, num_seeders } + } +} + +#[derive(Default)] +pub struct LargePeerMap { + peers: IndexMap, Peer>, + num_seeders: usize, +} + +impl LargePeerMap { + fn num_seeders_leechers(&self) -> (usize, usize) { + (self.num_seeders, self.peers.len() - self.num_seeders) + } + + fn insert(&mut self, key: ResponsePeer, peer: Peer) { + if peer.is_seeder { + self.num_seeders += 1; + } + + self.peers.insert(key, peer); + } + + fn remove_peer(&mut self, key: &ResponsePeer) -> Option { + let opt_removed_peer = self.peers.swap_remove(key); + + if let Some(Peer { + is_seeder: true, .. + }) = opt_removed_peer + { + self.num_seeders -= 1; + } + + opt_removed_peer + } + + /// Extract response peers + /// + /// If there are more peers in map than `max_num_peers_to_take`, do a random + /// selection of peers from first and second halves of map in order to avoid + /// returning too homogeneous peers. + /// + /// Does NOT filter out announcing peer. + fn extract_response_peers( + &self, + rng: &mut impl Rng, + max_num_peers_to_take: usize, + ) -> Vec> { + if self.peers.len() <= max_num_peers_to_take { + self.peers.keys().copied().collect() + } else { + let middle_index = self.peers.len() / 2; + let num_to_take_per_half = max_num_peers_to_take / 2; + + let offset_half_one = { + let from = 0; + let to = usize::max(1, middle_index - num_to_take_per_half); + + rng.gen_range(from..to) + }; + let offset_half_two = { + let from = middle_index; + let to = usize::max(middle_index + 1, self.peers.len() - num_to_take_per_half); + + rng.gen_range(from..to) + }; + + let end_half_one = offset_half_one + num_to_take_per_half; + let end_half_two = offset_half_two + num_to_take_per_half; + + let mut peers = Vec::with_capacity(max_num_peers_to_take); + + if let Some(slice) = self.peers.get_range(offset_half_one..end_half_one) { + peers.extend(slice.keys()); + } + if let Some(slice) = self.peers.get_range(offset_half_two..end_half_two) { + peers.extend(slice.keys()); + } + + peers + } + } + + fn clean_and_get_num_peers( + &mut self, + config: &Config, + statistics_sender: &Sender, + now: SecondsSinceServerStart, + ) -> usize { + self.peers.retain(|_, peer| { + let keep = peer.valid_until.valid(now); + + if !keep { + if peer.is_seeder { + self.num_seeders -= 1; + } + if config.statistics.peer_clients + && statistics_sender + .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) + .is_err() + { + // Should never happen in practice + ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); + } + } + + keep + }); + + if !self.peers.is_empty() { + self.peers.shrink_to_fit(); + } + + self.peers.len() + } + + fn try_shrink(&mut self) -> Option> { + (self.peers.len() <= SMALL_PEER_MAP_CAPACITY).then(|| { + SmallPeerMap(ArrayVec::from_iter( + self.peers.iter().map(|(k, v)| (*k, *v)), + )) + }) + } +} + +#[derive(Clone, Copy, Debug)] +struct Peer { + peer_id: PeerId, + is_seeder: bool, + valid_until: ValidUntil, +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum PeerStatus { + Seeding, + Leeching, + Stopped, +} + +impl PeerStatus { + /// Determine peer status from announce event and number of bytes left. + /// + /// Likely, the last branch will be taken most of the time. + #[inline] + pub fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: NumberOfBytes) -> Self { + if event == AnnounceEvent::Stopped { + Self::Stopped + } else if bytes_left.0.get() == 0 { + Self::Seeding + } else { + Self::Leeching + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_peer_status_from_event_and_bytes_left() { + use PeerStatus::*; + + let f = PeerStatus::from_event_and_bytes_left; + + assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(0))); + assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::Started, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::Started, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::Completed, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::Completed, NumberOfBytes::new(1))); + + assert_eq!(Seeding, f(AnnounceEvent::None, NumberOfBytes::new(0))); + assert_eq!(Leeching, f(AnnounceEvent::None, NumberOfBytes::new(1))); + } +} From 2da966098f0b0483020ca43eb6c0bd0e09e870ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 11:21:48 +0100 Subject: [PATCH 02/26] WIP: use shared swarm state in mio worker --- crates/udp/src/common.rs | 7 +- crates/udp/src/lib.rs | 67 +------- crates/udp/src/swarm.rs | 25 +-- crates/udp/src/workers/socket/mio.rs | 226 ++++++--------------------- crates/udp/src/workers/socket/mod.rs | 11 +- 5 files changed, 80 insertions(+), 256 deletions(-) diff --git a/crates/udp/src/common.rs b/crates/udp/src/common.rs index 4dd3496f..d26d519b 100644 --- a/crates/udp/src/common.rs +++ b/crates/udp/src/common.rs @@ -13,6 +13,7 @@ use crossbeam_utils::CachePadded; use hdrhistogram::Histogram; use crate::config::Config; +use crate::swarm::TorrentMaps; pub const BUFFER_SIZE: usize = 8192; @@ -230,13 +231,15 @@ pub enum StatisticsMessage { #[derive(Clone)] pub struct State { pub access_list: Arc, + pub torrent_maps: TorrentMaps, pub server_start_instant: ServerStartInstant, } -impl Default for State { - fn default() -> Self { +impl State { + pub fn new(config: &Config) -> Self { Self { access_list: Arc::new(AccessListArcSwap::default()), + torrent_maps: TorrentMaps::new(config), server_start_instant: ServerStartInstant::new(), } } diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index 49c8aa1c..f2a7ec87 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -23,6 +23,7 @@ use common::{ SwarmWorkerIndex, }; use config::Config; +use swarm::TorrentMaps; use workers::socket::ConnectionValidator; use workers::swarm::SwarmWorker; @@ -32,81 +33,24 @@ pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn run(config: Config) -> ::anyhow::Result<()> { let mut signals = Signals::new([SIGUSR1])?; - let state = State::default(); + let state = State::new(&config); let statistics = Statistics::new(&config); let connection_validator = ConnectionValidator::new(&config)?; let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); + let mut join_handles = Vec::new(); update_access_list(&config.access_list, &state.access_list)?; - let mut request_senders = Vec::new(); - let mut request_receivers = BTreeMap::new(); - - let mut response_senders = Vec::new(); - let mut response_receivers = BTreeMap::new(); - let (statistics_sender, statistics_receiver) = unbounded(); - for i in 0..config.swarm_workers { - let (request_sender, request_receiver) = bounded(config.worker_channel_size); - - request_senders.push(request_sender); - request_receivers.insert(i, request_receiver); - } - - for i in 0..config.socket_workers { - let (response_sender, response_receiver) = bounded(config.worker_channel_size); - - response_senders.push(response_sender); - response_receivers.insert(i, response_receiver); - } - - for i in 0..config.swarm_workers { - let config = config.clone(); - let state = state.clone(); - let request_receiver = request_receivers.remove(&i).unwrap().clone(); - let response_sender = ConnectedResponseSender::new(response_senders.clone()); - let statistics_sender = statistics_sender.clone(); - let statistics = statistics.swarm[i].clone(); - - let handle = Builder::new() - .name(format!("swarm-{:02}", i + 1)) - .spawn(move || { - #[cfg(feature = "cpu-pinning")] - pin_current_if_configured_to( - &config.cpu_pinning, - config.socket_workers, - config.swarm_workers, - WorkerIndex::SwarmWorker(i), - ); - - let mut worker = SwarmWorker { - config, - state, - statistics, - request_receiver, - response_sender, - statistics_sender, - worker_index: SwarmWorkerIndex(i), - }; - - worker.run() - }) - .with_context(|| "spawn swarm worker")?; - - join_handles.push((WorkerType::Swarm(i), handle)); - } - for i in 0..config.socket_workers { let state = state.clone(); let config = config.clone(); let connection_validator = connection_validator.clone(); - let request_sender = - ConnectedRequestSender::new(SocketWorkerIndex(i), request_senders.clone()); - let response_receiver = response_receivers.remove(&i).unwrap(); let priv_dropper = priv_dropper.clone(); let statistics = statistics.socket[i].clone(); + let statistics_sender = statistics_sender.clone(); let handle = Builder::new() .name(format!("socket-{:02}", i + 1)) @@ -123,9 +67,8 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { config, state, statistics, + statistics_sender, connection_validator, - request_sender, - response_receiver, priv_dropper, ) }) diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index 1c671e1b..159e145e 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -16,6 +16,7 @@ use aquatic_udp_protocol::*; use arrayvec::ArrayVec; use crossbeam_channel::Sender; use hdrhistogram::Histogram; +use parking_lot::RwLockUpgradableReadGuard; use rand::prelude::SmallRng; use rand::Rng; @@ -37,7 +38,7 @@ pub struct TorrentMaps { impl TorrentMaps { pub fn new(config: &Config) -> Self { - let num_shards = 128usize; + let num_shards = 16usize; Self { ipv4: TorrentMapShards::new(num_shards), @@ -51,10 +52,10 @@ impl TorrentMaps { statistics_sender: &Sender, rng: &mut SmallRng, request: &AnnounceRequest, - ip_address: CanonicalSocketAddr, + src: CanonicalSocketAddr, valid_until: ValidUntil, ) -> Response { - match ip_address.get().ip() { + match src.get().ip() { IpAddr::V4(ip_address) => Response::AnnounceIpv4(self.ipv4.announce( config, statistics_sender, @@ -74,8 +75,8 @@ impl TorrentMaps { } } - pub fn scrape(&self, ip_addr: CanonicalSocketAddr, request: ScrapeRequest) -> ScrapeResponse { - if ip_addr.is_ipv4() { + pub fn scrape(&self, request: ScrapeRequest, src: CanonicalSocketAddr) -> ScrapeResponse { + if src.is_ipv4() { self.ipv4.scrape(request) } else { self.ipv6.scrape(request) @@ -142,20 +143,20 @@ impl TorrentMapShards { ip_address: I, valid_until: ValidUntil, ) -> AnnounceResponse { - let torrent_map_shard = self.get_shard(&request.info_hash); + let torrent_data = { + let torrent_map_shard = self.get_shard(&request.info_hash).upgradable_read(); - // Clone Arc here to avoid keeping lock on whole shard - let torrent_data = - if let Some(torrent_data) = torrent_map_shard.read().get(&request.info_hash) { + // Clone Arc here to avoid keeping lock on whole shard + if let Some(torrent_data) = torrent_map_shard.get(&request.info_hash) { torrent_data.clone() } else { // Don't overwrite entry if created in the meantime - torrent_map_shard - .write() + RwLockUpgradableReadGuard::upgrade(torrent_map_shard) .entry(request.info_hash) .or_default() .clone() - }; + } + }; let mut peer_map = torrent_data.peer_map.write(); diff --git a/crates/udp/src/workers/socket/mio.rs b/crates/udp/src/workers/socket/mio.rs index 26ab5be8..9c720395 100644 --- a/crates/udp/src/workers/socket/mio.rs +++ b/crates/udp/src/workers/socket/mio.rs @@ -1,9 +1,10 @@ use std::io::{Cursor, ErrorKind}; use std::sync::atomic::Ordering; -use std::time::{Duration, Instant}; +use std::time::Duration; use anyhow::Context; use aquatic_common::access_list::AccessListCache; +use crossbeam_channel::Sender; use mio::net::UdpSocket; use mio::{Events, Interest, Poll, Token}; @@ -12,40 +13,26 @@ use aquatic_common::{ ValidUntil, }; use aquatic_udp_protocol::*; +use rand::rngs::SmallRng; +use rand::SeedableRng; use crate::common::*; use crate::config::Config; -use super::storage::PendingScrapeResponseSlab; use super::validator::ConnectionValidator; use super::{create_socket, EXTRA_PACKET_SIZE_IPV4, EXTRA_PACKET_SIZE_IPV6}; -enum HandleRequestError { - RequestChannelFull(Vec<(SwarmWorkerIndex, ConnectedRequest, CanonicalSocketAddr)>), -} - -#[derive(Clone, Copy, Debug)] -enum PollMode { - Regular, - SkipPolling, - SkipReceiving, -} - pub struct SocketWorker { config: Config, shared_state: State, statistics: CachePaddedArc>, - request_sender: ConnectedRequestSender, - response_receiver: ConnectedResponseReceiver, + statistics_sender: Sender, access_list_cache: AccessListCache, validator: ConnectionValidator, - pending_scrape_responses: PendingScrapeResponseSlab, socket: UdpSocket, opt_resend_buffer: Option>, buffer: [u8; BUFFER_SIZE], - polling_mode: PollMode, - /// Storage for requests that couldn't be sent to swarm worker because channel was full - pending_requests: Vec<(SwarmWorkerIndex, ConnectedRequest, CanonicalSocketAddr)>, + rng: SmallRng, } impl SocketWorker { @@ -53,9 +40,8 @@ impl SocketWorker { config: Config, shared_state: State, statistics: CachePaddedArc>, + statistics_sender: Sender, validator: ConnectionValidator, - request_sender: ConnectedRequestSender, - response_receiver: ConnectedResponseReceiver, priv_dropper: PrivilegeDropper, ) -> anyhow::Result<()> { let socket = UdpSocket::from_std(create_socket(&config, priv_dropper)?); @@ -66,16 +52,13 @@ impl SocketWorker { config, shared_state, statistics, + statistics_sender, validator, - request_sender, - response_receiver, access_list_cache, - pending_scrape_responses: Default::default(), socket, opt_resend_buffer, buffer: [0; BUFFER_SIZE], - polling_mode: PollMode::Regular, - pending_requests: Default::default(), + rng: SmallRng::from_entropy(), }; worker.run_inner() @@ -91,39 +74,12 @@ impl SocketWorker { let poll_timeout = Duration::from_millis(self.config.network.poll_timeout_ms); - let pending_scrape_cleaning_duration = - Duration::from_secs(self.config.cleaning.pending_scrape_cleaning_interval); - - let mut pending_scrape_valid_until = ValidUntil::new( - self.shared_state.server_start_instant, - self.config.cleaning.max_pending_scrape_age, - ); - let mut last_pending_scrape_cleaning = Instant::now(); - - let mut iter_counter = 0usize; - loop { - match self.polling_mode { - PollMode::Regular => { - poll.poll(&mut events, Some(poll_timeout)).context("poll")?; - - for event in events.iter() { - if event.is_readable() { - self.read_and_handle_requests(pending_scrape_valid_until); - } - } - } - PollMode::SkipPolling => { - self.polling_mode = PollMode::Regular; + poll.poll(&mut events, Some(poll_timeout)).context("poll")?; - // Continue reading from socket without polling, since - // reading was previouly cancelled - self.read_and_handle_requests(pending_scrape_valid_until); - } - PollMode::SkipReceiving => { - ::log::debug!("Postponing receiving requests because swarm worker channel is full. This means that the OS will be relied on to buffer incoming packets. To prevent this, raise config.worker_channel_size."); - - self.polling_mode = PollMode::SkipPolling; + for event in events.iter() { + if event.is_readable() { + self.read_and_handle_requests(); } } @@ -141,44 +97,10 @@ impl SocketWorker { ); } } - - // Check channel for any responses generated by swarm workers - self.handle_swarm_worker_responses(); - - // Try sending pending requests - while let Some((index, request, addr)) = self.pending_requests.pop() { - if let Err(r) = self.request_sender.try_send_to(index, request, addr) { - self.pending_requests.push(r); - - self.polling_mode = PollMode::SkipReceiving; - - break; - } - } - - // Run periodic ValidUntil updates and state cleaning - if iter_counter % 256 == 0 { - let seconds_since_start = self.shared_state.server_start_instant.seconds_elapsed(); - - pending_scrape_valid_until = ValidUntil::new_with_now( - seconds_since_start, - self.config.cleaning.max_pending_scrape_age, - ); - - let now = Instant::now(); - - if now > last_pending_scrape_cleaning + pending_scrape_cleaning_duration { - self.pending_scrape_responses.clean(seconds_since_start); - - last_pending_scrape_cleaning = now; - } - } - - iter_counter = iter_counter.wrapping_add(1); } } - fn read_and_handle_requests(&mut self, pending_scrape_valid_until: ValidUntil) { + fn read_and_handle_requests(&mut self) { let max_scrape_torrents = self.config.protocol.max_scrape_torrents; loop { @@ -222,14 +144,7 @@ impl SocketWorker { statistics.requests.fetch_add(1, Ordering::Relaxed); } - if let Err(HandleRequestError::RequestChannelFull(failed_requests)) = - self.handle_request(pending_scrape_valid_until, request, src) - { - self.pending_requests.extend(failed_requests); - self.polling_mode = PollMode::SkipReceiving; - - break; - } + self.handle_request(request, src); } Err(RequestParseError::Sendable { connection_id, @@ -271,20 +186,13 @@ impl SocketWorker { } } - fn handle_request( - &mut self, - pending_scrape_valid_until: ValidUntil, - request: Request, - src: CanonicalSocketAddr, - ) -> Result<(), HandleRequestError> { + fn handle_request(&mut self, request: Request, src: CanonicalSocketAddr) { let access_list_mode = self.config.access_list.mode; match request { Request::Connect(request) => { - let connection_id = self.validator.create_connection_id(src); - let response = ConnectResponse { - connection_id, + connection_id: self.validator.create_connection_id(src), transaction_id: request.transaction_id, }; @@ -297,8 +205,6 @@ impl SocketWorker { Response::Connect(response), src, ); - - Ok(()) } Request::Announce(request) => { if self @@ -310,14 +216,29 @@ impl SocketWorker { .load() .allows(access_list_mode, &request.info_hash.0) { - let worker_index = - SwarmWorkerIndex::from_info_hash(&self.config, request.info_hash); - - self.request_sender - .try_send_to(worker_index, ConnectedRequest::Announce(request), src) - .map_err(|request| { - HandleRequestError::RequestChannelFull(vec![request]) - }) + let peer_valid_until = ValidUntil::new( + self.shared_state.server_start_instant, + self.config.cleaning.max_peer_age, + ); + + let response = self.shared_state.torrent_maps.announce( + &self.config, + &self.statistics_sender, + &mut self.rng, + &request, + src, + peer_valid_until, + ); + + send_response( + &self.config, + &self.statistics, + &mut self.socket, + &mut self.buffer, + &mut self.opt_resend_buffer, + response, + src, + ); } else { let response = ErrorResponse { transaction_id: request.transaction_id, @@ -333,11 +254,7 @@ impl SocketWorker { Response::Error(response), src, ); - - Ok(()) } - } else { - Ok(()) } } Request::Scrape(request) => { @@ -345,64 +262,21 @@ impl SocketWorker { .validator .connection_id_valid(src, request.connection_id) { - let split_requests = self.pending_scrape_responses.prepare_split_requests( + let response = self.shared_state.torrent_maps.scrape(request, src); + + send_response( &self.config, - request, - pending_scrape_valid_until, + &self.statistics, + &mut self.socket, + &mut self.buffer, + &mut self.opt_resend_buffer, + Response::Scrape(response), + src, ); - - let mut failed = Vec::new(); - - for (swarm_worker_index, request) in split_requests { - if let Err(request) = self.request_sender.try_send_to( - swarm_worker_index, - ConnectedRequest::Scrape(request), - src, - ) { - failed.push(request); - } - } - - if failed.is_empty() { - Ok(()) - } else { - Err(HandleRequestError::RequestChannelFull(failed)) - } - } else { - Ok(()) } } } } - - fn handle_swarm_worker_responses(&mut self) { - for (addr, response) in self.response_receiver.try_iter() { - let response = match response { - ConnectedResponse::Scrape(response) => { - if let Some(r) = self - .pending_scrape_responses - .add_and_get_finished(&response) - { - Response::Scrape(r) - } else { - continue; - } - } - ConnectedResponse::AnnounceIpv4(r) => Response::AnnounceIpv4(r), - ConnectedResponse::AnnounceIpv6(r) => Response::AnnounceIpv6(r), - }; - - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - response, - addr, - ); - } - } } fn send_response( @@ -488,4 +362,6 @@ fn send_response( } } } + + ::log::debug!("send response fn finished"); } diff --git a/crates/udp/src/workers/socket/mod.rs b/crates/udp/src/workers/socket/mod.rs index d55e69dd..67e6f7fe 100644 --- a/crates/udp/src/workers/socket/mod.rs +++ b/crates/udp/src/workers/socket/mod.rs @@ -6,12 +6,13 @@ mod validator; use anyhow::Context; use aquatic_common::privileges::PrivilegeDropper; +use crossbeam_channel::Sender; use socket2::{Domain, Protocol, Socket, Type}; use crate::{ common::{ CachePaddedArc, ConnectedRequestSender, ConnectedResponseReceiver, IpVersionStatistics, - SocketWorkerStatistics, State, + SocketWorkerStatistics, State, StatisticsMessage, }, config::Config, }; @@ -43,11 +44,11 @@ pub fn run_socket_worker( config: Config, shared_state: State, statistics: CachePaddedArc>, + statistics_sender: Sender, validator: ConnectionValidator, - request_sender: ConnectedRequestSender, - response_receiver: ConnectedResponseReceiver, priv_dropper: PrivilegeDropper, ) -> anyhow::Result<()> { + /* #[cfg(all(target_os = "linux", feature = "io-uring"))] if config.network.use_io_uring { self::uring::supported_on_current_kernel().context("check for io_uring compatibility")?; @@ -62,14 +63,14 @@ pub fn run_socket_worker( priv_dropper, ); } + */ self::mio::SocketWorker::run( config, shared_state, statistics, + statistics_sender, validator, - request_sender, - response_receiver, priv_dropper, ) } From a2e1dd4eef41e3701ff29c58452e969117555c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 11:35:52 +0100 Subject: [PATCH 03/26] udp: use shared swarm state in io uring implementation --- crates/udp/src/workers/socket/mod.rs | 5 +- crates/udp/src/workers/socket/uring/mod.rs | 106 ++++++--------------- 2 files changed, 28 insertions(+), 83 deletions(-) diff --git a/crates/udp/src/workers/socket/mod.rs b/crates/udp/src/workers/socket/mod.rs index 67e6f7fe..2d71fba4 100644 --- a/crates/udp/src/workers/socket/mod.rs +++ b/crates/udp/src/workers/socket/mod.rs @@ -48,7 +48,6 @@ pub fn run_socket_worker( validator: ConnectionValidator, priv_dropper: PrivilegeDropper, ) -> anyhow::Result<()> { - /* #[cfg(all(target_os = "linux", feature = "io-uring"))] if config.network.use_io_uring { self::uring::supported_on_current_kernel().context("check for io_uring compatibility")?; @@ -57,13 +56,11 @@ pub fn run_socket_worker( config, shared_state, statistics, + statistics_sender, validator, - request_sender, - response_receiver, priv_dropper, ); } - */ self::mio::SocketWorker::run( config, diff --git a/crates/udp/src/workers/socket/uring/mod.rs b/crates/udp/src/workers/socket/uring/mod.rs index fe8b4907..7ad26c71 100644 --- a/crates/udp/src/workers/socket/uring/mod.rs +++ b/crates/udp/src/workers/socket/uring/mod.rs @@ -11,6 +11,7 @@ use std::sync::atomic::Ordering; use anyhow::Context; use aquatic_common::access_list::AccessListCache; +use crossbeam_channel::Sender; use io_uring::opcode::Timeout; use io_uring::types::{Fixed, Timespec}; use io_uring::{IoUring, Probe}; @@ -20,6 +21,8 @@ use aquatic_common::{ ValidUntil, }; use aquatic_udp_protocol::*; +use rand::rngs::SmallRng; +use rand::SeedableRng; use crate::common::*; use crate::config::Config; @@ -28,7 +31,6 @@ use self::buf_ring::BufRing; use self::recv_helper::RecvHelper; use self::send_buffers::{ResponseType, SendBuffers}; -use super::storage::PendingScrapeResponseSlab; use super::validator::ConnectionValidator; use super::{create_socket, EXTRA_PACKET_SIZE_IPV4, EXTRA_PACKET_SIZE_IPV6}; @@ -76,13 +78,11 @@ pub struct SocketWorker { config: Config, shared_state: State, statistics: CachePaddedArc>, - request_sender: ConnectedRequestSender, - response_receiver: ConnectedResponseReceiver, + statistics_sender: Sender, access_list_cache: AccessListCache, validator: ConnectionValidator, #[allow(dead_code)] socket: UdpSocket, - pending_scrape_responses: PendingScrapeResponseSlab, buf_ring: BufRing, send_buffers: SendBuffers, recv_helper: RecvHelper, @@ -91,7 +91,8 @@ pub struct SocketWorker { recv_sqe: io_uring::squeue::Entry, pulse_timeout_sqe: io_uring::squeue::Entry, cleaning_timeout_sqe: io_uring::squeue::Entry, - pending_scrape_valid_until: ValidUntil, + peer_valid_until: ValidUntil, + rng: SmallRng, } impl SocketWorker { @@ -99,9 +100,8 @@ impl SocketWorker { config: Config, shared_state: State, statistics: CachePaddedArc>, + statistics_sender: Sender, validator: ConnectionValidator, - request_sender: ConnectedRequestSender, - response_receiver: ConnectedResponseReceiver, priv_dropper: PrivilegeDropper, ) -> anyhow::Result<()> { let ring_entries = config.network.ring_size.next_power_of_two(); @@ -163,20 +163,18 @@ impl SocketWorker { cleaning_timeout_sqe.clone(), ]; - let pending_scrape_valid_until = ValidUntil::new( + let peer_valid_until = ValidUntil::new( shared_state.server_start_instant, - config.cleaning.max_pending_scrape_age, + config.cleaning.max_peer_age, ); let mut worker = Self { config, shared_state, statistics, + statistics_sender, validator, - request_sender, - response_receiver, access_list_cache, - pending_scrape_responses: Default::default(), send_buffers, recv_helper, local_responses: Default::default(), @@ -186,7 +184,8 @@ impl SocketWorker { cleaning_timeout_sqe, resubmittable_sqe_buf, socket, - pending_scrape_valid_until, + peer_valid_until, + rng: SmallRng::from_entropy(), }; CurrentRing::with(|ring| worker.run_inner(ring)); @@ -231,43 +230,6 @@ impl SocketWorker { } } - // Enqueue swarm worker responses - for _ in 0..(sq_space - num_send_added) { - let (addr, response) = if let Ok(r) = self.response_receiver.try_recv() { - r - } else { - break; - }; - - let response = match response { - ConnectedResponse::AnnounceIpv4(r) => Response::AnnounceIpv4(r), - ConnectedResponse::AnnounceIpv6(r) => Response::AnnounceIpv6(r), - ConnectedResponse::Scrape(r) => { - if let Some(r) = self.pending_scrape_responses.add_and_get_finished(&r) { - Response::Scrape(r) - } else { - continue; - } - } - }; - - match self.send_buffers.prepare_entry(response, addr) { - Ok(entry) => { - unsafe { ring.submission().push(&entry).unwrap() }; - - num_send_added += 1; - } - Err(send_buffers::Error::NoBuffers(response)) => { - self.local_responses.push_back((response, addr)); - - break; - } - Err(send_buffers::Error::SerializationFailed(err)) => { - ::log::error!("Failed serializing response: {:#}", err); - } - } - } - // Wait for all sendmsg entries to complete. If none were added, // wait for at least one recvmsg or timeout in order to avoid // busy-polling if there is no incoming data. @@ -293,18 +255,15 @@ impl SocketWorker { } } USER_DATA_PULSE_TIMEOUT => { - self.pending_scrape_valid_until = ValidUntil::new( + self.peer_valid_until = ValidUntil::new( self.shared_state.server_start_instant, - self.config.cleaning.max_pending_scrape_age, + self.config.cleaning.max_peer_age, ); self.resubmittable_sqe_buf .push(self.pulse_timeout_sqe.clone()); } USER_DATA_CLEANING_TIMEOUT => { - self.pending_scrape_responses - .clean(self.shared_state.server_start_instant.seconds_elapsed()); - self.resubmittable_sqe_buf .push(self.cleaning_timeout_sqe.clone()); } @@ -470,16 +429,16 @@ impl SocketWorker { .load() .allows(access_list_mode, &request.info_hash.0) { - let worker_index = - SwarmWorkerIndex::from_info_hash(&self.config, request.info_hash); - - if self - .request_sender - .try_send_to(worker_index, ConnectedRequest::Announce(request), src) - .is_err() - { - ::log::warn!("request sender full, dropping request"); - } + let response = self.shared_state.torrent_maps.announce( + &self.config, + &self.statistics_sender, + &mut self.rng, + &request, + src, + self.peer_valid_until, + ); + + self.local_responses.push_back((response, src)); } else { let response = Response::Error(ErrorResponse { transaction_id: request.transaction_id, @@ -495,21 +454,10 @@ impl SocketWorker { .validator .connection_id_valid(src, request.connection_id) { - let split_requests = self.pending_scrape_responses.prepare_split_requests( - &self.config, - request, - self.pending_scrape_valid_until, - ); + let response = + Response::Scrape(self.shared_state.torrent_maps.scrape(request, src)); - for (swarm_worker_index, request) in split_requests { - if self - .request_sender - .try_send_to(swarm_worker_index, ConnectedRequest::Scrape(request), src) - .is_err() - { - ::log::warn!("request sender full, dropping request"); - } - } + self.local_responses.push_back((response, src)); } } } From 7fa143964efed7add60c157bfa31c3ee221ccfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 11:40:11 +0100 Subject: [PATCH 04/26] udp: remove swarm worker and related logic --- crates/udp/src/common.rs | 141 +----- crates/udp/src/lib.rs | 10 +- crates/udp/src/workers/mod.rs | 1 - crates/udp/src/workers/socket/mod.rs | 4 +- crates/udp/src/workers/socket/storage.rs | 218 --------- crates/udp/src/workers/swarm/mod.rs | 149 ------ crates/udp/src/workers/swarm/storage.rs | 563 ----------------------- 7 files changed, 4 insertions(+), 1082 deletions(-) delete mode 100644 crates/udp/src/workers/socket/storage.rs delete mode 100644 crates/udp/src/workers/swarm/mod.rs delete mode 100644 crates/udp/src/workers/swarm/storage.rs diff --git a/crates/udp/src/common.rs b/crates/udp/src/common.rs index d26d519b..7e3f7a37 100644 --- a/crates/udp/src/common.rs +++ b/crates/udp/src/common.rs @@ -1,13 +1,9 @@ -use std::collections::BTreeMap; -use std::hash::Hash; use std::iter::repeat_with; use std::sync::atomic::AtomicUsize; use std::sync::Arc; -use crossbeam_channel::{Receiver, SendError, Sender, TrySendError}; - use aquatic_common::access_list::AccessListArcSwap; -use aquatic_common::{CanonicalSocketAddr, ServerStartInstant}; +use aquatic_common::ServerStartInstant; use aquatic_udp_protocol::*; use crossbeam_utils::CachePadded; use hdrhistogram::Histogram; @@ -33,141 +29,6 @@ impl IpVersion { } } -#[derive(Clone, Copy, Debug)] -pub struct SocketWorkerIndex(pub usize); - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct SwarmWorkerIndex(pub usize); - -impl SwarmWorkerIndex { - pub fn from_info_hash(config: &Config, info_hash: InfoHash) -> Self { - Self(info_hash.0[0] as usize % config.swarm_workers) - } -} - -#[derive(Debug)] -pub struct PendingScrapeRequest { - pub slab_key: usize, - pub info_hashes: BTreeMap, -} - -#[derive(Debug)] -pub struct PendingScrapeResponse { - pub slab_key: usize, - pub torrent_stats: BTreeMap, -} - -#[derive(Debug)] -pub enum ConnectedRequest { - Announce(AnnounceRequest), - Scrape(PendingScrapeRequest), -} - -#[derive(Debug)] -pub enum ConnectedResponse { - AnnounceIpv4(AnnounceResponse), - AnnounceIpv6(AnnounceResponse), - Scrape(PendingScrapeResponse), -} - -pub struct ConnectedRequestSender { - index: SocketWorkerIndex, - senders: Vec>, -} - -impl ConnectedRequestSender { - pub fn new( - index: SocketWorkerIndex, - senders: Vec>, - ) -> Self { - Self { index, senders } - } - - pub fn try_send_to( - &self, - index: SwarmWorkerIndex, - request: ConnectedRequest, - addr: CanonicalSocketAddr, - ) -> Result<(), (SwarmWorkerIndex, ConnectedRequest, CanonicalSocketAddr)> { - match self.senders[index.0].try_send((self.index, request, addr)) { - Ok(()) => Ok(()), - Err(TrySendError::Full(r)) => Err((index, r.1, r.2)), - Err(TrySendError::Disconnected(_)) => { - panic!("Request channel {} is disconnected", index.0); - } - } - } -} - -pub struct ConnectedResponseSender { - senders: Vec>, - to_any_last_index_picked: usize, -} - -impl ConnectedResponseSender { - pub fn new(senders: Vec>) -> Self { - Self { - senders, - to_any_last_index_picked: 0, - } - } - - pub fn try_send_to( - &self, - index: SocketWorkerIndex, - addr: CanonicalSocketAddr, - response: ConnectedResponse, - ) -> Result<(), TrySendError<(CanonicalSocketAddr, ConnectedResponse)>> { - self.senders[index.0].try_send((addr, response)) - } - - pub fn send_to( - &self, - index: SocketWorkerIndex, - addr: CanonicalSocketAddr, - response: ConnectedResponse, - ) -> Result<(), SendError<(CanonicalSocketAddr, ConnectedResponse)>> { - self.senders[index.0].send((addr, response)) - } - - pub fn send_to_any( - &mut self, - addr: CanonicalSocketAddr, - response: ConnectedResponse, - ) -> Result<(), SendError<(CanonicalSocketAddr, ConnectedResponse)>> { - let start = self.to_any_last_index_picked + 1; - - let mut message = Some((addr, response)); - - for i in (start..start + self.senders.len()).map(|i| i % self.senders.len()) { - match self.senders[i].try_send(message.take().unwrap()) { - Ok(()) => { - self.to_any_last_index_picked = i; - - return Ok(()); - } - Err(TrySendError::Full(msg)) => { - message = Some(msg); - } - Err(TrySendError::Disconnected(_)) => { - panic!("ConnectedResponseReceiver disconnected"); - } - } - } - - let (addr, response) = message.unwrap(); - - self.to_any_last_index_picked = start % self.senders.len(); - self.send_to( - SocketWorkerIndex(self.to_any_last_index_picked), - addr, - response, - ) - } -} - -pub type ConnectedResponseReceiver = Receiver<(CanonicalSocketAddr, ConnectedResponse)>; - #[derive(Clone)] pub struct Statistics { pub socket: Vec>>, diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index f2a7ec87..6e27fc49 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -3,13 +3,12 @@ pub mod config; pub mod swarm; pub mod workers; -use std::collections::BTreeMap; use std::thread::{sleep, Builder, JoinHandle}; use std::time::Duration; use anyhow::Context; use aquatic_common::WorkerType; -use crossbeam_channel::{bounded, unbounded}; +use crossbeam_channel::unbounded; use signal_hook::consts::SIGUSR1; use signal_hook::iterator::Signals; @@ -18,14 +17,9 @@ use aquatic_common::access_list::update_access_list; use aquatic_common::cpu_pinning::{pin_current_if_configured_to, WorkerIndex}; use aquatic_common::privileges::PrivilegeDropper; -use common::{ - ConnectedRequestSender, ConnectedResponseSender, SocketWorkerIndex, State, Statistics, - SwarmWorkerIndex, -}; +use common::{State, Statistics}; use config::Config; -use swarm::TorrentMaps; use workers::socket::ConnectionValidator; -use workers::swarm::SwarmWorker; pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker"; pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/udp/src/workers/mod.rs b/crates/udp/src/workers/mod.rs index 5446a1f6..02af829e 100644 --- a/crates/udp/src/workers/mod.rs +++ b/crates/udp/src/workers/mod.rs @@ -1,3 +1,2 @@ pub mod socket; pub mod statistics; -pub mod swarm; diff --git a/crates/udp/src/workers/socket/mod.rs b/crates/udp/src/workers/socket/mod.rs index 2d71fba4..ef1adeac 100644 --- a/crates/udp/src/workers/socket/mod.rs +++ b/crates/udp/src/workers/socket/mod.rs @@ -1,5 +1,4 @@ mod mio; -mod storage; #[cfg(all(target_os = "linux", feature = "io-uring"))] mod uring; mod validator; @@ -11,8 +10,7 @@ use socket2::{Domain, Protocol, Socket, Type}; use crate::{ common::{ - CachePaddedArc, ConnectedRequestSender, ConnectedResponseReceiver, IpVersionStatistics, - SocketWorkerStatistics, State, StatisticsMessage, + CachePaddedArc, IpVersionStatistics, SocketWorkerStatistics, State, StatisticsMessage, }, config::Config, }; diff --git a/crates/udp/src/workers/socket/storage.rs b/crates/udp/src/workers/socket/storage.rs deleted file mode 100644 index 84c11a79..00000000 --- a/crates/udp/src/workers/socket/storage.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::collections::BTreeMap; - -use hashbrown::HashMap; -use slab::Slab; - -use aquatic_common::{SecondsSinceServerStart, ValidUntil}; -use aquatic_udp_protocol::*; - -use crate::common::*; -use crate::config::Config; - -#[derive(Debug)] -pub struct PendingScrapeResponseSlabEntry { - num_pending: usize, - valid_until: ValidUntil, - torrent_stats: BTreeMap, - transaction_id: TransactionId, -} - -#[derive(Default)] -pub struct PendingScrapeResponseSlab(Slab); - -impl PendingScrapeResponseSlab { - pub fn prepare_split_requests( - &mut self, - config: &Config, - request: ScrapeRequest, - valid_until: ValidUntil, - ) -> impl IntoIterator { - let capacity = config.swarm_workers.min(request.info_hashes.len()); - let mut split_requests: HashMap = - HashMap::with_capacity(capacity); - - if request.info_hashes.is_empty() { - ::log::warn!( - "Attempted to prepare PendingScrapeResponseSlab entry with zero info hashes" - ); - - return split_requests; - } - - let vacant_entry = self.0.vacant_entry(); - let slab_key = vacant_entry.key(); - - for (i, info_hash) in request.info_hashes.into_iter().enumerate() { - let split_request = split_requests - .entry(SwarmWorkerIndex::from_info_hash(config, info_hash)) - .or_insert_with(|| PendingScrapeRequest { - slab_key, - info_hashes: BTreeMap::new(), - }); - - split_request.info_hashes.insert(i, info_hash); - } - - vacant_entry.insert(PendingScrapeResponseSlabEntry { - num_pending: split_requests.len(), - valid_until, - torrent_stats: Default::default(), - transaction_id: request.transaction_id, - }); - - split_requests - } - - pub fn add_and_get_finished( - &mut self, - response: &PendingScrapeResponse, - ) -> Option { - let finished = if let Some(entry) = self.0.get_mut(response.slab_key) { - entry.num_pending -= 1; - - entry.torrent_stats.extend(response.torrent_stats.iter()); - - entry.num_pending == 0 - } else { - ::log::warn!( - "PendingScrapeResponseSlab.add didn't find entry for key {:?}", - response.slab_key - ); - - false - }; - - if finished { - let entry = self.0.remove(response.slab_key); - - Some(ScrapeResponse { - transaction_id: entry.transaction_id, - torrent_stats: entry.torrent_stats.into_values().collect(), - }) - } else { - None - } - } - - pub fn clean(&mut self, now: SecondsSinceServerStart) { - self.0.retain(|k, v| { - if v.valid_until.valid(now) { - true - } else { - ::log::warn!( - "Unconsumed PendingScrapeResponseSlab entry. {:?}: {:?}", - k, - v - ); - - false - } - }); - - self.0.shrink_to_fit(); - } -} - -#[cfg(test)] -mod tests { - use aquatic_common::ServerStartInstant; - use quickcheck::TestResult; - use quickcheck_macros::quickcheck; - - use super::*; - - #[quickcheck] - fn test_pending_scrape_response_slab( - request_data: Vec<(i32, i64, u8)>, - swarm_workers: u8, - ) -> TestResult { - if swarm_workers == 0 { - return TestResult::discard(); - } - - let config = Config { - swarm_workers: swarm_workers as usize, - ..Default::default() - }; - - let valid_until = ValidUntil::new(ServerStartInstant::new(), 1); - - let mut map = PendingScrapeResponseSlab::default(); - - let mut requests = Vec::new(); - - for (t, c, b) in request_data { - if b == 0 { - return TestResult::discard(); - } - - let mut info_hashes = Vec::new(); - - for i in 0..b { - let info_hash = InfoHash([i; 20]); - - info_hashes.push(info_hash); - } - - let request = ScrapeRequest { - transaction_id: TransactionId::new(t), - connection_id: ConnectionId::new(c), - info_hashes, - }; - - requests.push(request); - } - - let mut all_split_requests = Vec::new(); - - for request in requests.iter() { - let split_requests = - map.prepare_split_requests(&config, request.to_owned(), valid_until); - - all_split_requests.push( - split_requests - .into_iter() - .collect::>(), - ); - } - - assert_eq!(map.0.len(), requests.len()); - - let mut responses = Vec::new(); - - for split_requests in all_split_requests { - for (worker_index, split_request) in split_requests { - assert!(worker_index.0 < swarm_workers as usize); - - let torrent_stats = split_request - .info_hashes - .into_iter() - .map(|(i, info_hash)| { - ( - i, - TorrentScrapeStatistics { - seeders: NumberOfPeers::new((info_hash.0[0]) as i32), - leechers: NumberOfPeers::new(0), - completed: NumberOfDownloads::new(0), - }, - ) - }) - .collect(); - - let response = PendingScrapeResponse { - slab_key: split_request.slab_key, - torrent_stats, - }; - - if let Some(response) = map.add_and_get_finished(&response) { - responses.push(response); - } - } - } - - assert!(map.0.is_empty()); - assert_eq!(responses.len(), requests.len()); - - TestResult::from_bool(true) - } -} diff --git a/crates/udp/src/workers/swarm/mod.rs b/crates/udp/src/workers/swarm/mod.rs deleted file mode 100644 index ccdab8ad..00000000 --- a/crates/udp/src/workers/swarm/mod.rs +++ /dev/null @@ -1,149 +0,0 @@ -mod storage; - -use std::net::IpAddr; -use std::sync::atomic::Ordering; -use std::time::Duration; -use std::time::Instant; - -use crossbeam_channel::Receiver; -use crossbeam_channel::Sender; -use rand::{rngs::SmallRng, SeedableRng}; - -use aquatic_common::{CanonicalSocketAddr, ValidUntil}; - -use crate::common::*; -use crate::config::Config; - -use storage::TorrentMaps; - -pub struct SwarmWorker { - pub config: Config, - pub state: State, - pub statistics: CachePaddedArc>, - pub request_receiver: Receiver<(SocketWorkerIndex, ConnectedRequest, CanonicalSocketAddr)>, - pub response_sender: ConnectedResponseSender, - pub statistics_sender: Sender, - pub worker_index: SwarmWorkerIndex, -} - -impl SwarmWorker { - pub fn run(&mut self) -> anyhow::Result<()> { - let mut torrents = TorrentMaps::default(); - let mut rng = SmallRng::from_entropy(); - - let timeout = Duration::from_millis(self.config.request_channel_recv_timeout_ms); - let mut peer_valid_until = ValidUntil::new( - self.state.server_start_instant, - self.config.cleaning.max_peer_age, - ); - - let cleaning_interval = Duration::from_secs(self.config.cleaning.torrent_cleaning_interval); - let statistics_update_interval = Duration::from_secs(self.config.statistics.interval); - - let mut last_cleaning = Instant::now(); - let mut last_statistics_update = Instant::now(); - - let mut iter_counter = 0usize; - - loop { - if let Ok((sender_index, request, src)) = self.request_receiver.recv_timeout(timeout) { - // It is OK to block here as long as we don't also do blocking - // sends in socket workers (doing both could cause a deadlock) - match (request, src.get().ip()) { - (ConnectedRequest::Announce(request), IpAddr::V4(ip)) => { - let response = torrents - .ipv4 - .0 - .entry(request.info_hash) - .or_default() - .announce( - &self.config, - &self.statistics_sender, - &mut rng, - &request, - ip.into(), - peer_valid_until, - ); - - // It doesn't matter which socket worker receives announce responses - self.response_sender - .send_to_any(src, ConnectedResponse::AnnounceIpv4(response)) - .expect("swarm response channel is closed"); - } - (ConnectedRequest::Announce(request), IpAddr::V6(ip)) => { - let response = torrents - .ipv6 - .0 - .entry(request.info_hash) - .or_default() - .announce( - &self.config, - &self.statistics_sender, - &mut rng, - &request, - ip.into(), - peer_valid_until, - ); - - // It doesn't matter which socket worker receives announce responses - self.response_sender - .send_to_any(src, ConnectedResponse::AnnounceIpv6(response)) - .expect("swarm response channel is closed"); - } - (ConnectedRequest::Scrape(request), IpAddr::V4(_)) => { - let response = torrents.ipv4.scrape(request); - - self.response_sender - .send_to(sender_index, src, ConnectedResponse::Scrape(response)) - .expect("swarm response channel is closed"); - } - (ConnectedRequest::Scrape(request), IpAddr::V6(_)) => { - let response = torrents.ipv6.scrape(request); - - self.response_sender - .send_to(sender_index, src, ConnectedResponse::Scrape(response)) - .expect("swarm response channel is closed"); - } - }; - } - - // Run periodic tasks - if iter_counter % 128 == 0 { - let now = Instant::now(); - - peer_valid_until = ValidUntil::new( - self.state.server_start_instant, - self.config.cleaning.max_peer_age, - ); - - if now > last_cleaning + cleaning_interval { - torrents.clean_and_update_statistics( - &self.config, - &self.state, - &self.statistics, - &self.statistics_sender, - &self.state.access_list, - ); - - last_cleaning = now; - } - if self.config.statistics.active() - && now > last_statistics_update + statistics_update_interval - { - self.statistics - .ipv4 - .torrents - .store(torrents.ipv4.num_torrents(), Ordering::Relaxed); - self.statistics - .ipv6 - .torrents - .store(torrents.ipv6.num_torrents(), Ordering::Relaxed); - - last_statistics_update = now; - } - } - - iter_counter = iter_counter.wrapping_add(1); - } - } -} diff --git a/crates/udp/src/workers/swarm/storage.rs b/crates/udp/src/workers/swarm/storage.rs deleted file mode 100644 index 3b042eac..00000000 --- a/crates/udp/src/workers/swarm/storage.rs +++ /dev/null @@ -1,563 +0,0 @@ -use std::sync::atomic::Ordering; -use std::sync::Arc; - -use aquatic_common::IndexMap; -use aquatic_common::SecondsSinceServerStart; -use aquatic_common::{ - access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, - ValidUntil, -}; - -use aquatic_udp_protocol::*; -use arrayvec::ArrayVec; -use crossbeam_channel::Sender; -use hdrhistogram::Histogram; -use rand::prelude::SmallRng; -use rand::Rng; - -use crate::common::*; -use crate::config::Config; - -const SMALL_PEER_MAP_CAPACITY: usize = 2; - -pub struct TorrentMaps { - pub ipv4: TorrentMap, - pub ipv6: TorrentMap, -} - -impl Default for TorrentMaps { - fn default() -> Self { - Self { - ipv4: TorrentMap(Default::default()), - ipv6: TorrentMap(Default::default()), - } - } -} - -impl TorrentMaps { - /// Remove forbidden or inactive torrents, reclaim space and update statistics - pub fn clean_and_update_statistics( - &mut self, - config: &Config, - state: &State, - statistics: &CachePaddedArc>, - statistics_sender: &Sender, - access_list: &Arc, - ) { - let mut cache = create_access_list_cache(access_list); - let mode = config.access_list.mode; - let now = state.server_start_instant.seconds_elapsed(); - - let ipv4 = - self.ipv4 - .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); - let ipv6 = - self.ipv6 - .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); - - if config.statistics.active() { - statistics.ipv4.peers.store(ipv4.0, Ordering::Relaxed); - statistics.ipv6.peers.store(ipv6.0, Ordering::Relaxed); - - if let Some(message) = ipv4.1.map(StatisticsMessage::Ipv4PeerHistogram) { - if let Err(err) = statistics_sender.try_send(message) { - ::log::error!("couldn't send statistics message: {:#}", err); - } - } - if let Some(message) = ipv6.1.map(StatisticsMessage::Ipv6PeerHistogram) { - if let Err(err) = statistics_sender.try_send(message) { - ::log::error!("couldn't send statistics message: {:#}", err); - } - } - } - } -} - -#[derive(Default)] -pub struct TorrentMap(pub IndexMap>); - -impl TorrentMap { - pub fn scrape(&mut self, request: PendingScrapeRequest) -> PendingScrapeResponse { - let torrent_stats = request - .info_hashes - .into_iter() - .map(|(i, info_hash)| { - let stats = self - .0 - .get(&info_hash) - .map(|torrent_data| torrent_data.scrape_statistics()) - .unwrap_or_else(|| TorrentScrapeStatistics { - seeders: NumberOfPeers::new(0), - leechers: NumberOfPeers::new(0), - completed: NumberOfDownloads::new(0), - }); - - (i, stats) - }) - .collect(); - - PendingScrapeResponse { - slab_key: request.slab_key, - torrent_stats, - } - } - /// Remove forbidden or inactive torrents, reclaim space and return number of remaining peers - fn clean_and_get_statistics( - &mut self, - config: &Config, - statistics_sender: &Sender, - access_list_cache: &mut AccessListCache, - access_list_mode: AccessListMode, - now: SecondsSinceServerStart, - ) -> (usize, Option>) { - let mut total_num_peers = 0; - - let mut opt_histogram: Option> = if config.statistics.torrent_peer_histograms - { - match Histogram::new(3) { - Ok(histogram) => Some(histogram), - Err(err) => { - ::log::error!("Couldn't create peer histogram: {:#}", err); - - None - } - } - } else { - None - }; - - self.0.retain(|info_hash, torrent| { - if !access_list_cache - .load() - .allows(access_list_mode, &info_hash.0) - { - return false; - } - - let num_peers = match torrent { - TorrentData::Small(peer_map) => { - peer_map.clean_and_get_num_peers(config, statistics_sender, now) - } - TorrentData::Large(peer_map) => { - let num_peers = - peer_map.clean_and_get_num_peers(config, statistics_sender, now); - - if let Some(peer_map) = peer_map.try_shrink() { - *torrent = TorrentData::Small(peer_map); - } - - num_peers - } - }; - - total_num_peers += num_peers; - - match opt_histogram { - Some(ref mut histogram) if num_peers > 0 => { - let n = num_peers.try_into().expect("Couldn't fit usize into u64"); - - if let Err(err) = histogram.record(n) { - ::log::error!("Couldn't record {} to histogram: {:#}", n, err); - } - } - _ => (), - } - - num_peers > 0 - }); - - self.0.shrink_to_fit(); - - (total_num_peers, opt_histogram) - } - - pub fn num_torrents(&self) -> usize { - self.0.len() - } -} - -pub enum TorrentData { - Small(SmallPeerMap), - Large(LargePeerMap), -} - -impl TorrentData { - pub fn announce( - &mut self, - config: &Config, - statistics_sender: &Sender, - rng: &mut SmallRng, - request: &AnnounceRequest, - ip_address: I, - valid_until: ValidUntil, - ) -> AnnounceResponse { - let max_num_peers_to_take: usize = if request.peers_wanted.0.get() <= 0 { - config.protocol.max_response_peers - } else { - ::std::cmp::min( - config.protocol.max_response_peers, - request.peers_wanted.0.get().try_into().unwrap(), - ) - }; - - let status = - PeerStatus::from_event_and_bytes_left(request.event.into(), request.bytes_left); - - let peer_map_key = ResponsePeer { - ip_address, - port: request.port, - }; - - // Create the response before inserting the peer. This means that we - // don't have to filter it out from the response peers, and that the - // reported number of seeders/leechers will not include it - let (response, opt_removed_peer) = match self { - Self::Small(peer_map) => { - let opt_removed_peer = peer_map.remove(&peer_map_key); - - let (seeders, leechers) = peer_map.num_seeders_leechers(); - - let response = AnnounceResponse { - fixed: AnnounceResponseFixedData { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval::new( - config.protocol.peer_announce_interval, - ), - leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), - seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), - }, - peers: peer_map.extract_response_peers(max_num_peers_to_take), - }; - - // Convert peer map to large variant if it is full and - // announcing peer is not stopped and will therefore be - // inserted - if peer_map.is_full() && status != PeerStatus::Stopped { - *self = Self::Large(peer_map.to_large()); - } - - (response, opt_removed_peer) - } - Self::Large(peer_map) => { - let opt_removed_peer = peer_map.remove_peer(&peer_map_key); - - let (seeders, leechers) = peer_map.num_seeders_leechers(); - - let response = AnnounceResponse { - fixed: AnnounceResponseFixedData { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval::new( - config.protocol.peer_announce_interval, - ), - leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), - seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), - }, - peers: peer_map.extract_response_peers(rng, max_num_peers_to_take), - }; - - // Try shrinking the map if announcing peer is stopped and - // will therefore not be inserted - if status == PeerStatus::Stopped { - if let Some(peer_map) = peer_map.try_shrink() { - *self = Self::Small(peer_map); - } - } - - (response, opt_removed_peer) - } - }; - - match status { - PeerStatus::Leeching | PeerStatus::Seeding => { - let peer = Peer { - peer_id: request.peer_id, - is_seeder: status == PeerStatus::Seeding, - valid_until, - }; - - match self { - Self::Small(peer_map) => peer_map.insert(peer_map_key, peer), - Self::Large(peer_map) => peer_map.insert(peer_map_key, peer), - } - - if config.statistics.peer_clients && opt_removed_peer.is_none() { - statistics_sender - .try_send(StatisticsMessage::PeerAdded(request.peer_id)) - .expect("statistics channel should be unbounded"); - } - } - PeerStatus::Stopped => { - if config.statistics.peer_clients && opt_removed_peer.is_some() { - statistics_sender - .try_send(StatisticsMessage::PeerRemoved(request.peer_id)) - .expect("statistics channel should be unbounded"); - } - } - }; - - response - } - - pub fn scrape_statistics(&self) -> TorrentScrapeStatistics { - let (seeders, leechers) = match self { - Self::Small(peer_map) => peer_map.num_seeders_leechers(), - Self::Large(peer_map) => peer_map.num_seeders_leechers(), - }; - - TorrentScrapeStatistics { - seeders: NumberOfPeers::new(seeders.try_into().unwrap_or(i32::MAX)), - leechers: NumberOfPeers::new(leechers.try_into().unwrap_or(i32::MAX)), - completed: NumberOfDownloads::new(0), - } - } -} - -impl Default for TorrentData { - fn default() -> Self { - Self::Small(SmallPeerMap(ArrayVec::default())) - } -} - -/// Store torrents with up to two peers without an extra heap allocation -/// -/// On public open trackers, this is likely to be the majority of torrents. -#[derive(Default, Debug)] -pub struct SmallPeerMap(ArrayVec<(ResponsePeer, Peer), SMALL_PEER_MAP_CAPACITY>); - -impl SmallPeerMap { - fn is_full(&self) -> bool { - self.0.is_full() - } - - fn num_seeders_leechers(&self) -> (usize, usize) { - let seeders = self.0.iter().filter(|(_, p)| p.is_seeder).count(); - let leechers = self.0.len() - seeders; - - (seeders, leechers) - } - - fn insert(&mut self, key: ResponsePeer, peer: Peer) { - self.0.push((key, peer)); - } - - fn remove(&mut self, key: &ResponsePeer) -> Option { - for (i, (k, _)) in self.0.iter().enumerate() { - if k == key { - return Some(self.0.remove(i).1); - } - } - - None - } - - fn extract_response_peers(&self, max_num_peers_to_take: usize) -> Vec> { - Vec::from_iter(self.0.iter().take(max_num_peers_to_take).map(|(k, _)| *k)) - } - - fn clean_and_get_num_peers( - &mut self, - config: &Config, - statistics_sender: &Sender, - now: SecondsSinceServerStart, - ) -> usize { - self.0.retain(|(_, peer)| { - let keep = peer.valid_until.valid(now); - - if !keep - && config.statistics.peer_clients - && statistics_sender - .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) - .is_err() - { - // Should never happen in practice - ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); - } - - keep - }); - - self.0.len() - } - - fn to_large(&self) -> LargePeerMap { - let (num_seeders, _) = self.num_seeders_leechers(); - let peers = self.0.iter().copied().collect(); - - LargePeerMap { peers, num_seeders } - } -} - -#[derive(Default)] -pub struct LargePeerMap { - peers: IndexMap, Peer>, - num_seeders: usize, -} - -impl LargePeerMap { - fn num_seeders_leechers(&self) -> (usize, usize) { - (self.num_seeders, self.peers.len() - self.num_seeders) - } - - fn insert(&mut self, key: ResponsePeer, peer: Peer) { - if peer.is_seeder { - self.num_seeders += 1; - } - - self.peers.insert(key, peer); - } - - fn remove_peer(&mut self, key: &ResponsePeer) -> Option { - let opt_removed_peer = self.peers.swap_remove(key); - - if let Some(Peer { - is_seeder: true, .. - }) = opt_removed_peer - { - self.num_seeders -= 1; - } - - opt_removed_peer - } - - /// Extract response peers - /// - /// If there are more peers in map than `max_num_peers_to_take`, do a random - /// selection of peers from first and second halves of map in order to avoid - /// returning too homogeneous peers. - /// - /// Does NOT filter out announcing peer. - pub fn extract_response_peers( - &self, - rng: &mut impl Rng, - max_num_peers_to_take: usize, - ) -> Vec> { - if self.peers.len() <= max_num_peers_to_take { - self.peers.keys().copied().collect() - } else { - let middle_index = self.peers.len() / 2; - let num_to_take_per_half = max_num_peers_to_take / 2; - - let offset_half_one = { - let from = 0; - let to = usize::max(1, middle_index - num_to_take_per_half); - - rng.gen_range(from..to) - }; - let offset_half_two = { - let from = middle_index; - let to = usize::max(middle_index + 1, self.peers.len() - num_to_take_per_half); - - rng.gen_range(from..to) - }; - - let end_half_one = offset_half_one + num_to_take_per_half; - let end_half_two = offset_half_two + num_to_take_per_half; - - let mut peers = Vec::with_capacity(max_num_peers_to_take); - - if let Some(slice) = self.peers.get_range(offset_half_one..end_half_one) { - peers.extend(slice.keys()); - } - if let Some(slice) = self.peers.get_range(offset_half_two..end_half_two) { - peers.extend(slice.keys()); - } - - peers - } - } - - fn clean_and_get_num_peers( - &mut self, - config: &Config, - statistics_sender: &Sender, - now: SecondsSinceServerStart, - ) -> usize { - self.peers.retain(|_, peer| { - let keep = peer.valid_until.valid(now); - - if !keep { - if peer.is_seeder { - self.num_seeders -= 1; - } - if config.statistics.peer_clients - && statistics_sender - .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) - .is_err() - { - // Should never happen in practice - ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); - } - } - - keep - }); - - if !self.peers.is_empty() { - self.peers.shrink_to_fit(); - } - - self.peers.len() - } - - fn try_shrink(&mut self) -> Option> { - (self.peers.len() <= SMALL_PEER_MAP_CAPACITY).then(|| { - SmallPeerMap(ArrayVec::from_iter( - self.peers.iter().map(|(k, v)| (*k, *v)), - )) - }) - } -} - -#[derive(Clone, Copy, Debug)] -struct Peer { - peer_id: PeerId, - is_seeder: bool, - valid_until: ValidUntil, -} - -#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] -pub enum PeerStatus { - Seeding, - Leeching, - Stopped, -} - -impl PeerStatus { - /// Determine peer status from announce event and number of bytes left. - /// - /// Likely, the last branch will be taken most of the time. - #[inline] - pub fn from_event_and_bytes_left(event: AnnounceEvent, bytes_left: NumberOfBytes) -> Self { - if event == AnnounceEvent::Stopped { - Self::Stopped - } else if bytes_left.0.get() == 0 { - Self::Seeding - } else { - Self::Leeching - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_peer_status_from_event_and_bytes_left() { - use PeerStatus::*; - - let f = PeerStatus::from_event_and_bytes_left; - - assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(0))); - assert_eq!(Stopped, f(AnnounceEvent::Stopped, NumberOfBytes::new(1))); - - assert_eq!(Seeding, f(AnnounceEvent::Started, NumberOfBytes::new(0))); - assert_eq!(Leeching, f(AnnounceEvent::Started, NumberOfBytes::new(1))); - - assert_eq!(Seeding, f(AnnounceEvent::Completed, NumberOfBytes::new(0))); - assert_eq!(Leeching, f(AnnounceEvent::Completed, NumberOfBytes::new(1))); - - assert_eq!(Seeding, f(AnnounceEvent::None, NumberOfBytes::new(0))); - assert_eq!(Leeching, f(AnnounceEvent::None, NumberOfBytes::new(1))); - } -} From 6d7ffd40ae849026025381b03d7bda80e6182c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 11:53:44 +0100 Subject: [PATCH 05/26] Update TODO --- TODO.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/TODO.md b/TODO.md index 17a10ef3..25bb6d7d 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,13 @@ ## High priority +* udp + * fix cleaning + * fix statistics + * fix config + * consider ways of avoiding response peer allocations + * make ConnectionValidator faster by avoiding calling time functions so often + ## Medium priority * stagger cleaning tasks? From c4fd3c9e8311b47c94331c95e3cc013bcaf75146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 15:48:09 +0100 Subject: [PATCH 06/26] udp: add cleaning worker --- TODO.md | 2 - crates/common/src/lib.rs | 2 + crates/udp/src/common.rs | 6 +- crates/udp/src/lib.rs | 31 +++++++--- crates/udp/src/swarm.rs | 9 +-- .../udp/src/workers/statistics/collector.rs | 59 ++++++++----------- 6 files changed, 57 insertions(+), 52 deletions(-) diff --git a/TODO.md b/TODO.md index 25bb6d7d..9024c23b 100644 --- a/TODO.md +++ b/TODO.md @@ -3,8 +3,6 @@ ## High priority * udp - * fix cleaning - * fix statistics * fix config * consider ways of avoiding response peer allocations * make ConnectionValidator faster by avoiding calling time functions so often diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index ea5b9285..fbd99b9b 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -163,6 +163,7 @@ pub enum WorkerType { Socket(usize), Statistics, Signals, + Cleaning, #[cfg(feature = "prometheus")] Prometheus, } @@ -174,6 +175,7 @@ impl Display for WorkerType { Self::Socket(index) => f.write_fmt(format_args!("Socket worker {}", index + 1)), Self::Statistics => f.write_str("Statistics worker"), Self::Signals => f.write_str("Signals worker"), + Self::Cleaning => f.write_str("Cleaning worker"), #[cfg(feature = "prometheus")] Self::Prometheus => f.write_str("Prometheus worker"), } diff --git a/crates/udp/src/common.rs b/crates/udp/src/common.rs index 7e3f7a37..c42aa358 100644 --- a/crates/udp/src/common.rs +++ b/crates/udp/src/common.rs @@ -32,7 +32,7 @@ impl IpVersion { #[derive(Clone)] pub struct Statistics { pub socket: Vec>>, - pub swarm: Vec>>, + pub swarm: CachePaddedArc>, } impl Statistics { @@ -41,9 +41,7 @@ impl Statistics { socket: repeat_with(Default::default) .take(config.socket_workers) .collect(), - swarm: repeat_with(Default::default) - .take(config.swarm_workers) - .collect(), + swarm: Default::default(), } } } diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index 6e27fc49..65a490e8 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -71,6 +71,29 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Socket(i), handle)); } + { + let state = state.clone(); + let config = config.clone(); + let statistics = statistics.swarm.clone(); + let statistics_sender = statistics_sender.clone(); + + let handle = Builder::new().name("cleaning".into()).spawn(move || loop { + sleep(Duration::from_secs( + config.cleaning.torrent_cleaning_interval, + )); + + state.torrent_maps.clean_and_update_statistics( + &config, + &statistics, + &statistics_sender, + &state.access_list, + state.server_start_instant, + ); + })?; + + join_handles.push((WorkerType::Cleaning, handle)); + } + if config.statistics.active() { let state = state.clone(); let config = config.clone(); @@ -142,14 +165,6 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Signals, handle)); } - #[cfg(feature = "cpu-pinning")] - pin_current_if_configured_to( - &config.cpu_pinning, - config.socket_workers, - config.swarm_workers, - WorkerIndex::Util, - ); - loop { for (i, (_, handle)) in join_handles.iter().enumerate() { if handle.is_finished() { diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index 159e145e..f6cb3367 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -6,6 +6,7 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use aquatic_common::SecondsSinceServerStart; +use aquatic_common::ServerStartInstant; use aquatic_common::{ access_list::{create_access_list_cache, AccessListArcSwap, AccessListCache, AccessListMode}, ValidUntil, @@ -84,16 +85,16 @@ impl TorrentMaps { } /// Remove forbidden or inactive torrents, reclaim space and update statistics pub fn clean_and_update_statistics( - &mut self, + &self, config: &Config, - state: &State, statistics: &CachePaddedArc>, statistics_sender: &Sender, access_list: &Arc, + server_start_instant: ServerStartInstant, ) { let mut cache = create_access_list_cache(access_list); let mode = config.access_list.mode; - let now = state.server_start_instant.seconds_elapsed(); + let now = server_start_instant.seconds_elapsed(); let ipv4 = self.ipv4 @@ -196,7 +197,7 @@ impl TorrentMapShards { } fn clean_and_get_statistics( - &mut self, + &self, config: &Config, statistics_sender: &Sender, access_list_cache: &mut AccessListCache, diff --git a/crates/udp/src/workers/statistics/collector.rs b/crates/udp/src/workers/statistics/collector.rs index 5297853e..1680695b 100644 --- a/crates/udp/src/workers/statistics/collector.rs +++ b/crates/udp/src/workers/statistics/collector.rs @@ -60,8 +60,6 @@ impl StatisticsCollector { let mut responses_error: usize = 0; let mut bytes_received: usize = 0; let mut bytes_sent: usize = 0; - let mut num_torrents: usize = 0; - let mut num_peers: usize = 0; #[cfg(feature = "prometheus")] let ip_version_prometheus_str = self.ip_version.prometheus_str(); @@ -186,44 +184,37 @@ impl StatisticsCollector { } } - for (i, statistics) in self - .statistics - .swarm - .iter() - .map(|s| s.by_ip_version(self.ip_version)) - .enumerate() - { - { - let n = statistics.torrents.load(Ordering::Relaxed); + let swarm_statistics = &self.statistics.swarm.by_ip_version(self.ip_version); - num_torrents += n; + let num_torrents = { + let num_torrents = swarm_statistics.torrents.load(Ordering::Relaxed); - #[cfg(feature = "prometheus")] - if config.statistics.run_prometheus_endpoint { - ::metrics::gauge!( - "aquatic_torrents", - "ip_version" => ip_version_prometheus_str, - "worker_index" => i.to_string(), - ) - .set(n as f64); - } + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::gauge!( + "aquatic_torrents", + "ip_version" => ip_version_prometheus_str, + ) + .set(num_torrents as f64); } - { - let n = statistics.peers.load(Ordering::Relaxed); - num_peers += n; + num_torrents + }; - #[cfg(feature = "prometheus")] - if config.statistics.run_prometheus_endpoint { - ::metrics::gauge!( - "aquatic_peers", - "ip_version" => ip_version_prometheus_str, - "worker_index" => i.to_string(), - ) - .set(n as f64); - } + let num_peers = { + let num_peers = swarm_statistics.peers.load(Ordering::Relaxed); + + #[cfg(feature = "prometheus")] + if config.statistics.run_prometheus_endpoint { + ::metrics::gauge!( + "aquatic_peers", + "ip_version" => ip_version_prometheus_str, + ) + .set(num_peers as f64); } - } + + num_peers + }; let elapsed = { let now = Instant::now(); From 71a3cb9a5ab0d135cf46af89ec9a24d034c95438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 15:54:07 +0100 Subject: [PATCH 07/26] udp: remove socket_worker config, adjust other code, fix statistics --- crates/udp/src/config.rs | 31 +--------------- crates/udp/src/lib.rs | 36 ++++--------------- .../udp/src/workers/statistics/collector.rs | 11 ++---- crates/udp/src/workers/statistics/mod.rs | 4 +-- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/crates/udp/src/config.rs b/crates/udp/src/config.rs index df83279e..8dfcfcef 100644 --- a/crates/udp/src/config.rs +++ b/crates/udp/src/config.rs @@ -11,36 +11,14 @@ use aquatic_toml_config::TomlConfig; #[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] #[serde(default, deny_unknown_fields)] pub struct Config { - /// Number of socket worker. One per physical core is recommended. - /// - /// Socket workers receive requests from clients and parse them. - /// Responses to connect requests are sent back immediately. Announce and - /// scrape requests are passed on to swarm workers, which generate - /// responses and send them back to the socket worker, which sends them - /// to the client. + /// Number of socket worker. One per virtual CPU is recommended pub socket_workers: usize, - /// Number of swarm workers. One is enough in almost all cases - /// - /// Swarm workers receive parsed announce and scrape requests from socket - /// workers, generate responses and send them back to the socket workers. - pub swarm_workers: usize, pub log_level: LogLevel, - /// Maximum number of items in each channel passing requests/responses - /// between workers. A value of zero is no longer allowed. - pub worker_channel_size: usize, - /// How long to block waiting for requests in swarm workers. - /// - /// Higher values means that with zero traffic, the worker will not - /// unnecessarily cause the CPU to wake up as often. However, high values - /// (something like larger than 1000) combined with very low traffic can - /// cause delays in torrent cleaning. - pub request_channel_recv_timeout_ms: u64, pub network: NetworkConfig, pub protocol: ProtocolConfig, pub statistics: StatisticsConfig, pub cleaning: CleaningConfig, pub privileges: PrivilegeConfig, - /// Access list configuration /// /// The file is read on start and when the program receives `SIGUSR1`. If @@ -48,26 +26,19 @@ pub struct Config { /// emitting of an error-level log message, while successful updates of the /// access list result in emitting of an info-level log message. pub access_list: AccessListConfig, - #[cfg(feature = "cpu-pinning")] - pub cpu_pinning: aquatic_common::cpu_pinning::asc::CpuPinningConfigAsc, } impl Default for Config { fn default() -> Self { Self { socket_workers: 1, - swarm_workers: 1, log_level: LogLevel::Error, - worker_channel_size: 1_024, - request_channel_recv_timeout_ms: 100, network: NetworkConfig::default(), protocol: ProtocolConfig::default(), statistics: StatisticsConfig::default(), cleaning: CleaningConfig::default(), privileges: PrivilegeConfig::default(), access_list: AccessListConfig::default(), - #[cfg(feature = "cpu-pinning")] - cpu_pinning: Default::default(), } } } diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index 65a490e8..c0a7d717 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -13,8 +13,6 @@ use signal_hook::consts::SIGUSR1; use signal_hook::iterator::Signals; use aquatic_common::access_list::update_access_list; -#[cfg(feature = "cpu-pinning")] -use aquatic_common::cpu_pinning::{pin_current_if_configured_to, WorkerIndex}; use aquatic_common::privileges::PrivilegeDropper; use common::{State, Statistics}; @@ -31,13 +29,13 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let statistics = Statistics::new(&config); let connection_validator = ConnectionValidator::new(&config)?; let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); - - let mut join_handles = Vec::new(); + let (statistics_sender, statistics_receiver) = unbounded(); update_access_list(&config.access_list, &state.access_list)?; - let (statistics_sender, statistics_receiver) = unbounded(); + let mut join_handles = Vec::new(); + // Spawn socket worker threads for i in 0..config.socket_workers { let state = state.clone(); let config = config.clone(); @@ -49,14 +47,6 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let handle = Builder::new() .name(format!("socket-{:02}", i + 1)) .spawn(move || { - #[cfg(feature = "cpu-pinning")] - pin_current_if_configured_to( - &config.cpu_pinning, - config.socket_workers, - config.swarm_workers, - WorkerIndex::SocketWorker(i), - ); - workers::socket::run_socket_worker( config, state, @@ -71,6 +61,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Socket(i), handle)); } + // Spawn cleaning thread { let state = state.clone(); let config = config.clone(); @@ -94,6 +85,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Cleaning, handle)); } + // Spawn statistics thread if config.statistics.active() { let state = state.clone(); let config = config.clone(); @@ -101,14 +93,6 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let handle = Builder::new() .name("statistics".into()) .spawn(move || { - #[cfg(feature = "cpu-pinning")] - pin_current_if_configured_to( - &config.cpu_pinning, - config.socket_workers, - config.swarm_workers, - WorkerIndex::Util, - ); - workers::statistics::run_statistics_worker( config, state, @@ -121,6 +105,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Statistics, handle)); } + // Spawn prometheus endpoint thread #[cfg(feature = "prometheus")] if config.statistics.active() && config.statistics.run_prometheus_endpoint { let handle = aquatic_common::spawn_prometheus_endpoint( @@ -141,14 +126,6 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { let handle: JoinHandle> = Builder::new() .name("signals".into()) .spawn(move || { - #[cfg(feature = "cpu-pinning")] - pin_current_if_configured_to( - &config.cpu_pinning, - config.socket_workers, - config.swarm_workers, - WorkerIndex::Util, - ); - for signal in &mut signals { match signal { SIGUSR1 => { @@ -165,6 +142,7 @@ pub fn run(config: Config) -> ::anyhow::Result<()> { join_handles.push((WorkerType::Signals, handle)); } + // Quit application if any worker returns or panics loop { for (i, (_, handle)) in join_handles.iter().enumerate() { if handle.is_finished() { diff --git a/crates/udp/src/workers/statistics/collector.rs b/crates/udp/src/workers/statistics/collector.rs index 1680695b..93fe11d8 100644 --- a/crates/udp/src/workers/statistics/collector.rs +++ b/crates/udp/src/workers/statistics/collector.rs @@ -25,7 +25,6 @@ pub struct StatisticsCollector { statistics: Statistics, ip_version: IpVersion, last_update: Instant, - pending_histograms: Vec>, last_complete_histogram: PeerHistogramStatistics, } @@ -34,19 +33,13 @@ impl StatisticsCollector { Self { statistics, last_update: Instant::now(), - pending_histograms: Vec::new(), last_complete_histogram: Default::default(), ip_version, } } - pub fn add_histogram(&mut self, config: &Config, histogram: Histogram) { - self.pending_histograms.push(histogram); - - if self.pending_histograms.len() == config.swarm_workers { - self.last_complete_histogram = - PeerHistogramStatistics::new(self.pending_histograms.drain(..).sum()); - } + pub fn add_histogram(&mut self, histogram: Histogram) { + self.last_complete_histogram = PeerHistogramStatistics::new(histogram); } pub fn collect_from_shared( diff --git a/crates/udp/src/workers/statistics/mod.rs b/crates/udp/src/workers/statistics/mod.rs index beafa2d1..4814bb79 100644 --- a/crates/udp/src/workers/statistics/mod.rs +++ b/crates/udp/src/workers/statistics/mod.rs @@ -81,8 +81,8 @@ pub fn run_statistics_worker( for message in statistics_receiver.try_iter() { match message { - StatisticsMessage::Ipv4PeerHistogram(h) => ipv4_collector.add_histogram(&config, h), - StatisticsMessage::Ipv6PeerHistogram(h) => ipv6_collector.add_histogram(&config, h), + StatisticsMessage::Ipv4PeerHistogram(h) => ipv4_collector.add_histogram(h), + StatisticsMessage::Ipv6PeerHistogram(h) => ipv6_collector.add_histogram(h), StatisticsMessage::PeerAdded(peer_id) => { if process_peer_client_data { peers From 6d784c25e99e5f647c7aea273493abbf279428d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 15:56:34 +0100 Subject: [PATCH 08/26] udp: remove pending scrape config stuff, adjust io uring code --- crates/udp/src/config.rs | 10 ---------- crates/udp/src/workers/socket/uring/mod.rs | 23 +--------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/crates/udp/src/config.rs b/crates/udp/src/config.rs index 8dfcfcef..7532cf82 100644 --- a/crates/udp/src/config.rs +++ b/crates/udp/src/config.rs @@ -210,28 +210,18 @@ impl Default for StatisticsConfig { pub struct CleaningConfig { /// Clean torrents this often (seconds) pub torrent_cleaning_interval: u64, - /// Clean pending scrape responses this often (seconds) - /// - /// In regular operation, there should be no pending scrape responses - /// lingering for long enough to have to be cleaned up this way. - pub pending_scrape_cleaning_interval: u64, /// Allow clients to use a connection token for this long (seconds) pub max_connection_age: u32, /// Remove peers who have not announced for this long (seconds) pub max_peer_age: u32, - /// Remove pending scrape responses that have not been returned from swarm - /// workers for this long (seconds) - pub max_pending_scrape_age: u32, } impl Default for CleaningConfig { fn default() -> Self { Self { torrent_cleaning_interval: 60 * 2, - pending_scrape_cleaning_interval: 60 * 10, max_connection_age: 60 * 2, max_peer_age: 60 * 20, - max_pending_scrape_age: 60, } } } diff --git a/crates/udp/src/workers/socket/uring/mod.rs b/crates/udp/src/workers/socket/uring/mod.rs index 7ad26c71..fc3ab997 100644 --- a/crates/udp/src/workers/socket/uring/mod.rs +++ b/crates/udp/src/workers/socket/uring/mod.rs @@ -50,7 +50,6 @@ const RESPONSE_BUF_LEN: usize = 2048; const USER_DATA_RECV: u64 = u64::MAX; const USER_DATA_PULSE_TIMEOUT: u64 = u64::MAX - 1; -const USER_DATA_CLEANING_TIMEOUT: u64 = u64::MAX - 2; const SOCKET_IDENTIFIER: Fixed = Fixed(0); @@ -90,7 +89,6 @@ pub struct SocketWorker { resubmittable_sqe_buf: Vec, recv_sqe: io_uring::squeue::Entry, pulse_timeout_sqe: io_uring::squeue::Entry, - cleaning_timeout_sqe: io_uring::squeue::Entry, peer_valid_until: ValidUntil, rng: SmallRng, } @@ -147,21 +145,7 @@ impl SocketWorker { .user_data(USER_DATA_PULSE_TIMEOUT) }; - let cleaning_timeout_sqe = { - let timespec_ptr = Box::into_raw(Box::new( - Timespec::new().sec(config.cleaning.pending_scrape_cleaning_interval), - )) as *const _; - - Timeout::new(timespec_ptr) - .build() - .user_data(USER_DATA_CLEANING_TIMEOUT) - }; - - let resubmittable_sqe_buf = vec![ - recv_sqe.clone(), - pulse_timeout_sqe.clone(), - cleaning_timeout_sqe.clone(), - ]; + let resubmittable_sqe_buf = vec![recv_sqe.clone(), pulse_timeout_sqe.clone()]; let peer_valid_until = ValidUntil::new( shared_state.server_start_instant, @@ -181,7 +165,6 @@ impl SocketWorker { buf_ring, recv_sqe, pulse_timeout_sqe, - cleaning_timeout_sqe, resubmittable_sqe_buf, socket, peer_valid_until, @@ -263,10 +246,6 @@ impl SocketWorker { self.resubmittable_sqe_buf .push(self.pulse_timeout_sqe.clone()); } - USER_DATA_CLEANING_TIMEOUT => { - self.resubmittable_sqe_buf - .push(self.cleaning_timeout_sqe.clone()); - } send_buffer_index => { let result = cqe.result(); From 1248c945a9b013b2e2e089fe4c6a40b3c8c3bc47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 15:57:58 +0100 Subject: [PATCH 09/26] Update TODO --- TODO.md | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO.md b/TODO.md index 9024c23b..c724d879 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,6 @@ ## High priority * udp - * fix config * consider ways of avoiding response peer allocations * make ConnectionValidator faster by avoiding calling time functions so often From 358c8951c03b4d140e92d3da6a48e9b19d26c240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 18:06:42 +0100 Subject: [PATCH 10/26] udp: improve udp uring code --- TODO.md | 1 - crates/udp/src/workers/socket/uring/mod.rs | 99 +++++++++++++--------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/TODO.md b/TODO.md index c724d879..875687eb 100644 --- a/TODO.md +++ b/TODO.md @@ -3,7 +3,6 @@ ## High priority * udp - * consider ways of avoiding response peer allocations * make ConnectionValidator faster by avoiding calling time functions so often ## Medium priority diff --git a/crates/udp/src/workers/socket/uring/mod.rs b/crates/udp/src/workers/socket/uring/mod.rs index fc3ab997..299209c8 100644 --- a/crates/udp/src/workers/socket/uring/mod.rs +++ b/crates/udp/src/workers/socket/uring/mod.rs @@ -85,7 +85,7 @@ pub struct SocketWorker { buf_ring: BufRing, send_buffers: SendBuffers, recv_helper: RecvHelper, - local_responses: VecDeque<(Response, CanonicalSocketAddr)>, + local_responses: VecDeque<(CanonicalSocketAddr, Response)>, resubmittable_sqe_buf: Vec, recv_sqe: io_uring::squeue::Entry, pulse_timeout_sqe: io_uring::squeue::Entry, @@ -192,7 +192,7 @@ impl SocketWorker { // Enqueue local responses for _ in 0..sq_space { - if let Some((response, addr)) = self.local_responses.pop_front() { + if let Some((addr, response)) = self.local_responses.pop_front() { match self.send_buffers.prepare_entry(response, addr) { Ok(entry) => { unsafe { ring.submission().push(&entry).unwrap() }; @@ -200,7 +200,7 @@ impl SocketWorker { num_send_added += 1; } Err(send_buffers::Error::NoBuffers(response)) => { - self.local_responses.push_front((response, addr)); + self.local_responses.push_front((addr, response)); break; } @@ -231,7 +231,9 @@ impl SocketWorker { fn handle_cqe(&mut self, cqe: io_uring::cqueue::Entry) { match cqe.user_data() { USER_DATA_RECV => { - self.handle_recv_cqe(&cqe); + if let Some((addr, response)) = self.handle_recv_cqe(&cqe) { + self.local_responses.push_back((addr, response)); + } if !io_uring::cqueue::more(cqe.flags()) { self.resubmittable_sqe_buf.push(self.recv_sqe.clone()); @@ -290,12 +292,15 @@ impl SocketWorker { } } - fn handle_recv_cqe(&mut self, cqe: &io_uring::cqueue::Entry) { + fn handle_recv_cqe( + &mut self, + cqe: &io_uring::cqueue::Entry, + ) -> Option<(CanonicalSocketAddr, Response)> { let result = cqe.result(); if result < 0 { if -result == libc::ENOBUFS { - ::log::info!("recv failed due to lack of buffers. If increasing ring size doesn't help, get faster hardware"); + ::log::info!("recv failed due to lack of buffers, try increasing ring size"); } else { ::log::warn!( "recv failed: {:#}", @@ -303,7 +308,7 @@ impl SocketWorker { ); } - return; + return None; } let buffer = unsafe { @@ -312,23 +317,48 @@ impl SocketWorker { Ok(None) => { ::log::error!("Couldn't get recv buffer"); - return; + return None; } Err(err) => { ::log::error!("Couldn't get recv buffer: {:#}", err); - return; + return None; } } }; - let addr = match self.recv_helper.parse(buffer.as_slice()) { + match self.recv_helper.parse(buffer.as_slice()) { Ok((request, addr)) => { - self.handle_request(request, addr); + if self.config.statistics.active() { + let (statistics, extra_bytes) = if addr.is_ipv4() { + (&self.statistics.ipv4, EXTRA_PACKET_SIZE_IPV4) + } else { + (&self.statistics.ipv6, EXTRA_PACKET_SIZE_IPV6) + }; + + statistics + .bytes_received + .fetch_add(buffer.len() + extra_bytes, Ordering::Relaxed); + statistics.requests.fetch_add(1, Ordering::Relaxed); + } - addr + return self.handle_request(request, addr); } Err(self::recv_helper::Error::RequestParseError(err, addr)) => { + if self.config.statistics.active() { + if addr.is_ipv4() { + self.statistics + .ipv4 + .bytes_received + .fetch_add(buffer.len() + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); + } else { + self.statistics + .ipv6 + .bytes_received + .fetch_add(buffer.len() + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); + } + } + match err { RequestParseError::Sendable { connection_id, @@ -343,60 +373,43 @@ impl SocketWorker { message: err.into(), }; - self.local_responses.push_back((response.into(), addr)); + return Some((addr, Response::Error(response))); } } RequestParseError::Unsendable { err } => { ::log::debug!("Couldn't parse request from {:?}: {}", addr, err); } } - - addr } Err(self::recv_helper::Error::InvalidSocketAddress) => { ::log::debug!("Ignored request claiming to be from port 0"); - - return; } Err(self::recv_helper::Error::RecvMsgParseError) => { ::log::error!("RecvMsgOut::parse failed"); - - return; } Err(self::recv_helper::Error::RecvMsgTruncated) => { ::log::warn!("RecvMsgOut::parse failed: sockaddr or payload truncated"); - - return; } - }; - - if self.config.statistics.active() { - let (statistics, extra_bytes) = if addr.is_ipv4() { - (&self.statistics.ipv4, EXTRA_PACKET_SIZE_IPV4) - } else { - (&self.statistics.ipv6, EXTRA_PACKET_SIZE_IPV6) - }; - - statistics - .bytes_received - .fetch_add(buffer.len() + extra_bytes, Ordering::Relaxed); - statistics.requests.fetch_add(1, Ordering::Relaxed); } + + None } - fn handle_request(&mut self, request: Request, src: CanonicalSocketAddr) { + fn handle_request( + &mut self, + request: Request, + src: CanonicalSocketAddr, + ) -> Option<(CanonicalSocketAddr, Response)> { let access_list_mode = self.config.access_list.mode; match request { Request::Connect(request) => { - let connection_id = self.validator.create_connection_id(src); - let response = Response::Connect(ConnectResponse { - connection_id, + connection_id: self.validator.create_connection_id(src), transaction_id: request.transaction_id, }); - self.local_responses.push_back((response, src)); + return Some((src, response)); } Request::Announce(request) => { if self @@ -417,14 +430,14 @@ impl SocketWorker { self.peer_valid_until, ); - self.local_responses.push_back((response, src)); + return Some((src, response)); } else { let response = Response::Error(ErrorResponse { transaction_id: request.transaction_id, message: "Info hash not allowed".into(), }); - self.local_responses.push_back((response, src)) + return Some((src, response)); } } } @@ -436,10 +449,12 @@ impl SocketWorker { let response = Response::Scrape(self.shared_state.torrent_maps.scrape(request, src)); - self.local_responses.push_back((response, src)); + return Some((src, response)); } } } + + None } } From 2c7bcf71ad232d0cb1211a8f0381942eb15cec85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 18:51:13 +0100 Subject: [PATCH 11/26] bencher: change to account for new aquatic_udp implementation --- crates/bencher/src/protocols/udp.rs | 43 ++++++++++++----------------- crates/udp/src/common.rs | 6 ++-- crates/udp/src/lib.rs | 2 +- crates/udp/src/swarm.rs | 18 +++++++----- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/crates/bencher/src/protocols/udp.rs b/crates/bencher/src/protocols/udp.rs index e60d730c..7c743b15 100644 --- a/crates/bencher/src/protocols/udp.rs +++ b/crates/bencher/src/protocols/udp.rs @@ -58,6 +58,12 @@ impl UdpCommand { indexmap::indexmap! { 1 => SetConfig { implementations: indexmap! { + UdpTracker::Aquatic => vec![ + AquaticUdpRunner::with_mio(1, Priority::High), + ], + UdpTracker::AquaticIoUring => vec![ + AquaticUdpRunner::with_io_uring(1, Priority::High), + ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(0, Priority::Medium), // Handle requests within event loop OpenTrackerUdpRunner::new(1, Priority::High), @@ -74,12 +80,10 @@ impl UdpCommand { 2 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(1, 1, Priority::Medium), - AquaticUdpRunner::with_mio(2, 1, Priority::High), + AquaticUdpRunner::with_mio(2, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(1, 1, Priority::Medium), - AquaticUdpRunner::with_io_uring(2, 1, Priority::High), + AquaticUdpRunner::with_io_uring(2, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(2, Priority::High), @@ -97,12 +101,10 @@ impl UdpCommand { 4 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(3, 1, Priority::High), - AquaticUdpRunner::with_mio(4, 1, Priority::Medium), + AquaticUdpRunner::with_mio(4, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(3, 1, Priority::High), - AquaticUdpRunner::with_io_uring(4, 1, Priority::Medium), + AquaticUdpRunner::with_io_uring(4, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(4, Priority::High), @@ -119,10 +121,10 @@ impl UdpCommand { 6 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(5, 1, Priority::High), + AquaticUdpRunner::with_mio(6, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(5, 1, Priority::High), + AquaticUdpRunner::with_io_uring(6, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(6, Priority::High), @@ -139,10 +141,10 @@ impl UdpCommand { 8 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(7, 1, Priority::High), + AquaticUdpRunner::with_mio(8, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(7, 1, Priority::High), + AquaticUdpRunner::with_io_uring(8, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(8, Priority::High), @@ -159,12 +161,10 @@ impl UdpCommand { 12 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(10, 2, Priority::High), - AquaticUdpRunner::with_mio(9, 3, Priority::Medium), + AquaticUdpRunner::with_mio(12, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(10, 2, Priority::High), - AquaticUdpRunner::with_io_uring(9, 3, Priority::Medium), + AquaticUdpRunner::with_io_uring(12, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(12, Priority::High), @@ -181,10 +181,10 @@ impl UdpCommand { 16 => SetConfig { implementations: indexmap! { UdpTracker::Aquatic => vec![ - AquaticUdpRunner::with_mio(13, 3, Priority::High), + AquaticUdpRunner::with_mio(16, Priority::High), ], UdpTracker::AquaticIoUring => vec![ - AquaticUdpRunner::with_io_uring(13, 3, Priority::High), + AquaticUdpRunner::with_io_uring(16, Priority::High), ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(16, Priority::High), @@ -211,7 +211,6 @@ impl UdpCommand { #[derive(Debug, Clone)] struct AquaticUdpRunner { socket_workers: usize, - swarm_workers: usize, use_io_uring: bool, priority: Priority, } @@ -219,24 +218,20 @@ struct AquaticUdpRunner { impl AquaticUdpRunner { fn with_mio( socket_workers: usize, - swarm_workers: usize, priority: Priority, ) -> Rc> { Rc::new(Self { socket_workers, - swarm_workers, use_io_uring: false, priority, }) } fn with_io_uring( socket_workers: usize, - swarm_workers: usize, priority: Priority, ) -> Rc> { Rc::new(Self { socket_workers, - swarm_workers, use_io_uring: true, priority, }) @@ -256,7 +251,6 @@ impl ProcessRunner for AquaticUdpRunner { let mut c = aquatic_udp::config::Config::default(); c.socket_workers = self.socket_workers; - c.swarm_workers = self.swarm_workers; c.network.address = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 3000)); c.network.use_io_uring = self.use_io_uring; c.protocol.max_response_peers = 30; @@ -283,7 +277,6 @@ impl ProcessRunner for AquaticUdpRunner { fn keys(&self) -> IndexMap { indexmap! { "socket workers".to_string() => self.socket_workers.to_string(), - "swarm workers".to_string() => self.swarm_workers.to_string(), } } } diff --git a/crates/udp/src/common.rs b/crates/udp/src/common.rs index c42aa358..dce58cb7 100644 --- a/crates/udp/src/common.rs +++ b/crates/udp/src/common.rs @@ -94,11 +94,11 @@ pub struct State { pub server_start_instant: ServerStartInstant, } -impl State { - pub fn new(config: &Config) -> Self { +impl Default for State { + fn default() -> Self { Self { access_list: Arc::new(AccessListArcSwap::default()), - torrent_maps: TorrentMaps::new(config), + torrent_maps: TorrentMaps::default(), server_start_instant: ServerStartInstant::new(), } } diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index c0a7d717..e91027ee 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -25,7 +25,7 @@ pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn run(config: Config) -> ::anyhow::Result<()> { let mut signals = Signals::new([SIGUSR1])?; - let state = State::new(&config); + let state = State::default(); let statistics = Statistics::new(&config); let connection_validator = ConnectionValidator::new(&config)?; let priv_dropper = PrivilegeDropper::new(config.privileges.clone(), config.socket_workers); diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index f6cb3367..d88b9d33 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -16,6 +16,7 @@ use aquatic_common::{CanonicalSocketAddr, IndexMap}; use aquatic_udp_protocol::*; use arrayvec::ArrayVec; use crossbeam_channel::Sender; +use hashbrown::HashMap; use hdrhistogram::Histogram; use parking_lot::RwLockUpgradableReadGuard; use rand::prelude::SmallRng; @@ -29,24 +30,24 @@ const SMALL_PEER_MAP_CAPACITY: usize = 2; use aquatic_udp_protocol::InfoHash; use parking_lot::RwLock; -type TorrentMapShard = IndexMap>>; - #[derive(Clone)] pub struct TorrentMaps { ipv4: TorrentMapShards, ipv6: TorrentMapShards, } -impl TorrentMaps { - pub fn new(config: &Config) -> Self { - let num_shards = 16usize; +impl Default for TorrentMaps { + fn default() -> Self { + const NUM_SHARDS: usize = 16; Self { - ipv4: TorrentMapShards::new(num_shards), - ipv6: TorrentMapShards::new(num_shards), + ipv4: TorrentMapShards::new(NUM_SHARDS), + ipv6: TorrentMapShards::new(NUM_SHARDS), } } +} +impl TorrentMaps { pub fn announce( &self, config: &Config, @@ -294,6 +295,9 @@ impl TorrentMapShards { } } +/// Use HashMap instead of IndexMap for better lookup performance +type TorrentMapShard = HashMap>>; + pub struct TorrentData { peer_map: RwLock>, pending_removal: AtomicBool, From 21a530189e593072ff96d0557386cb99a80f8928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 20:00:17 +0100 Subject: [PATCH 12/26] bencher: fix udp sets --- crates/bencher/src/protocols/udp.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bencher/src/protocols/udp.rs b/crates/bencher/src/protocols/udp.rs index 7c743b15..700dea6b 100644 --- a/crates/bencher/src/protocols/udp.rs +++ b/crates/bencher/src/protocols/udp.rs @@ -87,7 +87,6 @@ impl UdpCommand { ], UdpTracker::OpenTracker => vec![ OpenTrackerUdpRunner::new(2, Priority::High), - OpenTrackerUdpRunner::new(4, Priority::Medium), ], UdpTracker::Chihaya => vec![ ChihayaUdpRunner::new(), From a7ad3266d8db8d9efcf22b3f1822ba455706fdd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:21:43 +0100 Subject: [PATCH 13/26] Update UDP benchmarks --- README.md | 4 +- crates/udp/README.md | 4 +- documents/aquatic-udp-load-test-2024-02-10.md | 106 ++++++++++++++++++ .../aquatic-udp-load-test-2024-02-10.png | Bin 0 -> 59692 bytes 4 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 documents/aquatic-udp-load-test-2024-02-10.md create mode 100644 documents/aquatic-udp-load-test-2024-02-10.png diff --git a/README.md b/README.md index 9621f8c1..f26d5abb 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ Known users: ## Performance of the UDP implementation -![UDP BitTorrent tracker throughput comparison](./documents/aquatic-udp-load-test-illustration-2023-01-11.png) +![UDP BitTorrent tracker throughput](./documents/aquatic-udp-load-test-2024-02-10.png) -More benchmark details are available [here](./documents/aquatic-udp-load-test-2023-01-11.pdf). +More benchmark details are available [here](./documents/aquatic-udp-load-test-2024-02-10.md). ## Usage diff --git a/crates/udp/README.md b/crates/udp/README.md index 60a5c2d1..6e371480 100644 --- a/crates/udp/README.md +++ b/crates/udp/README.md @@ -21,9 +21,9 @@ This is the most mature implementation in the aquatic family. I consider it full ## Performance -![UDP BitTorrent tracker throughput comparison](../../documents/aquatic-udp-load-test-illustration-2023-01-11.png) +![UDP BitTorrent tracker throughput](../../documents/aquatic-udp-load-test-2024-02-10.png) -More benchmark details are available [here](../../documents/aquatic-udp-load-test-2023-01-11.pdf). +More benchmark details are available [here](./documents/aquatic-udp-load-test-2024-02-10.md). ## Usage diff --git a/documents/aquatic-udp-load-test-2024-02-10.md b/documents/aquatic-udp-load-test-2024-02-10.md new file mode 100644 index 00000000..7e8544d9 --- /dev/null +++ b/documents/aquatic-udp-load-test-2024-02-10.md @@ -0,0 +1,106 @@ +2024-02-10 Joakim Frostegård + +# UDP BitTorrent tracker throughput comparison + +This is a performance comparison of several UDP BitTorrent tracker implementations. + +Benchmarks were run using [aquatic_bencher](https://github.com/greatest-ape/aquatic), with `--cpu-mode subsequent-one-per-pair`. + +## Software and hardware + +### Tracker implementations + +| Name | Commit | +|---------------|---------| +| [aquatic_udp] | 21a5301 | +| [opentracker] | 110868e | +| [chihaya] | 2f79440 | + +[aquatic_udp]: https://github.com/greatest-ape/aquatic +[opentracker]: http://erdgeist.org/arts/software/opentracker/ +[chihaya]: https://github.com/chihaya/chihaya + +### OS and compilers + +| Name | Version | +|--------|---------| +| Debian | 12.4 | +| Linux | 6.5.10 | +| rustc | 1.76.0 | +| GCC | 12.2.0 | +| go | 1.19.8 | + +### Hardware + +Hetzner CCX63: 48 dedicated vCPUs (AMD Milan Epyc 7003) + +## Results + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ UDP BitTorrent tracker troughput +

Average responses per second, best result.

+
CPU coresaquatic_udp (mio)aquatic_udp (io_uring)opentrackerchihaya
1186,939226,065190,54055,989
2371,478444,353379,623111,226
4734,709876,642748,401136,983
61,034,8041,267,006901,600131,827
81,296,6931,521,1131,170,928131,779
121,871,3531,837,2231,675,059130,942
162,037,7132,258,3211,645,828127,256
+ +![UDP BitTorrent tracker throughput](./aquatic-udp-load-test-2024-02-10.png) diff --git a/documents/aquatic-udp-load-test-2024-02-10.png b/documents/aquatic-udp-load-test-2024-02-10.png new file mode 100644 index 0000000000000000000000000000000000000000..a91b862b0f1074c8511264a18d3325e51ba15f03 GIT binary patch literal 59692 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 CY;*Yl literal 0 HcmV?d00001 From 6384e3401b9be2e6b5fffb8463756887d9cb9829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:24:14 +0100 Subject: [PATCH 14/26] Move architectural overview image to ws and http crate READMEs --- README.md | 4 ---- crates/http/README.md | 4 ++++ crates/ws/README.md | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f26d5abb..b1446aec 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,6 @@ More benchmark details are available [here](./documents/aquatic-udp-load-test-20 Please refer to the README pages for the respective implementations listed in the table above. -## Architectural overview - -![Architectural overview of aquatic](./documents/aquatic-architecture-2024.svg) - ## Copyright and license Copyright (c) Joakim Frostegård diff --git a/crates/http/README.md b/crates/http/README.md index 36f8c4e3..219792f9 100644 --- a/crates/http/README.md +++ b/crates/http/README.md @@ -109,6 +109,10 @@ Implements: `aquatic_http` has not been tested as much as `aquatic_udp`, but likely works fine in production. +## Architectural overview + +![Architectural overview of aquatic](../../documents/aquatic-architecture-2024.svg) + ## Copyright and license Copyright (c) Joakim Frostegård diff --git a/crates/ws/README.md b/crates/ws/README.md index c21fadb7..ae5a34b9 100644 --- a/crates/ws/README.md +++ b/crates/ws/README.md @@ -105,6 +105,10 @@ clients. Notes: `aquatic_ws` has not been tested as much as `aquatic_udp`, but likely works fine in production. +## Architectural overview + +![Architectural overview of aquatic](../../documents/aquatic-architecture-2024.svg) + ## Copyright and license Copyright (c) Joakim Frostegård From 68e951cf799705502db8a9d6e1963b31c1624fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:29:12 +0100 Subject: [PATCH 15/26] Improve udp README and latest load test md --- crates/udp/README.md | 2 +- documents/aquatic-udp-load-test-2024-02-10.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/udp/README.md b/crates/udp/README.md index 6e371480..d8abbf93 100644 --- a/crates/udp/README.md +++ b/crates/udp/README.md @@ -23,7 +23,7 @@ This is the most mature implementation in the aquatic family. I consider it full ![UDP BitTorrent tracker throughput](../../documents/aquatic-udp-load-test-2024-02-10.png) -More benchmark details are available [here](./documents/aquatic-udp-load-test-2024-02-10.md). +More benchmark details are available [here](../../documents/aquatic-udp-load-test-2024-02-10.md). ## Usage diff --git a/documents/aquatic-udp-load-test-2024-02-10.md b/documents/aquatic-udp-load-test-2024-02-10.md index 7e8544d9..82d6e700 100644 --- a/documents/aquatic-udp-load-test-2024-02-10.md +++ b/documents/aquatic-udp-load-test-2024-02-10.md @@ -4,7 +4,7 @@ This is a performance comparison of several UDP BitTorrent tracker implementations. -Benchmarks were run using [aquatic_bencher](https://github.com/greatest-ape/aquatic), with `--cpu-mode subsequent-one-per-pair`. +Benchmarks were run using [aquatic_bencher](../crates/bencher), with `--cpu-mode subsequent-one-per-pair`. ## Software and hardware @@ -16,7 +16,7 @@ Benchmarks were run using [aquatic_bencher](https://github.com/greatest-ape/aqua | [opentracker] | 110868e | | [chihaya] | 2f79440 | -[aquatic_udp]: https://github.com/greatest-ape/aquatic +[aquatic_udp]: ../crates/udp [opentracker]: http://erdgeist.org/arts/software/opentracker/ [chihaya]: https://github.com/chihaya/chihaya @@ -36,6 +36,8 @@ Hetzner CCX63: 48 dedicated vCPUs (AMD Milan Epyc 7003) ## Results +![UDP BitTorrent tracker throughput](./aquatic-udp-load-test-2024-02-10.png) +
UDP BitTorrent tracker troughput @@ -102,5 +104,3 @@ Hetzner CCX63: 48 dedicated vCPUs (AMD Milan Epyc 7003)
- -![UDP BitTorrent tracker throughput](./aquatic-udp-load-test-2024-02-10.png) From ebf4ecbf6a9c8182a407044972c7b2ca8c5c37d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:45:34 +0100 Subject: [PATCH 16/26] udp: fix torrent count statistics --- crates/udp/src/swarm.rs | 25 ++++++++++++++++-------- crates/udp/src/workers/statistics/mod.rs | 5 ++++- crates/udp/templates/statistics.html | 8 ++++---- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index d88b9d33..6a5e3278 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -84,6 +84,7 @@ impl TorrentMaps { self.ipv6.scrape(request) } } + /// Remove forbidden or inactive torrents, reclaim space and update statistics pub fn clean_and_update_statistics( &self, @@ -105,15 +106,18 @@ impl TorrentMaps { .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); if config.statistics.active() { - statistics.ipv4.peers.store(ipv4.0, Ordering::Relaxed); - statistics.ipv6.peers.store(ipv6.0, Ordering::Relaxed); + statistics.ipv4.torrents.store(ipv4.0, Ordering::Relaxed); + statistics.ipv6.torrents.store(ipv6.0, Ordering::Relaxed); + + statistics.ipv4.peers.store(ipv4.1, Ordering::Relaxed); + statistics.ipv6.peers.store(ipv6.1, Ordering::Relaxed); - if let Some(message) = ipv4.1.map(StatisticsMessage::Ipv4PeerHistogram) { + if let Some(message) = ipv4.2.map(StatisticsMessage::Ipv4PeerHistogram) { if let Err(err) = statistics_sender.try_send(message) { ::log::error!("couldn't send statistics message: {:#}", err); } } - if let Some(message) = ipv6.1.map(StatisticsMessage::Ipv6PeerHistogram) { + if let Some(message) = ipv6.2.map(StatisticsMessage::Ipv6PeerHistogram) { if let Err(err) = statistics_sender.try_send(message) { ::log::error!("couldn't send statistics message: {:#}", err); } @@ -204,7 +208,8 @@ impl TorrentMapShards { access_list_cache: &mut AccessListCache, access_list_mode: AccessListMode, now: SecondsSinceServerStart, - ) -> (usize, Option>) { + ) -> (usize, usize, Option>) { + let mut total_num_torrents = 0; let mut total_num_peers = 0; let mut opt_histogram: Option> = if config.statistics.torrent_peer_histograms @@ -271,8 +276,10 @@ impl TorrentMapShards { return false; } - // Only remove if no peers have been added since previous - // cleaning step + // Check pending_removal flag set in previous cleaning step. This + // prevents us from removing TorrentData entries that were just + // added but do not yet contain any peers. Also double-check that + // no peers have been added since we last checked. if torrent_data .pending_removal .fetch_and(false, Ordering::Acquire) @@ -285,9 +292,11 @@ impl TorrentMapShards { }); torrent_map_shard.shrink_to_fit(); + + total_num_torrents += torrent_map_shard.len(); } - (total_num_peers, opt_histogram) + (total_num_torrents, total_num_peers, opt_histogram) } fn get_shard(&self, info_hash: &InfoHash) -> &RwLock> { diff --git a/crates/udp/src/workers/statistics/mod.rs b/crates/udp/src/workers/statistics/mod.rs index 4814bb79..9ae5b91d 100644 --- a/crates/udp/src/workers/statistics/mod.rs +++ b/crates/udp/src/workers/statistics/mod.rs @@ -249,7 +249,10 @@ fn print_to_stdout(config: &Config, statistics: &CollectedStatistics) { " error: {:>10}", statistics.responses_per_second_error ); - println!(" torrents: {:>10}", statistics.num_torrents); + println!( + " torrents: {:>10} (updated every {}s)", + statistics.num_torrents, config.cleaning.torrent_cleaning_interval + ); println!( " peers: {:>10} (updated every {}s)", statistics.num_peers, config.cleaning.torrent_cleaning_interval diff --git a/crates/udp/templates/statistics.html b/crates/udp/templates/statistics.html index 0fe8930e..01bd37f4 100644 --- a/crates/udp/templates/statistics.html +++ b/crates/udp/templates/statistics.html @@ -25,10 +25,10 @@

BitTorrent tracker statistics

IPv4

- + - + @@ -141,10 +141,10 @@

Peers per torrent

IPv6

* Peer count is updated every { peer_update_interval } seconds* Torrent/peer count is updated every { peer_update_interval } seconds
Number of torrents{ ipv4.num_torrents }{ ipv4.num_torrents } *
Number of peers
- + - + From b1908329e55942d4d182c5d3b5041f639c168397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:48:55 +0100 Subject: [PATCH 17/26] udp: improve config docs and key order --- crates/udp/src/config.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/udp/src/config.rs b/crates/udp/src/config.rs index 7532cf82..b25eae94 100644 --- a/crates/udp/src/config.rs +++ b/crates/udp/src/config.rs @@ -11,7 +11,7 @@ use aquatic_toml_config::TomlConfig; #[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] #[serde(default, deny_unknown_fields)] pub struct Config { - /// Number of socket worker. One per virtual CPU is recommended + /// Number of socket workers. One per virtual CPU is recommended pub socket_workers: usize, pub log_level: LogLevel, pub network: NetworkConfig, @@ -71,13 +71,6 @@ pub struct NetworkConfig { pub socket_recv_buffer_size: usize, /// Poll timeout in milliseconds (mio backend only) pub poll_timeout_ms: u64, - #[cfg(feature = "io-uring")] - pub use_io_uring: bool, - /// Number of ring entries (io_uring backend only) - /// - /// Will be rounded to next power of two if not already one. - #[cfg(feature = "io-uring")] - pub ring_size: u16, /// Store this many responses at most for retrying (once) on send failure /// (mio backend only) /// @@ -85,6 +78,13 @@ pub struct NetworkConfig { /// such as FreeBSD. Setting the value to zero disables resending /// functionality. pub resend_buffer_max_len: usize, + #[cfg(feature = "io-uring")] + pub use_io_uring: bool, + /// Number of ring entries (io_uring backend only) + /// + /// Will be rounded to next power of two if not already one. + #[cfg(feature = "io-uring")] + pub ring_size: u16, } impl NetworkConfig { @@ -103,11 +103,11 @@ impl Default for NetworkConfig { only_ipv6: false, socket_recv_buffer_size: 8_000_000, poll_timeout_ms: 50, + resend_buffer_max_len: 0, #[cfg(feature = "io-uring")] use_io_uring: true, #[cfg(feature = "io-uring")] ring_size: 128, - resend_buffer_max_len: 0, } } } From 7116fdd8621f722bd6d97afa5d501e9a566e275b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 21:55:39 +0100 Subject: [PATCH 18/26] udp: io_uring: improve docs --- crates/udp/src/workers/socket/uring/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/udp/src/workers/socket/uring/mod.rs b/crates/udp/src/workers/socket/uring/mod.rs index 299209c8..2f85e428 100644 --- a/crates/udp/src/workers/socket/uring/mod.rs +++ b/crates/udp/src/workers/socket/uring/mod.rs @@ -134,9 +134,7 @@ impl SocketWorker { let recv_sqe = recv_helper.create_entry(buf_ring.bgid()); - // This timeout enables regular updates of pending_scrape_valid_until - // and wakes the main loop to send any pending responses in the case - // of no incoming requests + // This timeout enables regular updates of peer_valid_until let pulse_timeout_sqe = { let timespec_ptr = Box::into_raw(Box::new(Timespec::new().sec(1))) as *const _; From 19533b3f8e4332ebf60c55c38e01272a959c9f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 22:13:01 +0100 Subject: [PATCH 19/26] udp: mio: make send_response a method --- crates/udp/src/workers/socket/mio.rs | 212 +++++++++++---------------- 1 file changed, 82 insertions(+), 130 deletions(-) diff --git a/crates/udp/src/workers/socket/mio.rs b/crates/udp/src/workers/socket/mio.rs index 9c720395..fb24bef9 100644 --- a/crates/udp/src/workers/socket/mio.rs +++ b/crates/udp/src/workers/socket/mio.rs @@ -30,7 +30,6 @@ pub struct SocketWorker { access_list_cache: AccessListCache, validator: ConnectionValidator, socket: UdpSocket, - opt_resend_buffer: Option>, buffer: [u8; BUFFER_SIZE], rng: SmallRng, } @@ -46,7 +45,6 @@ impl SocketWorker { ) -> anyhow::Result<()> { let socket = UdpSocket::from_std(create_socket(&config, priv_dropper)?); let access_list_cache = create_access_list_cache(&shared_state.access_list); - let opt_resend_buffer = (config.network.resend_buffer_max_len > 0).then_some(Vec::new()); let mut worker = Self { config, @@ -56,7 +54,6 @@ impl SocketWorker { validator, access_list_cache, socket, - opt_resend_buffer, buffer: [0; BUFFER_SIZE], rng: SmallRng::from_entropy(), }; @@ -65,6 +62,8 @@ impl SocketWorker { } pub fn run_inner(&mut self) -> anyhow::Result<()> { + let mut opt_resend_buffer = + (self.config.network.resend_buffer_max_len > 0).then_some(Vec::new()); let mut events = Events::with_capacity(1); let mut poll = Poll::new().context("create poll")?; @@ -79,28 +78,23 @@ impl SocketWorker { for event in events.iter() { if event.is_readable() { - self.read_and_handle_requests(); + self.read_and_handle_requests(&mut opt_resend_buffer); } } // If resend buffer is enabled, send any responses in it - if let Some(resend_buffer) = self.opt_resend_buffer.as_mut() { + if let Some(resend_buffer) = opt_resend_buffer.as_mut() { for (addr, response) in resend_buffer.drain(..) { - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut None, - response, - addr, - ); + self.send_response(&mut None, addr, response); } } } } - fn read_and_handle_requests(&mut self) { + fn read_and_handle_requests( + &mut self, + opt_resend_buffer: &mut Option>, + ) { let max_scrape_torrents = self.config.protocol.max_scrape_torrents; loop { @@ -144,7 +138,9 @@ impl SocketWorker { statistics.requests.fetch_add(1, Ordering::Relaxed); } - self.handle_request(request, src); + if let Some(response) = self.handle_request(request, src) { + self.send_response(opt_resend_buffer, src, response); + } } Err(RequestParseError::Sendable { connection_id, @@ -156,15 +152,7 @@ impl SocketWorker { message: err.into(), }; - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - Response::Error(response), - src, - ); + self.send_response(opt_resend_buffer, src, Response::Error(response)); ::log::debug!("request parse error (sent error response): {:?}", err); } @@ -186,25 +174,15 @@ impl SocketWorker { } } - fn handle_request(&mut self, request: Request, src: CanonicalSocketAddr) { + fn handle_request(&mut self, request: Request, src: CanonicalSocketAddr) -> Option { let access_list_mode = self.config.access_list.mode; match request { Request::Connect(request) => { - let response = ConnectResponse { + return Some(Response::Connect(ConnectResponse { connection_id: self.validator.create_connection_id(src), transaction_id: request.transaction_id, - }; - - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - Response::Connect(response), - src, - ); + })); } Request::Announce(request) => { if self @@ -230,30 +208,12 @@ impl SocketWorker { peer_valid_until, ); - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - response, - src, - ); + return Some(response); } else { - let response = ErrorResponse { + return Some(Response::Error(ErrorResponse { transaction_id: request.transaction_id, message: "Info hash not allowed".into(), - }; - - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - Response::Error(response), - src, - ); + })); } } } @@ -262,93 +222,85 @@ impl SocketWorker { .validator .connection_id_valid(src, request.connection_id) { - let response = self.shared_state.torrent_maps.scrape(request, src); - - send_response( - &self.config, - &self.statistics, - &mut self.socket, - &mut self.buffer, - &mut self.opt_resend_buffer, - Response::Scrape(response), - src, - ); + return Some(Response::Scrape( + self.shared_state.torrent_maps.scrape(request, src), + )); } } } - } -} -fn send_response( - config: &Config, - statistics: &CachePaddedArc>, - socket: &mut UdpSocket, - buffer: &mut [u8], - opt_resend_buffer: &mut Option>, - response: Response, - canonical_addr: CanonicalSocketAddr, -) { - let mut buffer = Cursor::new(&mut buffer[..]); - - if let Err(err) = response.write_bytes(&mut buffer) { - ::log::error!("failed writing response to buffer: {:#}", err); - - return; + None } - let bytes_written = buffer.position() as usize; + fn send_response( + &mut self, + opt_resend_buffer: &mut Option>, + canonical_addr: CanonicalSocketAddr, + response: Response, + ) { + let mut buffer = Cursor::new(&mut self.buffer[..]); + + if let Err(err) = response.write_bytes(&mut buffer) { + ::log::error!("failed writing response to buffer: {:#}", err); + + return; + } - let addr = if config.network.address.is_ipv4() { - canonical_addr - .get_ipv4() - .expect("found peer ipv6 address while running bound to ipv4 address") - } else { - canonical_addr.get_ipv6_mapped() - }; + let bytes_written = buffer.position() as usize; - match socket.send_to(&buffer.into_inner()[..bytes_written], addr) { - Ok(amt) if config.statistics.active() => { - let stats = if canonical_addr.is_ipv4() { - let stats = &statistics.ipv4; + let addr = if self.config.network.address.is_ipv4() { + canonical_addr + .get_ipv4() + .expect("found peer ipv6 address while running bound to ipv4 address") + } else { + canonical_addr.get_ipv6_mapped() + }; - stats - .bytes_sent - .fetch_add(amt + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); + match self + .socket + .send_to(&buffer.into_inner()[..bytes_written], addr) + { + Ok(bytes_sent) if self.config.statistics.active() => { + let stats = if canonical_addr.is_ipv4() { + let stats = &self.statistics.ipv4; - stats - } else { - let stats = &statistics.ipv6; + stats + .bytes_sent + .fetch_add(bytes_sent + EXTRA_PACKET_SIZE_IPV4, Ordering::Relaxed); - stats - .bytes_sent - .fetch_add(amt + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); + stats + } else { + let stats = &self.statistics.ipv6; - stats - }; + stats + .bytes_sent + .fetch_add(bytes_sent + EXTRA_PACKET_SIZE_IPV6, Ordering::Relaxed); - match response { - Response::Connect(_) => { - stats.responses_connect.fetch_add(1, Ordering::Relaxed); - } - Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => { - stats.responses_announce.fetch_add(1, Ordering::Relaxed); - } - Response::Scrape(_) => { - stats.responses_scrape.fetch_add(1, Ordering::Relaxed); - } - Response::Error(_) => { - stats.responses_error.fetch_add(1, Ordering::Relaxed); + stats + }; + + match response { + Response::Connect(_) => { + stats.responses_connect.fetch_add(1, Ordering::Relaxed); + } + Response::AnnounceIpv4(_) | Response::AnnounceIpv6(_) => { + stats.responses_announce.fetch_add(1, Ordering::Relaxed); + } + Response::Scrape(_) => { + stats.responses_scrape.fetch_add(1, Ordering::Relaxed); + } + Response::Error(_) => { + stats.responses_error.fetch_add(1, Ordering::Relaxed); + } } } - } - Ok(_) => (), - Err(err) => { - match opt_resend_buffer.as_mut() { + Ok(_) => (), + Err(err) => match opt_resend_buffer.as_mut() { Some(resend_buffer) if (err.raw_os_error() == Some(libc::ENOBUFS)) || (err.kind() == ErrorKind::WouldBlock) => { - if resend_buffer.len() < config.network.resend_buffer_max_len { + if resend_buffer.len() < self.config.network.resend_buffer_max_len { ::log::debug!("Adding response to resend queue, since sending it to {} failed with: {:#}", addr, err); resend_buffer.push((canonical_addr, response)); @@ -359,9 +311,9 @@ fn send_response( _ => { ::log::warn!("Sending response to {} failed: {:#}", addr, err); } - } + }, } - } - ::log::debug!("send response fn finished"); + ::log::debug!("send response fn finished"); + } } From 94247b8e3554283a0c20aedbf21b1312bb551d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 22:22:19 +0100 Subject: [PATCH 20/26] udp: mio: don't recalculate PeerUntil every announce request --- crates/udp/src/workers/socket/mio.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/udp/src/workers/socket/mio.rs b/crates/udp/src/workers/socket/mio.rs index fb24bef9..95bca786 100644 --- a/crates/udp/src/workers/socket/mio.rs +++ b/crates/udp/src/workers/socket/mio.rs @@ -32,6 +32,7 @@ pub struct SocketWorker { socket: UdpSocket, buffer: [u8; BUFFER_SIZE], rng: SmallRng, + peer_valid_until: ValidUntil, } impl SocketWorker { @@ -45,6 +46,10 @@ impl SocketWorker { ) -> anyhow::Result<()> { let socket = UdpSocket::from_std(create_socket(&config, priv_dropper)?); let access_list_cache = create_access_list_cache(&shared_state.access_list); + let peer_valid_until = ValidUntil::new( + shared_state.server_start_instant, + config.cleaning.max_peer_age, + ); let mut worker = Self { config, @@ -56,6 +61,7 @@ impl SocketWorker { socket, buffer: [0; BUFFER_SIZE], rng: SmallRng::from_entropy(), + peer_valid_until, }; worker.run_inner() @@ -73,6 +79,8 @@ impl SocketWorker { let poll_timeout = Duration::from_millis(self.config.network.poll_timeout_ms); + let mut iter_counter = 0u64; + loop { poll.poll(&mut events, Some(poll_timeout)).context("poll")?; @@ -88,6 +96,15 @@ impl SocketWorker { self.send_response(&mut None, addr, response); } } + + if iter_counter % 256 == 0 { + self.peer_valid_until = ValidUntil::new( + self.shared_state.server_start_instant, + self.config.cleaning.max_peer_age, + ); + } + + iter_counter = iter_counter.wrapping_add(1); } } @@ -194,18 +211,13 @@ impl SocketWorker { .load() .allows(access_list_mode, &request.info_hash.0) { - let peer_valid_until = ValidUntil::new( - self.shared_state.server_start_instant, - self.config.cleaning.max_peer_age, - ); - let response = self.shared_state.torrent_maps.announce( &self.config, &self.statistics_sender, &mut self.rng, &request, src, - peer_valid_until, + self.peer_valid_until, ); return Some(response); From 680da048b8ab41f0400973332db4ecad6d05b39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 22:47:00 +0100 Subject: [PATCH 21/26] udp: add ConnectionValidator.update_elapsed, call regularly This is faster than doing it for each request --- TODO.md | 3 --- crates/udp/src/workers/socket/mio.rs | 2 ++ crates/udp/src/workers/socket/uring/mod.rs | 7 +++-- crates/udp/src/workers/socket/validator.rs | 30 +++++++++++++--------- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/TODO.md b/TODO.md index 875687eb..17a10ef3 100644 --- a/TODO.md +++ b/TODO.md @@ -2,9 +2,6 @@ ## High priority -* udp - * make ConnectionValidator faster by avoiding calling time functions so often - ## Medium priority * stagger cleaning tasks? diff --git a/crates/udp/src/workers/socket/mio.rs b/crates/udp/src/workers/socket/mio.rs index 95bca786..a73a35eb 100644 --- a/crates/udp/src/workers/socket/mio.rs +++ b/crates/udp/src/workers/socket/mio.rs @@ -98,6 +98,8 @@ impl SocketWorker { } if iter_counter % 256 == 0 { + self.validator.update_elapsed(); + self.peer_valid_until = ValidUntil::new( self.shared_state.server_start_instant, self.config.cleaning.max_peer_age, diff --git a/crates/udp/src/workers/socket/uring/mod.rs b/crates/udp/src/workers/socket/uring/mod.rs index 2f85e428..ac572d4e 100644 --- a/crates/udp/src/workers/socket/uring/mod.rs +++ b/crates/udp/src/workers/socket/uring/mod.rs @@ -134,9 +134,10 @@ impl SocketWorker { let recv_sqe = recv_helper.create_entry(buf_ring.bgid()); - // This timeout enables regular updates of peer_valid_until + // This timeout enables regular updates of ConnectionValidator and + // peer_valid_until let pulse_timeout_sqe = { - let timespec_ptr = Box::into_raw(Box::new(Timespec::new().sec(1))) as *const _; + let timespec_ptr = Box::into_raw(Box::new(Timespec::new().sec(5))) as *const _; Timeout::new(timespec_ptr) .build() @@ -238,6 +239,8 @@ impl SocketWorker { } } USER_DATA_PULSE_TIMEOUT => { + self.validator.update_elapsed(); + self.peer_valid_until = ValidUntil::new( self.shared_state.server_start_instant, self.config.cleaning.max_peer_age, diff --git a/crates/udp/src/workers/socket/validator.rs b/crates/udp/src/workers/socket/validator.rs index c68d1efc..61e744c2 100644 --- a/crates/udp/src/workers/socket/validator.rs +++ b/crates/udp/src/workers/socket/validator.rs @@ -12,6 +12,8 @@ use crate::config::Config; /// HMAC (BLAKE3) based ConnectionId creator and validator /// +/// Method update_elapsed must be called at least once a minute. +/// /// The purpose of using ConnectionIds is to make IP spoofing costly, mainly to /// prevent the tracker from being used as an amplification vector for DDoS /// attacks. By including 32 bits of BLAKE3 keyed hash output in the Ids, an @@ -32,6 +34,7 @@ pub struct ConnectionValidator { start_time: Instant, max_connection_age: u64, keyed_hasher: blake3::Hasher, + seconds_since_start: u32, } impl ConnectionValidator { @@ -49,11 +52,12 @@ impl ConnectionValidator { keyed_hasher, start_time: Instant::now(), max_connection_age: config.cleaning.max_connection_age.into(), + seconds_since_start: 0, }) } pub fn create_connection_id(&mut self, source_addr: CanonicalSocketAddr) -> ConnectionId { - let elapsed = (self.start_time.elapsed().as_secs() as u32).to_ne_bytes(); + let elapsed = (self.seconds_since_start).to_ne_bytes(); let hash = self.hash(elapsed, source_addr.get().ip()); @@ -78,16 +82,23 @@ impl ConnectionValidator { return false; } - let tracker_elapsed = self.start_time.elapsed().as_secs(); + let seconds_since_start = self.seconds_since_start as u64; let client_elapsed = u64::from(u32::from_ne_bytes(elapsed)); let client_expiration_time = client_elapsed + self.max_connection_age; // In addition to checking if the client connection is expired, - // disallow client_elapsed values that are in future and thus could not - // have been sent by the tracker. This prevents brute forcing with - // `u32::MAX` as 'elapsed' part of ConnectionId to find a hash that + // disallow client_elapsed values that are too far in future and thus + // could not have been sent by the tracker. This prevents brute forcing + // with `u32::MAX` as 'elapsed' part of ConnectionId to find a hash that // works until the tracker is restarted. - (client_expiration_time > tracker_elapsed) & (client_elapsed <= tracker_elapsed) + let client_not_expired = client_expiration_time > seconds_since_start; + let client_elapsed_not_in_far_future = client_elapsed <= (seconds_since_start + 60); + + client_not_expired & client_elapsed_not_in_far_future + } + + pub fn update_elapsed(&mut self) { + self.seconds_since_start = self.start_time.elapsed().as_secs() as u32; } fn hash(&mut self, elapsed: [u8; 4], ip_addr: IpAddr) -> [u8; 4] { @@ -145,11 +156,6 @@ mod tests { return quickcheck::TestResult::failed(); } - if max_connection_age == 0 { - quickcheck::TestResult::from_bool(!original_valid) - } else { - // Note: depends on that running this test takes less than a second - quickcheck::TestResult::from_bool(original_valid) - } + quickcheck::TestResult::from_bool(original_valid) } } From d7ebf5e546acfdc07c065ed222ce7a81a1267d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 22:52:36 +0100 Subject: [PATCH 22/26] Update CHANGELOG --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed43774..cf4ab65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,28 +18,28 @@ #### Changed +* Switch from socket worker/swarm worker division to a single type of worker, + for performance reasons. Several config file keys were removed since they + are no longer needed. * Index peers by packet source IP and provided port, instead of by peer_id. This prevents users from impersonating others and is likely also slightly faster for IPv4 peers. -* Remove support for unbounded worker channels -* Add backpressure in socket workers. They will postpone reading from the - socket if sending a request to a swarm worker failed * Avoid a heap allocation for torrents with two or less peers. This can save a lot of memory if many torrents are tracked * Improve announce performance by avoiding having to filter response peers * In announce response statistics, don't include announcing peer -* Distribute announce responses from swarm workers over socket workers to - decrease performance loss due to underutilized threads * Harden ConnectionValidator to make IP spoofing even more costly * Remove config key `network.poll_event_capacity` (always use 1) * Speed up parsing and serialization of requests and responses by using [zerocopy](https://crates.io/crates/zerocopy) * Report socket worker related prometheus stats per worker +* Remove CPU pinning support #### Fixed * Quit whole application if any worker thread quits * Disallow announce requests with port value of 0 +* Fix io_uring UB issues ### aquatic_http From f4ef9fcfc9a9b40f333cfba0b479a524ce273e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 22:56:37 +0100 Subject: [PATCH 23/26] udp: fix test_connection_validator --- crates/udp/src/workers/socket/validator.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/udp/src/workers/socket/validator.rs b/crates/udp/src/workers/socket/validator.rs index 61e744c2..f96b9159 100644 --- a/crates/udp/src/workers/socket/validator.rs +++ b/crates/udp/src/workers/socket/validator.rs @@ -156,6 +156,10 @@ mod tests { return quickcheck::TestResult::failed(); } - quickcheck::TestResult::from_bool(original_valid) + if max_connection_age == 0 { + quickcheck::TestResult::from_bool(!original_valid) + } else { + quickcheck::TestResult::from_bool(original_valid) + } } } From 14c973f72fac29ea4d8e0669b8671152cffdc72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sat, 10 Feb 2024 23:07:59 +0100 Subject: [PATCH 24/26] udp: Config.socket_workers: make value 0 auto-use available vCPUs --- crates/udp/src/config.rs | 4 +++- crates/udp/src/lib.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/udp/src/config.rs b/crates/udp/src/config.rs index b25eae94..ae97a4f0 100644 --- a/crates/udp/src/config.rs +++ b/crates/udp/src/config.rs @@ -11,7 +11,9 @@ use aquatic_toml_config::TomlConfig; #[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize, Serialize)] #[serde(default, deny_unknown_fields)] pub struct Config { - /// Number of socket workers. One per virtual CPU is recommended + /// Number of socket workers + /// + /// 0 = automatically set to number of available virtual CPUs pub socket_workers: usize, pub log_level: LogLevel, pub network: NetworkConfig, diff --git a/crates/udp/src/lib.rs b/crates/udp/src/lib.rs index e91027ee..b1215c1f 100644 --- a/crates/udp/src/lib.rs +++ b/crates/udp/src/lib.rs @@ -3,7 +3,7 @@ pub mod config; pub mod swarm; pub mod workers; -use std::thread::{sleep, Builder, JoinHandle}; +use std::thread::{available_parallelism, sleep, Builder, JoinHandle}; use std::time::Duration; use anyhow::Context; @@ -22,9 +22,13 @@ use workers::socket::ConnectionValidator; pub const APP_NAME: &str = "aquatic_udp: UDP BitTorrent tracker"; pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub fn run(config: Config) -> ::anyhow::Result<()> { +pub fn run(mut config: Config) -> ::anyhow::Result<()> { let mut signals = Signals::new([SIGUSR1])?; + if config.socket_workers == 0 { + config.socket_workers = available_parallelism().map(Into::into).unwrap_or(1); + }; + let state = State::default(); let statistics = Statistics::new(&config); let connection_validator = ConnectionValidator::new(&config)?; From f455e5825114a3f72e8d28ce8876e70504f46c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sun, 11 Feb 2024 00:59:23 +0100 Subject: [PATCH 25/26] udp: swarm cleaning: send statistics messages after releasing locks --- crates/udp/src/swarm.rs | 100 ++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 54 deletions(-) diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index 6a5e3278..c9523a2d 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -98,26 +98,37 @@ impl TorrentMaps { let mode = config.access_list.mode; let now = server_start_instant.seconds_elapsed(); - let ipv4 = - self.ipv4 - .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); - let ipv6 = - self.ipv6 - .clean_and_get_statistics(config, statistics_sender, &mut cache, mode, now); + let mut statistics_messages = Vec::new(); + + let ipv4 = self.ipv4.clean_and_get_statistics( + config, + &mut statistics_messages, + &mut cache, + mode, + now, + ); + let ipv6 = self.ipv6.clean_and_get_statistics( + config, + &mut statistics_messages, + &mut cache, + mode, + now, + ); if config.statistics.active() { statistics.ipv4.torrents.store(ipv4.0, Ordering::Relaxed); statistics.ipv6.torrents.store(ipv6.0, Ordering::Relaxed); - statistics.ipv4.peers.store(ipv4.1, Ordering::Relaxed); statistics.ipv6.peers.store(ipv6.1, Ordering::Relaxed); - if let Some(message) = ipv4.2.map(StatisticsMessage::Ipv4PeerHistogram) { - if let Err(err) = statistics_sender.try_send(message) { - ::log::error!("couldn't send statistics message: {:#}", err); - } + if let Some(message) = ipv4.2 { + statistics_messages.push(StatisticsMessage::Ipv4PeerHistogram(message)); + } + if let Some(message) = ipv6.2 { + statistics_messages.push(StatisticsMessage::Ipv6PeerHistogram(message)); } - if let Some(message) = ipv6.2.map(StatisticsMessage::Ipv6PeerHistogram) { + + for message in statistics_messages { if let Err(err) = statistics_sender.try_send(message) { ::log::error!("couldn't send statistics message: {:#}", err); } @@ -204,7 +215,7 @@ impl TorrentMapShards { fn clean_and_get_statistics( &self, config: &Config, - statistics_sender: &Sender, + statistics_messages: &mut Vec, access_list_cache: &mut AccessListCache, access_list_mode: AccessListMode, now: SecondsSinceServerStart, @@ -212,19 +223,10 @@ impl TorrentMapShards { let mut total_num_torrents = 0; let mut total_num_peers = 0; - let mut opt_histogram: Option> = if config.statistics.torrent_peer_histograms - { - match Histogram::new(3) { - Ok(histogram) => Some(histogram), - Err(err) => { - ::log::error!("Couldn't create peer histogram: {:#}", err); - - None - } - } - } else { - None - }; + let mut opt_histogram: Option> = config + .statistics + .torrent_peer_histograms + .then(|| Histogram::new(3).expect("create peer histogram")); for torrent_map_shard in self.0.iter() { for torrent_data in torrent_map_shard.read().values() { @@ -232,11 +234,14 @@ impl TorrentMapShards { let num_peers = match peer_map.deref_mut() { PeerMap::Small(small_peer_map) => { - small_peer_map.clean_and_get_num_peers(config, statistics_sender, now) + small_peer_map.clean_and_get_num_peers(config, statistics_messages, now) } PeerMap::Large(large_peer_map) => { - let num_peers = - large_peer_map.clean_and_get_num_peers(config, statistics_sender, now); + let num_peers = large_peer_map.clean_and_get_num_peers( + config, + statistics_messages, + now, + ); if let Some(small_peer_map) = large_peer_map.try_shrink() { *peer_map = PeerMap::Small(small_peer_map); @@ -248,22 +253,20 @@ impl TorrentMapShards { drop(peer_map); - match opt_histogram { - Some(ref mut histogram) if num_peers > 0 => { - let n = num_peers.try_into().expect("Couldn't fit usize into u64"); - - if let Err(err) = histogram.record(n) { - ::log::error!("Couldn't record {} to histogram: {:#}", n, err); + match opt_histogram.as_mut() { + Some(histogram) if num_peers > 0 => { + if let Err(err) = histogram.record(num_peers as u64) { + ::log::error!("Couldn't record {} to histogram: {:#}", num_peers, err); } } _ => (), } + total_num_peers += num_peers; + torrent_data .pending_removal .store(num_peers == 0, Ordering::Release); - - total_num_peers += num_peers; } let mut torrent_map_shard = torrent_map_shard.write(); @@ -509,20 +512,14 @@ impl SmallPeerMap { fn clean_and_get_num_peers( &mut self, config: &Config, - statistics_sender: &Sender, + statistics_messages: &mut Vec, now: SecondsSinceServerStart, ) -> usize { self.0.retain(|(_, peer)| { let keep = peer.valid_until.valid(now); - if !keep - && config.statistics.peer_clients - && statistics_sender - .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) - .is_err() - { - // Should never happen in practice - ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); + if !keep && config.statistics.peer_clients { + statistics_messages.push(StatisticsMessage::PeerRemoved(peer.peer_id)); } keep @@ -621,7 +618,7 @@ impl LargePeerMap { fn clean_and_get_num_peers( &mut self, config: &Config, - statistics_sender: &Sender, + statistics_messages: &mut Vec, now: SecondsSinceServerStart, ) -> usize { self.peers.retain(|_, peer| { @@ -631,13 +628,8 @@ impl LargePeerMap { if peer.is_seeder { self.num_seeders -= 1; } - if config.statistics.peer_clients - && statistics_sender - .try_send(StatisticsMessage::PeerRemoved(peer.peer_id)) - .is_err() - { - // Should never happen in practice - ::log::error!("Couldn't send StatisticsMessage::PeerRemoved"); + if config.statistics.peer_clients { + statistics_messages.push(StatisticsMessage::PeerRemoved(peer.peer_id)); } } From 61bc4f0d9d6230abe4c8c437bd21b6b4cdfe8984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Sun, 11 Feb 2024 01:02:16 +0100 Subject: [PATCH 26/26] udp: swarm: extract_response_peers: improve docs, add .copied() to iters --- crates/udp/src/swarm.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/udp/src/swarm.rs b/crates/udp/src/swarm.rs index c9523a2d..2d422f93 100644 --- a/crates/udp/src/swarm.rs +++ b/crates/udp/src/swarm.rs @@ -570,11 +570,10 @@ impl LargePeerMap { /// Extract response peers /// - /// If there are more peers in map than `max_num_peers_to_take`, do a random - /// selection of peers from first and second halves of map in order to avoid - /// returning too homogeneous peers. - /// - /// Does NOT filter out announcing peer. + /// If there are more peers in map than `max_num_peers_to_take`, do a + /// random selection of peers from first and second halves of map in + /// order to avoid returning too homogeneous peers. This is a lot more + /// cache-friendly than doing a fully random selection. fn extract_response_peers( &self, rng: &mut impl Rng, @@ -605,10 +604,10 @@ impl LargePeerMap { let mut peers = Vec::with_capacity(max_num_peers_to_take); if let Some(slice) = self.peers.get_range(offset_half_one..end_half_one) { - peers.extend(slice.keys()); + peers.extend(slice.keys().copied()); } if let Some(slice) = self.peers.get_range(offset_half_two..end_half_two) { - peers.extend(slice.keys()); + peers.extend(slice.keys().copied()); } peers
* Peer count is updated every { peer_update_interval } seconds* Torrent/peer count is updated every { peer_update_interval } seconds
Number of torrents{ ipv6.num_torrents }{ ipv6.num_torrents } *
Number of peers