From 2e1893a83aa17fbe6a9da50bd5f1bc017d6f2e0e Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Thu, 28 Mar 2024 12:43:53 +0800 Subject: [PATCH 001/217] dev: rework error for torrent tracker --- .vscode/settings.json | 1 + Cargo.lock | 12 -- Cargo.toml | 1 - cSpell.json | 3 +- src/console/clients/checker/app.rs | 24 +-- src/console/clients/checker/checks/health.rs | 90 +++++++---- src/console/clients/checker/checks/http.rs | 128 ++++++++------- src/console/clients/checker/checks/udp.rs | 9 +- src/console/clients/checker/config.rs | 17 +- src/console/clients/checker/console.rs | 1 + src/console/clients/checker/logger.rs | 1 + src/console/clients/checker/service.rs | 32 ++-- src/console/clients/http/app.rs | 34 ++-- .../bit_torrent/tracker/http/client/mod.rs | 150 ++++++++++++++---- .../tracker/http/client/responses/scrape.rs | 7 +- 15 files changed, 326 insertions(+), 184 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index caa48dd01..bf78ed3f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,5 +31,6 @@ "evenBetterToml.formatter.trailingNewline": true, "evenBetterToml.formatter.reorderKeys": true, "evenBetterToml.formatter.reorderArrays": true, + "rust-analyzer.showUnlinkedFileNotification": false, } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e77b5de6d..d61ac26d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3940,7 +3940,6 @@ dependencies = [ "torrust-tracker-test-helpers", "torrust-tracker-torrent-repository", "tower-http", - "trace", "tracing", "url", "uuid", @@ -4071,17 +4070,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" -[[package]] -name = "trace" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad0c048e114d19d1140662762bfdb10682f3bc806d8be18af846600214dd9af" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "tracing" version = "0.1.40" diff --git a/Cargo.toml b/Cargo.toml index 99b7a334a..623971552 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,6 @@ torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "pa torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" } torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path = "packages/torrent-repository" } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } -trace = "0" tracing = "0" url = "2" uuid = { version = "1", features = ["v4"] } diff --git a/cSpell.json b/cSpell.json index bbcba98a7..dbe0ca430 100644 --- a/cSpell.json +++ b/cSpell.json @@ -29,6 +29,7 @@ "certbot", "chrono", "clippy", + "Cloneable", "codecov", "codegen", "completei", @@ -69,8 +70,8 @@ "infoschema", "Intermodal", "intervali", - "kcachegrind", "Joakim", + "kcachegrind", "keyout", "lcov", "leecher", diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index 82ea800d0..0c9852557 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -53,7 +53,7 @@ pub async fn run() -> Result> { console: console_printer, }; - Ok(service.run_checks().await) + service.run_checks().await } fn setup_logging(level: LevelFilter) { @@ -78,15 +78,17 @@ fn setup_logging(level: LevelFilter) { } fn setup_config(args: Args) -> Result { - match (args.config_path, args.config_content) { - (Some(config_path), _) => load_config_from_file(&config_path), - (_, Some(config_content)) => parse_from_json(&config_content).context("invalid config format"), - _ => Err(anyhow::anyhow!("no configuration provided")), + // If a config is directly supplied, we use it. + if let Some(config) = args.config_content { + parse_from_json(&config).context("invalid config format") + } + // or we load it from a file... + else if let Some(path) = args.config_path { + let file_content = std::fs::read_to_string(path.clone()).with_context(|| format!("can't read config file {path:?}"))?; + parse_from_json(&file_content).context("invalid config format") + } + // but we cannot run without any config... + else { + Err(anyhow::anyhow!("no configuration provided")) } -} - -fn load_config_from_file(path: &PathBuf) -> Result { - let file_content = std::fs::read_to_string(path).with_context(|| format!("can't read config file {path:?}"))?; - - parse_from_json(&file_content).context("invalid config format") } diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index 9c28da514..a579ffe1e 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -1,51 +1,75 @@ +use std::sync::Arc; use std::time::Duration; +use anyhow::Result; use colored::Colorize; -use reqwest::{Client as HttpClient, Url, Url as ServiceUrl}; +use hyper::StatusCode; +use reqwest::{Client as HttpClient, Response}; +use thiserror::Error; +use url::Url; use crate::console::clients::checker::console::Console; use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; -pub async fn run(health_checks: &Vec, console: &Console, check_results: &mut Vec) { - console.println("Health checks ..."); - - for health_check_url in health_checks { - match run_health_check(health_check_url.clone(), console).await { - Ok(()) => check_results.push(Ok(())), - Err(err) => check_results.push(Err(err)), - } - } +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("Failed to Build a Http Client: {err:?}")] + ClientBuildingError { err: Arc }, + #[error("Heath check failed to get a response: {err:?}")] + ResponseError { err: Arc }, + #[error("Http check returned a non-success code: \"{code}\" with the response: \"{response:?}\"")] + UnsuccessfulResponse { code: StatusCode, response: Arc }, } -async fn run_health_check(url: Url, console: &Console) -> Result<(), CheckError> { - let client = HttpClient::builder().timeout(Duration::from_secs(5)).build().unwrap(); +pub async fn run(health_checks: Vec, timeout: Duration, console: Console) -> Vec { + let mut check_results = Vec::default(); - let colored_url = url.to_string().yellow(); + console.println("Health checks ..."); + + for url in health_checks { + let colored_url = url.to_string().yellow(); - match client.get(url.clone()).send().await { - Ok(response) => { - if response.status().is_success() { - console.println(&format!("{} - Health API at {} is OK", "✓".green(), colored_url)); - Ok(()) - } else { - console.eprintln(&format!( - "{} - Health API at {} is failing: {:?}", - "✗".red(), + match run_health_check(url.clone(), timeout).await { + Ok(response) => { + console.println(&format!( + "{} - Health API at {} is {}", + "✓".green(), colored_url, - response + response.status() )); - Err(CheckError::HealthCheckError { url }) + + check_results.push(Ok(())); + } + Err(err) => { + console.eprintln(&format!("{} - Health API at {} is failing: {}", "✗".red(), colored_url, err)); + + check_results.push(Err(CheckError::HealthCheckError { url, err })); } } - Err(err) => { - console.eprintln(&format!( - "{} - Health API at {} is failing: {:?}", - "✗".red(), - colored_url, - err - )); - Err(CheckError::HealthCheckError { url }) - } + } + + check_results +} + +async fn run_health_check(url: Url, timeout: Duration) -> Result { + let client = HttpClient::builder() + .timeout(timeout) + .build() + .map_err(|e| Error::ClientBuildingError { err: e.into() })?; + + let response = client + .get(url.clone()) + .send() + .await + .map_err(|e| Error::ResponseError { err: e.into() })?; + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::UnsuccessfulResponse { + code: response.status(), + response: response.into(), + }) } } diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index 501696df4..046963dd5 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,8 +1,10 @@ use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use anyhow::Result; use colored::Colorize; -use log::debug; -use reqwest::Url as ServiceUrl; +use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; @@ -14,82 +16,92 @@ use crate::shared::bit_torrent::tracker::http::client::responses::announce::Anno use crate::shared::bit_torrent::tracker::http::client::responses::scrape; use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; -pub async fn run(http_trackers: &Vec, console: &Console, check_results: &mut Vec) { +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("Http request did not receive a response within the timeout: {err:?}")] + HttpClientError { + err: crate::shared::bit_torrent::tracker::http::client::Error, + }, + #[error("Http failed to get a response at all: {err:?}")] + ResponseError { err: Arc }, + #[error("Failed to deserialize the serde bencoded response data with the error: \"{err:?}\"")] + ParseSerdeBencodeError { + data: hyper::body::Bytes, + err: Arc, + }, + + #[error("Failed to deserialize the bencoded response data with the error: \"{err:?}\"")] + ParseScrapeBencodeError { + data: hyper::body::Bytes, + err: Arc, + }, +} + +pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) -> Vec { + let mut check_results = Vec::default(); + console.println("HTTP trackers ..."); - for http_tracker in http_trackers { - let colored_tracker_url = http_tracker.to_string().yellow(); + for ref url in http_trackers { + let colored_url = url.to_string().yellow(); - match check_http_announce(http_tracker).await { - Ok(()) => { - check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); + check_results.push(match check_http_announce(url, timeout).await { + Ok(_) => { + console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_url)); + Ok(()) } Err(err) => { - check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); + console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_url)); + Err(CheckError::HttpCheckError { url: url.clone(), err }) } - } + }); - match check_http_scrape(http_tracker).await { - Ok(()) => { - check_results.push(Ok(())); - console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_tracker_url)); + check_results.push(match check_http_scrape(url, timeout).await { + Ok(_) => { + console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_url)); + Ok(()) } Err(err) => { - check_results.push(Err(err)); - console.println(&format!("{} - Scrape at {} is failing", "✗".red(), colored_tracker_url)); + console.println(&format!("{} - Scrape at {} is failing", "✗".red(), colored_url)); + Err(CheckError::HttpCheckError { url: url.clone(), err }) } - } + }); } + + check_results } -async fn check_http_announce(tracker_url: &Url) -> Result<(), CheckError> { +async fn check_http_announce(url: &Url, timeout: Duration) -> Result { let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237 let info_hash = InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required"); - // todo: HTTP request could panic.For example, if the server is not accessible. - // We should change the client to catch that error and return a `CheckError`. - // Otherwise the checking process will stop. The idea is to process all checks - // and return a final report. - let response = Client::new(tracker_url.clone()) + let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; + + let response = client .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await; - - if let Ok(body) = response.bytes().await { - if let Ok(_announce_response) = serde_bencode::from_bytes::(&body) { - Ok(()) - } else { - debug!("announce body {:#?}", body); - Err(CheckError::HttpError { - url: tracker_url.clone(), - }) - } - } else { - Err(CheckError::HttpError { - url: tracker_url.clone(), - }) - } + .await + .map_err(|err| Error::HttpClientError { err })?; + + let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; + + serde_bencode::from_bytes::(&body).map_err(|e| Error::ParseSerdeBencodeError { + data: body, + err: e.into(), + }) } -async fn check_http_scrape(url: &Url) -> Result<(), CheckError> { +async fn check_http_scrape(url: &Url, timeout: Duration) -> Result { let info_hashes: Vec = vec!["9c38422213e30bff212b30c360d26f9a02136422".to_string()]; // # DevSkim: ignore DS173237 let query = requests::scrape::Query::try_from(info_hashes).expect("a valid array of info-hashes is required"); - // todo: HTTP request could panic.For example, if the server is not accessible. - // We should change the client to catch that error and return a `CheckError`. - // Otherwise the checking process will stop. The idea is to process all checks - // and return a final report. - let response = Client::new(url.clone()).scrape(&query).await; - - if let Ok(body) = response.bytes().await { - if let Ok(_scrape_response) = scrape::Response::try_from_bencoded(&body) { - Ok(()) - } else { - debug!("scrape body {:#?}", body); - Err(CheckError::HttpError { url: url.clone() }) - } - } else { - Err(CheckError::HttpError { url: url.clone() }) - } + let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; + + let response = client.scrape(&query).await.map_err(|err| Error::HttpClientError { err })?; + + let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; + + scrape::Response::try_from_bencoded(&body).map_err(|e| Error::ParseScrapeBencodeError { + data: body, + err: e.into(), + }) } diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 47a2a1a00..fae489f44 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -1,4 +1,5 @@ use std::net::SocketAddr; +use std::time::Duration; use aquatic_udp_protocol::{Port, TransactionId}; use colored::Colorize; @@ -14,10 +15,12 @@ use crate::console::clients::udp::checker; const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; -pub async fn run(udp_trackers: &Vec, console: &Console, check_results: &mut Vec) { +pub async fn run(udp_trackers: Vec, _: Duration, console: Console) -> Vec { + let mut check_results = Vec::default(); + console.println("UDP trackers ..."); - for udp_tracker in udp_trackers { + for ref udp_tracker in udp_trackers { debug!("UDP tracker: {:?}", udp_tracker); let colored_tracker_url = udp_tracker.to_string().yellow(); @@ -84,4 +87,6 @@ pub async fn run(udp_trackers: &Vec, console: &Console, check_result console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); } } + + check_results } diff --git a/src/console/clients/checker/config.rs b/src/console/clients/checker/config.rs index 6e44d889b..f3be50da1 100644 --- a/src/console/clients/checker/config.rs +++ b/src/console/clients/checker/config.rs @@ -1,19 +1,19 @@ use std::error::Error; use std::fmt; use std::net::SocketAddr; +use std::time::Duration; use reqwest::Url as ServiceUrl; use serde::Deserialize; +/// Client Timeout +const TIMEOUT_SEC: Duration = Duration::from_secs(5); + /// It parses the configuration from a JSON format. /// /// # Errors /// /// Will return an error if the configuration is not valid. -/// -/// # Panics -/// -/// Will panic if unable to read the configuration file. pub fn parse_from_json(json: &str) -> Result { let plain_config: PlainConfiguration = serde_json::from_str(json).map_err(ConfigurationError::JsonParseError)?; Configuration::try_from(plain_config) @@ -22,7 +22,7 @@ pub fn parse_from_json(json: &str) -> Result /// DTO for the configuration to serialize/deserialize configuration. /// /// Configuration does not need to be valid. -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] struct PlainConfiguration { pub udp_trackers: Vec, pub http_trackers: Vec, @@ -30,10 +30,12 @@ struct PlainConfiguration { } /// Validated configuration +#[derive(Debug, Clone)] pub struct Configuration { pub udp_trackers: Vec, pub http_trackers: Vec, pub health_checks: Vec, + pub client_timeout: Duration, } #[derive(Debug)] @@ -81,6 +83,7 @@ impl TryFrom for Configuration { udp_trackers, http_trackers, health_checks, + client_timeout: TIMEOUT_SEC, }) } } @@ -107,11 +110,11 @@ mod tests { ); assert_eq!( config.http_trackers, - vec![ServiceUrl::parse("http://127.0.0.1:8080").unwrap()] + vec![ServiceUrl::parse("http://127.0.0.1:8080").expect("it should be a valid url")] ); assert_eq!( config.health_checks, - vec![ServiceUrl::parse("http://127.0.0.1:8080/health").unwrap()] + vec![ServiceUrl::parse("http://127.0.0.1:8080/health").expect("it should be a valid url")] ); } diff --git a/src/console/clients/checker/console.rs b/src/console/clients/checker/console.rs index b55c559fc..b516c81ce 100644 --- a/src/console/clients/checker/console.rs +++ b/src/console/clients/checker/console.rs @@ -1,5 +1,6 @@ use super::printer::{Printer, CLEAR_SCREEN}; +#[derive(Debug, Clone, Copy)] pub struct Console {} impl Default for Console { diff --git a/src/console/clients/checker/logger.rs b/src/console/clients/checker/logger.rs index 50e97189f..a1295bacc 100644 --- a/src/console/clients/checker/logger.rs +++ b/src/console/clients/checker/logger.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use super::printer::{Printer, CLEAR_SCREEN}; +#[derive(Debug, Clone)] pub struct Logger { output: RefCell, } diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index 94eff4a88..d484c560a 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -1,7 +1,10 @@ use std::net::SocketAddr; use std::sync::Arc; +use anyhow::{Context, Result}; use reqwest::Url; +use thiserror::Error; +use tokio::task::JoinSet; use super::checks; use super::config::Configuration; @@ -15,28 +18,37 @@ pub struct Service { pub type CheckResult = Result<(), CheckError>; -#[derive(Debug)] +#[derive(Debug, Clone, Error)] pub enum CheckError { + #[error("Error In Udp: socket: {socket_addr:?}")] UdpError { socket_addr: SocketAddr }, - HttpError { url: Url }, - HealthCheckError { url: Url }, + #[error("Error In Http: url: {url:?}")] + HttpCheckError { url: Url, err: checks::http::Error }, + #[error("Error In HeathCheck: url: {url:?}")] + HealthCheckError { url: Url, err: checks::health::Error }, } impl Service { /// # Errors /// - /// Will return OK is all checks pass or an array with the check errors. - pub async fn run_checks(&self) -> Vec { + /// It will return an error if some of the tests panic or otherwise fail to run. + /// On success it will return a vector of `Ok(())` of [`CheckResult`]. + pub async fn run_checks(self) -> Result> { self.console.println("Running checks for trackers ..."); - let mut check_results = vec![]; + let mut check_results = Vec::::default(); - checks::udp::run(&self.config.udp_trackers, &self.console, &mut check_results).await; + let timeout = self.config.client_timeout; - checks::http::run(&self.config.http_trackers, &self.console, &mut check_results).await; + let mut checks = JoinSet::new(); + checks.spawn(checks::udp::run(self.config.udp_trackers.clone(), timeout, self.console)); + checks.spawn(checks::http::run(self.config.http_trackers.clone(), timeout, self.console)); + checks.spawn(checks::health::run(self.config.health_checks.clone(), timeout, self.console)); - checks::health::run(&self.config.health_checks, &self.console, &mut check_results).await; + while let Some(results) = checks.join_next().await { + check_results.append(&mut results.context("failed to join check")?); + } - check_results + Ok(check_results) } } diff --git a/src/console/clients/http/app.rs b/src/console/clients/http/app.rs index 511fb6628..b183be7ac 100644 --- a/src/console/clients/http/app.rs +++ b/src/console/clients/http/app.rs @@ -14,6 +14,7 @@ //! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq //! ``` use std::str::FromStr; +use std::time::Duration; use anyhow::Context; use clap::{Parser, Subcommand}; @@ -30,6 +31,7 @@ use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; struct Args { #[command(subcommand)] command: Command, + timeout: u64, } #[derive(Subcommand, Debug)] @@ -44,34 +46,38 @@ enum Command { pub async fn run() -> anyhow::Result<()> { let args = Args::parse(); + let timeout = Duration::from_secs(args.timeout); + match args.command { Command::Announce { tracker_url, info_hash } => { - announce_command(tracker_url, info_hash).await?; + announce_command(tracker_url, timeout, info_hash).await?; } Command::Scrape { tracker_url, info_hashes, } => { - scrape_command(&tracker_url, &info_hashes).await?; + scrape_command(&tracker_url, timeout, &info_hashes).await?; } } Ok(()) } -async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Result<()> { +async fn announce_command(tracker_url: String, timeout: Duration, info_hash: String) -> anyhow::Result<()> { let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; let info_hash = InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); - let response = Client::new(base_url) + let response = Client::new(base_url, timeout)? .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await; + .await?; - let body = response.bytes().await.unwrap(); + let body = response.bytes().await.context("it should get back a valid response")?; - let announce_response: Announce = serde_bencode::from_bytes(&body) - .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body)); + let announce_response: Announce = serde_bencode::from_bytes(&body).context(format!( + "response body should be a valid announce response, got: \"{:#?}\"", + &body + ))?; let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?; @@ -80,17 +86,19 @@ async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Res Ok(()) } -async fn scrape_command(tracker_url: &str, info_hashes: &[String]) -> anyhow::Result<()> { +async fn scrape_command(tracker_url: &str, timeout: Duration, info_hashes: &[String]) -> anyhow::Result<()> { let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; - let response = Client::new(base_url).scrape(&query).await; + let response = Client::new(base_url, timeout)?.scrape(&query).await?; - let body = response.bytes().await.unwrap(); + let body = response.bytes().await.context("it should get back a valid response")?; - let scrape_response = scrape::Response::try_from_bencoded(&body) - .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body)); + let scrape_response = scrape::Response::try_from_bencoded(&body).context(format!( + "response body should be a valid scrape response, got: \"{:#?}\"", + &body + ))?; let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; diff --git a/src/shared/bit_torrent/tracker/http/client/mod.rs b/src/shared/bit_torrent/tracker/http/client/mod.rs index a75b0fec3..4cddc98f4 100644 --- a/src/shared/bit_torrent/tracker/http/client/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/mod.rs @@ -2,17 +2,31 @@ pub mod requests; pub mod responses; use std::net::IpAddr; +use std::sync::Arc; +use std::time::Duration; +use hyper::StatusCode; use requests::announce::{self, Query}; use requests::scrape; use reqwest::{Client as ReqwestClient, Response, Url}; +use thiserror::Error; use crate::core::auth::Key; +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("Failed to Build a Http Client: {err:?}")] + ClientBuildingError { err: Arc }, + #[error("Failed to get a response: {err:?}")] + ResponseError { err: Arc }, + #[error("Returned a non-success code: \"{code}\" with the response: \"{response:?}\"")] + UnsuccessfulResponse { code: StatusCode, response: Arc }, +} + /// HTTP Tracker Client pub struct Client { + client: ReqwestClient, base_url: Url, - reqwest: ReqwestClient, key: Option, } @@ -25,78 +39,144 @@ pub struct Client { /// base url path query /// ``` impl Client { - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn new(base_url: Url) -> Self { - Self { + pub fn new(base_url: Url, timeout: Duration) -> Result { + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|e| Error::ClientBuildingError { err: e.into() })?; + + Ok(Self { base_url, - reqwest: reqwest::Client::builder().build().unwrap(), + client, key: None, - } + }) } /// Creates the new client binding it to an specific local address. /// - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn bind(base_url: Url, local_address: IpAddr) -> Self { - Self { + pub fn bind(base_url: Url, timeout: Duration, local_address: IpAddr) -> Result { + let client = reqwest::Client::builder() + .timeout(timeout) + .local_address(local_address) + .build() + .map_err(|e| Error::ClientBuildingError { err: e.into() })?; + + Ok(Self { base_url, - reqwest: reqwest::Client::builder().local_address(local_address).build().unwrap(), + client, key: None, - } + }) } - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn authenticated(base_url: Url, key: Key) -> Self { - Self { + pub fn authenticated(base_url: Url, key: Key, timeout: Duration) -> Result { + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|e| Error::ClientBuildingError { err: e.into() })?; + + Ok(Self { base_url, - reqwest: reqwest::Client::builder().build().unwrap(), + client, key: Some(key), - } + }) } - pub async fn announce(&self, query: &announce::Query) -> Response { - self.get(&self.build_announce_path_and_query(query)).await + /// # Errors + /// + /// This method fails if the returned response was not successful + pub async fn announce(&self, query: &announce::Query) -> Result { + let response = self.get(&self.build_announce_path_and_query(query)).await?; + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::UnsuccessfulResponse { + code: response.status(), + response: response.into(), + }) + } } - pub async fn scrape(&self, query: &scrape::Query) -> Response { - self.get(&self.build_scrape_path_and_query(query)).await + /// # Errors + /// + /// This method fails if the returned response was not successful + pub async fn scrape(&self, query: &scrape::Query) -> Result { + let response = self.get(&self.build_scrape_path_and_query(query)).await?; + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::UnsuccessfulResponse { + code: response.status(), + response: response.into(), + }) + } } - pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Response { - self.get_with_header(&self.build_announce_path_and_query(query), key, value) - .await + /// # Errors + /// + /// This method fails if the returned response was not successful + pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Result { + let response = self + .get_with_header(&self.build_announce_path_and_query(query), key, value) + .await?; + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::UnsuccessfulResponse { + code: response.status(), + response: response.into(), + }) + } } - pub async fn health_check(&self) -> Response { - self.get(&self.build_path("health_check")).await + /// # Errors + /// + /// This method fails if the returned response was not successful + pub async fn health_check(&self) -> Result { + let response = self.get(&self.build_path("health_check")).await?; + + if response.status().is_success() { + Ok(response) + } else { + Err(Error::UnsuccessfulResponse { + code: response.status(), + response: response.into(), + }) + } } - /// # Panics + /// # Errors /// /// This method fails if there was an error while sending request. - pub async fn get(&self, path: &str) -> Response { - self.reqwest.get(self.build_url(path)).send().await.unwrap() + pub async fn get(&self, path: &str) -> Result { + self.client + .get(self.build_url(path)) + .send() + .await + .map_err(|e| Error::ResponseError { err: e.into() }) } - /// # Panics + /// # Errors /// /// This method fails if there was an error while sending request. - pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Response { - self.reqwest + pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Result { + self.client .get(self.build_url(path)) .header(key, value) .send() .await - .unwrap() + .map_err(|e| Error::ResponseError { err: e.into() }) } fn build_announce_path_and_query(&self, query: &announce::Query) -> String { diff --git a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs index 25a2f0a81..0cb7eeb17 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs @@ -5,6 +5,7 @@ use std::str; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_bencode::value::Value; +use thiserror::Error; use crate::shared::bit_torrent::tracker::http::{ByteArray20, InfoHash}; @@ -105,11 +106,15 @@ impl ResponseBuilder { } } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum BencodeParseError { + #[error("Invalid Value in Dictionary: {value:?}")] InvalidValueExpectedDict { value: Value }, + #[error("Invalid Value in Integer: {value:?}")] InvalidValueExpectedInt { value: Value }, + #[error("Invalid File Field: {value:?}")] InvalidFileField { value: Value }, + #[error("Missing File Field: {field_name}")] MissingFileField { field_name: String }, } From d67bae08cbe286c69c8ffc225ac5d43a4b9478e8 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Fri, 29 Mar 2024 04:57:50 +0800 Subject: [PATCH 002/217] dev: some basic cleanup for the http client --- packages/primitives/src/info_hash.rs | 55 +++++---- src/console/clients/checker/checks/http.rs | 66 ++--------- src/console/clients/checker/service.rs | 3 +- src/console/clients/http/app.rs | 46 +++----- src/console/clients/http/mod.rs | 69 +++++++++++ src/servers/udp/logging.rs | 2 +- .../bit_torrent/tracker/http/client/mod.rs | 16 ++- .../tracker/http/client/requests/announce.rs | 110 +++++++++--------- .../tracker/http/client/requests/mod.rs | 14 +++ .../tracker/http/client/requests/scrape.rs | 76 +++++++----- .../tracker/http/client/responses/announce.rs | 37 +++++- .../tracker/http/client/responses/error.rs | 2 +- .../tracker/http/client/responses/mod.rs | 30 +++++ .../tracker/http/client/responses/scrape.rs | 97 ++++++++------- src/shared/bit_torrent/tracker/http/mod.rs | 19 +-- 15 files changed, 370 insertions(+), 272 deletions(-) diff --git a/packages/primitives/src/info_hash.rs b/packages/primitives/src/info_hash.rs index a07cc41a2..8059ba047 100644 --- a/packages/primitives/src/info_hash.rs +++ b/packages/primitives/src/info_hash.rs @@ -3,29 +3,18 @@ use std::panic::Location; use thiserror::Error; +pub type ByteArray20 = [u8; 20]; + /// `BitTorrent` Info Hash v1 #[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] -pub struct InfoHash(pub [u8; 20]); +pub struct InfoHash(pub ByteArray20); pub const INFO_HASH_BYTES_LEN: usize = 20; impl InfoHash { - /// Create a new `InfoHash` from a byte slice. - /// - /// # Panics - /// - /// Will panic if byte slice does not contains the exact amount of bytes need for the `InfoHash`. - #[must_use] - pub fn from_bytes(bytes: &[u8]) -> Self { - assert_eq!(bytes.len(), INFO_HASH_BYTES_LEN); - let mut ret = Self([0u8; INFO_HASH_BYTES_LEN]); - ret.0.clone_from_slice(bytes); - ret - } - /// Returns the `InfoHash` internal byte array. #[must_use] - pub fn bytes(&self) -> [u8; 20] { + pub fn bytes(&self) -> ByteArray20 { self.0 } @@ -57,14 +46,30 @@ impl std::fmt::Display for InfoHash { } impl std::str::FromStr for InfoHash { - type Err = binascii::ConvertError; + type Err = ConversionError; fn from_str(s: &str) -> Result { + const INFO_HASH_CHAR_LEN: usize = INFO_HASH_BYTES_LEN * 2; let mut i = Self([0u8; 20]); - if s.len() != 40 { - return Err(binascii::ConvertError::InvalidInputLength); + + if s.len() < INFO_HASH_CHAR_LEN { + return Err(ConversionError::NotEnoughBytes { + location: Location::caller(), + message: format! {"got {} string, expected {}", s.len(), INFO_HASH_CHAR_LEN}, + }); } - binascii::hex2bin(s.as_bytes(), &mut i.0)?; + if s.len() > INFO_HASH_CHAR_LEN { + return Err(ConversionError::TooManyBytes { + location: Location::caller(), + message: format! {"got {} string, expected {}", s.len(), INFO_HASH_CHAR_LEN}, + }); + } + + binascii::hex2bin(s.as_bytes(), &mut i.0).map_err(|e| ConversionError::HexToBinError { + location: Location::caller(), + message: format! {"got {e:?} error"}, + })?; + Ok(i) } } @@ -96,8 +101,8 @@ impl std::convert::From<&i32> for InfoHash { } } -impl std::convert::From<[u8; 20]> for InfoHash { - fn from(val: [u8; 20]) -> Self { +impl std::convert::From for InfoHash { + fn from(val: ByteArray20) -> Self { InfoHash(val) } } @@ -117,6 +122,12 @@ pub enum ConversionError { location: &'static Location<'static>, message: String, }, + + #[error("hex to bin didn't parse: {message} {location}")] + HexToBinError { + location: &'static Location<'static>, + message: String, + }, } impl TryFrom> for InfoHash { @@ -135,7 +146,7 @@ impl TryFrom> for InfoHash { message: format! {"got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN}, }); } - Ok(Self::from_bytes(&bytes)) + Ok((*bytes).into()) } } diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index 046963dd5..61b3f961f 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,41 +1,16 @@ -use std::str::FromStr; -use std::sync::Arc; +use std::str::FromStr as _; use std::time::Duration; -use anyhow::Result; use colored::Colorize; -use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; +use crate::console::clients; use crate::console::clients::checker::console::Console; use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; -use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; -use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce; -use crate::shared::bit_torrent::tracker::http::client::responses::scrape; -use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; - -#[derive(Debug, Clone, Error)] -pub enum Error { - #[error("Http request did not receive a response within the timeout: {err:?}")] - HttpClientError { - err: crate::shared::bit_torrent::tracker::http::client::Error, - }, - #[error("Http failed to get a response at all: {err:?}")] - ResponseError { err: Arc }, - #[error("Failed to deserialize the serde bencoded response data with the error: \"{err:?}\"")] - ParseSerdeBencodeError { - data: hyper::body::Bytes, - err: Arc, - }, - - #[error("Failed to deserialize the bencoded response data with the error: \"{err:?}\"")] - ParseScrapeBencodeError { - data: hyper::body::Bytes, - err: Arc, - }, -} +use crate::console::clients::http::Error; +use crate::shared::bit_torrent::tracker::http::client::responses; pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) -> Vec { let mut check_results = Vec::default(); @@ -71,37 +46,16 @@ pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) - check_results } -async fn check_http_announce(url: &Url, timeout: Duration) -> Result { +async fn check_http_announce(url: &Url, timeout: Duration) -> Result { let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237 let info_hash = InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required"); - let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; - - let response = client - .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await - .map_err(|err| Error::HttpClientError { err })?; - - let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; - - serde_bencode::from_bytes::(&body).map_err(|e| Error::ParseSerdeBencodeError { - data: body, - err: e.into(), - }) + clients::http::check_http_announce(url, timeout, info_hash).await } -async fn check_http_scrape(url: &Url, timeout: Duration) -> Result { - let info_hashes: Vec = vec!["9c38422213e30bff212b30c360d26f9a02136422".to_string()]; // # DevSkim: ignore DS173237 - let query = requests::scrape::Query::try_from(info_hashes).expect("a valid array of info-hashes is required"); - - let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; - - let response = client.scrape(&query).await.map_err(|err| Error::HttpClientError { err })?; - - let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; +async fn check_http_scrape(url: &Url, timeout: Duration) -> Result { + let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237 + let info_hashes = vec![InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required")]; // # DevSkim: ignore DS173237 - scrape::Response::try_from_bencoded(&body).map_err(|e| Error::ParseScrapeBencodeError { - data: body, - err: e.into(), - }) + clients::http::check_http_scrape(url, timeout, &info_hashes).await } diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index d484c560a..8ec5a4623 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -10,6 +10,7 @@ use super::checks; use super::config::Configuration; use super::console::Console; use crate::console::clients::checker::printer::Printer; +use crate::console::clients::http; pub struct Service { pub(crate) config: Arc, @@ -23,7 +24,7 @@ pub enum CheckError { #[error("Error In Udp: socket: {socket_addr:?}")] UdpError { socket_addr: SocketAddr }, #[error("Error In Http: url: {url:?}")] - HttpCheckError { url: Url, err: checks::http::Error }, + HttpCheckError { url: Url, err: http::Error }, #[error("Error In HeathCheck: url: {url:?}")] HealthCheckError { url: Url, err: checks::health::Error }, } diff --git a/src/console/clients/http/app.rs b/src/console/clients/http/app.rs index b183be7ac..e184fcd14 100644 --- a/src/console/clients/http/app.rs +++ b/src/console/clients/http/app.rs @@ -16,15 +16,12 @@ use std::str::FromStr; use std::time::Duration; -use anyhow::Context; +use anyhow::{bail, Context}; use clap::{Parser, Subcommand}; use reqwest::Url; use torrust_tracker_primitives::info_hash::InfoHash; -use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; -use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce; -use crate::shared::bit_torrent::tracker::http::client::responses::scrape; -use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; +use crate::console::clients::http::{check_http_announce, check_http_scrape}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -64,22 +61,14 @@ pub async fn run() -> anyhow::Result<()> { } async fn announce_command(tracker_url: String, timeout: Duration, info_hash: String) -> anyhow::Result<()> { - let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; - let info_hash = - InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); + let url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; + let info_hash = InfoHash::from_str(&info_hash).context("Unable to parse info_hash provided as a string")?; - let response = Client::new(base_url, timeout)? - .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await?; + let response = check_http_announce(&url, timeout, info_hash) + .await + .context("it should get a announce response")?; - let body = response.bytes().await.context("it should get back a valid response")?; - - let announce_response: Announce = serde_bencode::from_bytes(&body).context(format!( - "response body should be a valid announce response, got: \"{:#?}\"", - &body - ))?; - - let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?; + let json = serde_json::to_string(&response).context("failed to serialize scrape response into JSON")?; println!("{json}"); @@ -87,20 +76,19 @@ async fn announce_command(tracker_url: String, timeout: Duration, info_hash: Str } async fn scrape_command(tracker_url: &str, timeout: Duration, info_hashes: &[String]) -> anyhow::Result<()> { - let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; + let i = info_hashes.iter().map(|s| InfoHash::from_str(s)); - let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; - - let response = Client::new(base_url, timeout)?.scrape(&query).await?; + if i.clone().any(|i| i.is_err()) { + bail!("supplied bad infohash: {:?}", i); + } - let body = response.bytes().await.context("it should get back a valid response")?; + let url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; - let scrape_response = scrape::Response::try_from_bencoded(&body).context(format!( - "response body should be a valid scrape response, got: \"{:#?}\"", - &body - ))?; + let response = check_http_scrape(&url, timeout, &i.flatten().collect::>()) + .await + .context("it should get the scrape result")?; - let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; + let json = serde_json::to_string(&response).context("failed to serialize scrape response into JSON")?; println!("{json}"); diff --git a/src/console/clients/http/mod.rs b/src/console/clients/http/mod.rs index 309be6287..b70e7b60a 100644 --- a/src/console/clients/http/mod.rs +++ b/src/console/clients/http/mod.rs @@ -1 +1,70 @@ +use std::sync::Arc; +use std::time::Duration; + +use thiserror::Error; +use torrust_tracker_primitives::info_hash::InfoHash; +use url::Url; + +use crate::shared::bit_torrent::tracker::http::client::requests::{announce, scrape}; +use crate::shared::bit_torrent::tracker::http::client::{responses, Client}; + pub mod app; + +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("Http request did not receive a response within the timeout: {err:?}")] + HttpClientError { + err: crate::shared::bit_torrent::tracker::http::client::Error, + }, + #[error("Http failed to get a response at all: {err:?}")] + ResponseError { err: Arc }, + + #[error("Failed to deserialize the bencoded response data with the error: \"{err:?}\"")] + ParseBencodeError { + data: hyper::body::Bytes, + err: responses::BencodeParseError, + }, +} + +/// . +/// +/// # Errors +/// +/// This function will return an error if . +pub async fn check_http_announce(url: &Url, timeout: Duration, info_hash: InfoHash) -> Result { + let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; + + let response = client + .announce( + &announce::QueryBuilder::with_default_values() + .with_info_hash(&info_hash) + .build(), + ) + .await + .map_err(|err| Error::HttpClientError { err })?; + + let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; + + responses::announce::ResponseBuilder::try_from(&body) + .map_err(|err| Error::ParseBencodeError { data: body, err }) + .map(responses::announce::ResponseBuilder::build) +} + +/// . +/// +/// # Errors +/// +/// This function will return an error if . +pub async fn check_http_scrape(url: &Url, timeout: Duration, info_hashes: &[InfoHash]) -> Result { + let query = info_hashes.iter().copied().collect::().build(); + + let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; + + let response = client.scrape(&query).await.map_err(|err| Error::HttpClientError { err })?; + + let body = response.bytes().await.map_err(|e| Error::ResponseError { err: e.into() })?; + + responses::scrape::ResponseBuilder::try_from(&body) + .map_err(|err| Error::ParseBencodeError { data: body, err }) + .map(responses::scrape::ResponseBuilder::build) +} diff --git a/src/servers/udp/logging.rs b/src/servers/udp/logging.rs index 9bbb48f6a..13a7b3eeb 100644 --- a/src/servers/udp/logging.rs +++ b/src/servers/udp/logging.rs @@ -24,7 +24,7 @@ pub fn log_request(request: &Request, request_id: &RequestId, server_socket_addr let transaction_id = announce_request.transaction_id; let transaction_id_str = transaction_id.0.to_string(); let connection_id_str = announce_request.connection_id.0.to_string(); - let info_hash_str = InfoHash::from_bytes(&announce_request.info_hash.0).to_hex_string(); + let info_hash_str = InfoHash::from(announce_request.info_hash.0).to_hex_string(); tracing::span!( target: "UDP TRACKER", diff --git a/src/shared/bit_torrent/tracker/http/client/mod.rs b/src/shared/bit_torrent/tracker/http/client/mod.rs index 4cddc98f4..506126427 100644 --- a/src/shared/bit_torrent/tracker/http/client/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/mod.rs @@ -6,9 +6,7 @@ use std::sync::Arc; use std::time::Duration; use hyper::StatusCode; -use requests::announce::{self, Query}; -use requests::scrape; -use reqwest::{Client as ReqwestClient, Response, Url}; +use reqwest::{Response, Url}; use thiserror::Error; use crate::core::auth::Key; @@ -25,7 +23,7 @@ pub enum Error { /// HTTP Tracker Client pub struct Client { - client: ReqwestClient, + client: reqwest::Client, base_url: Url, key: Option, } @@ -93,7 +91,7 @@ impl Client { /// # Errors /// /// This method fails if the returned response was not successful - pub async fn announce(&self, query: &announce::Query) -> Result { + pub async fn announce(&self, query: &requests::Announce) -> Result { let response = self.get(&self.build_announce_path_and_query(query)).await?; if response.status().is_success() { @@ -109,7 +107,7 @@ impl Client { /// # Errors /// /// This method fails if the returned response was not successful - pub async fn scrape(&self, query: &scrape::Query) -> Result { + pub async fn scrape(&self, query: &requests::Scrape) -> Result { let response = self.get(&self.build_scrape_path_and_query(query)).await?; if response.status().is_success() { @@ -125,7 +123,7 @@ impl Client { /// # Errors /// /// This method fails if the returned response was not successful - pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Result { + pub async fn announce_with_header(&self, query: &requests::Announce, key: &str, value: &str) -> Result { let response = self .get_with_header(&self.build_announce_path_and_query(query), key, value) .await?; @@ -179,11 +177,11 @@ impl Client { .map_err(|e| Error::ResponseError { err: e.into() }) } - fn build_announce_path_and_query(&self, query: &announce::Query) -> String { + fn build_announce_path_and_query(&self, query: &requests::Announce) -> String { format!("{}?{query}", self.build_path("announce")) } - fn build_scrape_path_and_query(&self, query: &scrape::Query) -> String { + fn build_scrape_path_and_query(&self, query: &requests::Scrape) -> String { format!("{}?{query}", self.build_path("scrape")) } diff --git a/src/shared/bit_torrent/tracker/http/client/requests/announce.rs b/src/shared/bit_torrent/tracker/http/client/requests/announce.rs index b872e76e9..4fbbb617d 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/announce.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/announce.rs @@ -6,9 +6,11 @@ use serde_repr::Serialize_repr; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; +use super::Announce; use crate::shared::bit_torrent::tracker::http::{percent_encode_byte_array, ByteArray20}; -pub struct Query { +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(super) struct Query { pub info_hash: ByteArray20, pub peer_addr: IpAddr, pub downloaded: BaseTenASCII, @@ -22,36 +24,14 @@ pub struct Query { impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.build()) - } -} - -/// HTTP Tracker Announce Request: -/// -/// -/// -/// Some parameters in the specification are not implemented in this tracker yet. -impl Query { - /// It builds the URL query component for the announce request. - /// - /// This custom URL query params encoding is needed because `reqwest` does not allow - /// bytes arrays in query parameters. More info on this issue: - /// - /// - #[must_use] - pub fn build(&self) -> String { - self.params().to_string() - } - - #[must_use] - pub fn params(&self) -> QueryParams { - QueryParams::from(self) + write!(f, "{}", QueryParams::from(self)) } } pub type BaseTenASCII = u64; pub type PortNumber = u16; +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Event { //Started, //Stopped, @@ -68,7 +48,7 @@ impl fmt::Display for Event { } } -#[derive(Serialize_repr, PartialEq, Debug)] +#[derive(Serialize_repr, PartialEq, Eq, Debug, Clone, Copy)] #[repr(u8)] pub enum Compact { Accepted = 1, @@ -85,7 +65,7 @@ impl fmt::Display for Compact { } pub struct QueryBuilder { - announce_query: Query, + query: Query, } impl QueryBuilder { @@ -94,55 +74,54 @@ impl QueryBuilder { /// Will panic if the default info-hash value is not a valid info-hash. #[must_use] pub fn with_default_values() -> QueryBuilder { - let default_announce_query = Query { - info_hash: InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0, // # DevSkim: ignore DS173237 - peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)), - downloaded: 0, - uploaded: 0, - peer_id: peer::Id(*b"-qB00000000000000001").0, - port: 17548, - left: 0, - event: Some(Event::Completed), - compact: Some(Compact::NotAccepted), - }; Self { - announce_query: default_announce_query, + query: Query { + info_hash: InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0, // # DevSkim: ignore DS173237 + peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)), + downloaded: 0, + uploaded: 0, + peer_id: peer::Id(*b"-qB00000000000000001").0, + port: 17548, + left: 0, + event: Some(Event::Completed), + compact: Some(Compact::NotAccepted), + }, } } #[must_use] pub fn with_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.announce_query.info_hash = info_hash.0; + self.query.info_hash = info_hash.0; self } #[must_use] pub fn with_peer_id(mut self, peer_id: &peer::Id) -> Self { - self.announce_query.peer_id = peer_id.0; + self.query.peer_id = peer_id.0; self } #[must_use] pub fn with_compact(mut self, compact: Compact) -> Self { - self.announce_query.compact = Some(compact); + self.query.compact = Some(compact); self } #[must_use] pub fn with_peer_addr(mut self, peer_addr: &IpAddr) -> Self { - self.announce_query.peer_addr = *peer_addr; + self.query.peer_addr = *peer_addr; self } #[must_use] pub fn without_compact(mut self) -> Self { - self.announce_query.compact = None; + self.query.compact = None; self } #[must_use] - pub fn query(self) -> Query { - self.announce_query + pub fn build(self) -> Announce { + self.query.into() } } @@ -174,6 +153,12 @@ pub struct QueryParams { pub compact: Option, } +/// It builds the URL query component for the announce request. +/// +/// This custom URL query params encoding is needed because `reqwest` does not allow +/// bytes arrays in query parameters. More info on this issue: +/// +/// impl std::fmt::Display for QueryParams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut params = vec![]; @@ -216,24 +201,35 @@ impl std::fmt::Display for QueryParams { } } -impl QueryParams { - pub fn from(announce_query: &Query) -> Self { - let event = announce_query.event.as_ref().map(std::string::ToString::to_string); - let compact = announce_query.compact.as_ref().map(std::string::ToString::to_string); +impl From<&Announce> for QueryParams { + fn from(value: &Announce) -> Self { + let query: &Query = &Announce::into(*value); + query.into() + } +} + +impl From<&Query> for QueryParams { + fn from(value: &Query) -> Self { + let query = value; + + let event = query.event.as_ref().map(std::string::ToString::to_string); + let compact = query.compact.as_ref().map(std::string::ToString::to_string); Self { - info_hash: Some(percent_encode_byte_array(&announce_query.info_hash)), - peer_addr: Some(announce_query.peer_addr.to_string()), - downloaded: Some(announce_query.downloaded.to_string()), - uploaded: Some(announce_query.uploaded.to_string()), - peer_id: Some(percent_encode_byte_array(&announce_query.peer_id)), - port: Some(announce_query.port.to_string()), - left: Some(announce_query.left.to_string()), + info_hash: Some(percent_encode_byte_array(&query.info_hash)), + peer_addr: Some(query.peer_addr.to_string()), + downloaded: Some(query.downloaded.to_string()), + uploaded: Some(query.uploaded.to_string()), + peer_id: Some(percent_encode_byte_array(&query.peer_id)), + port: Some(query.port.to_string()), + left: Some(query.left.to_string()), event, compact, } } +} +impl QueryParams { pub fn remove_optional_params(&mut self) { // todo: make them optional with the Option<...> in the AnnounceQuery struct // if they are really optional. So that we can crete a minimal AnnounceQuery diff --git a/src/shared/bit_torrent/tracker/http/client/requests/mod.rs b/src/shared/bit_torrent/tracker/http/client/requests/mod.rs index 776d2dfbf..ceb3ed5a1 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/mod.rs @@ -1,2 +1,16 @@ pub mod announce; pub mod scrape; + +use derive_more::{Display, From, Into}; + +#[derive(Debug, Display, From, Into, PartialEq, Eq, Clone, Copy)] +#[display(fmt = "{query}")] +pub struct Announce { + query: announce::Query, +} + +#[derive(Debug, Display, From, Into, PartialEq, Eq, Clone)] +#[display(fmt = "{query}")] +pub struct Scrape { + query: scrape::Query, +} diff --git a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs index 4d12fc2d2..aa44ebd2a 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs @@ -4,10 +4,12 @@ use std::str::FromStr; use torrust_tracker_primitives::info_hash::InfoHash; +use super::Scrape; use crate::shared::bit_torrent::tracker::http::{percent_encode_byte_array, ByteArray20}; +#[derive(Default, Debug, PartialEq, Eq, Clone)] pub struct Query { - pub info_hash: Vec, + pub infohashes: Vec, } impl fmt::Display for Query { @@ -28,20 +30,29 @@ impl fmt::Display for ConversionError { impl Error for ConversionError {} +impl FromIterator for Query { + fn from_iter>(iter: T) -> Self { + let mut infohashes = Vec::default(); + + for infohash in iter { + infohashes.push(infohash.0); + } + + Query { infohashes } + } +} + impl TryFrom<&[String]> for Query { type Error = ConversionError; - fn try_from(info_hashes: &[String]) -> Result { - let mut validated_info_hashes: Vec = Vec::new(); + fn try_from(values: &[String]) -> Result { + let mut infohashes = Vec::default(); - for info_hash in info_hashes { - let validated_info_hash = InfoHash::from_str(info_hash).map_err(|_| ConversionError(info_hash.clone()))?; - validated_info_hashes.push(validated_info_hash.0); + for i in values { + infohashes.push(InfoHash::from_str(i).map_err(|_| ConversionError(i.clone()))?); } - Ok(Self { - info_hash: validated_info_hashes, - }) + Ok(Query::from_iter(infohashes)) } } @@ -57,7 +68,7 @@ impl TryFrom> for Query { } Ok(Self { - info_hash: validated_info_hashes, + infohashes: validated_info_hashes, }) } } @@ -66,12 +77,6 @@ impl TryFrom> for Query { /// /// impl Query { - /// It builds the URL query component for the scrape request. - /// - /// This custom URL query params encoding is needed because `reqwest` does not allow - /// bytes arrays in query parameters. More info on this issue: - /// - /// #[must_use] pub fn build(&self) -> String { self.params().to_string() @@ -83,17 +88,15 @@ impl Query { } } +#[derive(Default)] pub struct QueryBuilder { scrape_query: Query, } -impl Default for QueryBuilder { - fn default() -> Self { - let default_scrape_query = Query { - info_hash: [InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0].to_vec(), // # DevSkim: ignore DS173237 - }; +impl FromIterator for QueryBuilder { + fn from_iter>(iter: T) -> Self { Self { - scrape_query: default_scrape_query, + scrape_query: Query::from_iter(iter), } } } @@ -101,19 +104,19 @@ impl Default for QueryBuilder { impl QueryBuilder { #[must_use] pub fn with_one_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.info_hash = [info_hash.0].to_vec(); + self.scrape_query.infohashes = [info_hash.0].to_vec(); self } #[must_use] pub fn add_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.info_hash.push(info_hash.0); + self.scrape_query.infohashes.push(info_hash.0); self } #[must_use] - pub fn query(self) -> Query { - self.scrape_query + pub fn build(self) -> Scrape { + self.scrape_query.into() } } @@ -146,6 +149,12 @@ impl QueryParams { } } +/// It builds the URL query component for the scrape request. +/// +/// This custom URL query params encoding is needed because `reqwest` does not allow +/// bytes arrays in query parameters. More info on this issue: +/// +/// impl std::fmt::Display for QueryParams { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let query = self @@ -159,10 +168,17 @@ impl std::fmt::Display for QueryParams { } } -impl QueryParams { - pub fn from(scrape_query: &Query) -> Self { - let info_hashes = scrape_query - .info_hash +impl From for QueryParams { + fn from(value: Scrape) -> Self { + let query: &Query = &Scrape::into(value); + query.into() + } +} + +impl From<&Query> for QueryParams { + fn from(value: &Query) -> Self { + let info_hashes = value + .infohashes .iter() .map(percent_encode_byte_array) .collect::>(); diff --git a/src/shared/bit_torrent/tracker/http/client/responses/announce.rs b/src/shared/bit_torrent/tracker/http/client/responses/announce.rs index 15ec446cb..5861df10a 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/announce.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/announce.rs @@ -1,10 +1,13 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use axum::body::Bytes; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::peer; -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Announce { +use super::{Announce, BencodeParseError}; + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)] +pub(super) struct Response { pub complete: u32, pub incomplete: u32, pub interval: u32, @@ -13,7 +16,35 @@ pub struct Announce { pub peers: Vec, // Peers using IPV4 and IPV6 } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Default)] +pub struct ResponseBuilder { + response: Response, +} + +impl TryFrom<&Bytes> for ResponseBuilder { + type Error = BencodeParseError; + + /// # Errors + /// + /// Will return an error if the deserialized bencoded response can't not be converted into a valid response. + fn try_from(value: &Bytes) -> Result { + let response: Response = serde_bencode::from_bytes(value).map_err(|e| BencodeParseError::ParseSerdeBencodeError { + data: value.to_vec(), + err: e.into(), + })?; + + Ok(Self { response }) + } +} + +impl ResponseBuilder { + #[must_use] + pub fn build(self) -> Announce { + self.response.into() + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct DictionaryPeer { pub ip: String, #[serde(rename = "peer id")] diff --git a/src/shared/bit_torrent/tracker/http/client/responses/error.rs b/src/shared/bit_torrent/tracker/http/client/responses/error.rs index 00befdb54..c27d807c0 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/error.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/error.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct Error { #[serde(rename = "failure reason")] pub failure_reason: String, diff --git a/src/shared/bit_torrent/tracker/http/client/responses/mod.rs b/src/shared/bit_torrent/tracker/http/client/responses/mod.rs index bdc689056..64fe646a0 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/mod.rs @@ -1,3 +1,33 @@ +use std::sync::Arc; + +use derive_more::From; +use serde::Serialize; +use thiserror::Error; + pub mod announce; pub mod error; pub mod scrape; + +#[derive(Serialize, Debug, From, PartialEq, Eq, Clone)] +pub struct Announce { + response: announce::Response, +} + +#[derive(Serialize, Debug, From, PartialEq, Eq, Clone)] +pub struct Scrape { + response: scrape::Response, +} + +#[derive(Debug, Error, Clone)] +pub enum BencodeParseError { + #[error("Invalid Value in Dictionary: {value:?}")] + InvalidValueExpectedDict { value: serde_bencode::value::Value }, + #[error("Invalid Value in Integer: {value:?}")] + InvalidValueExpectedInt { value: serde_bencode::value::Value }, + #[error("Invalid File Field: {value:?}")] + InvalidFileField { value: serde_bencode::value::Value }, + #[error("Missing File Field: {field_name}")] + MissingFileField { field_name: String }, + #[error("Failed to deserialize the serde bencoded response data with the error: \"{err:?}\"")] + ParseSerdeBencodeError { data: Vec, err: Arc }, +} diff --git a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs index 0cb7eeb17..8fe65a036 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs @@ -2,41 +2,21 @@ use std::collections::HashMap; use std::fmt::Write; use std::str; +use axum::body::Bytes; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_bencode::value::Value; -use thiserror::Error; +use torrust_tracker_primitives::info_hash::InfoHash; -use crate::shared::bit_torrent::tracker::http::{ByteArray20, InfoHash}; +use super::{BencodeParseError, Scrape}; +use crate::shared::bit_torrent::tracker::http::ByteArray20; -#[derive(Debug, PartialEq, Default, Deserialize)] -pub struct Response { +#[derive(Debug, PartialEq, Eq, Default, Deserialize, Clone)] +pub(super) struct Response { pub files: HashMap, } -impl Response { - #[must_use] - pub fn with_one_file(info_hash_bytes: ByteArray20, file: File) -> Self { - let mut files: HashMap = HashMap::new(); - files.insert(info_hash_bytes, file); - Self { files } - } - - /// # Errors - /// - /// Will return an error if the deserialized bencoded response can't not be converted into a valid response. - /// - /// # Panics - /// - /// Will panic if it can't deserialize the bencoded response. - pub fn try_from_bencoded(bytes: &[u8]) -> Result { - let scrape_response: DeserializedResponse = - serde_bencode::from_bytes(bytes).expect("provided bytes should be a valid bencoded response"); - Self::try_from(scrape_response) - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone, Copy)] pub struct File { pub complete: i64, // The number of active peers that have completed downloading pub downloaded: i64, // The number of peers that have ever completed downloading @@ -88,36 +68,56 @@ fn byte_array_to_hex_string(byte_array: &ByteArray20) -> String { hex_string } +pub type OneFile = (ByteArray20, File); + #[derive(Default)] pub struct ResponseBuilder { response: Response, } +impl From for ResponseBuilder { + fn from((infohash, file): OneFile) -> Self { + let mut files: HashMap = HashMap::new(); + files.insert(infohash, file); + + Self { + response: Response { files }, + } + } +} + +impl TryFrom<&Bytes> for ResponseBuilder { + type Error = BencodeParseError; + + /// # Errors + /// + /// Will return an error if the deserialized bencoded response can't not be converted into a valid response. + fn try_from(value: &Bytes) -> Result { + let scrape_response: DeserializedResponse = + serde_bencode::from_bytes(value).map_err(|e| BencodeParseError::ParseSerdeBencodeError { + data: value.to_vec(), + err: e.into(), + })?; + + Ok(Self { + response: Response::try_from(scrape_response)?, + }) + } +} + impl ResponseBuilder { #[must_use] - pub fn add_file(mut self, info_hash_bytes: ByteArray20, file: File) -> Self { - self.response.files.insert(info_hash_bytes, file); + pub fn add_file(mut self, (infohash, file): OneFile) -> Self { + self.response.files.insert(infohash, file); self } #[must_use] - pub fn build(self) -> Response { - self.response + pub fn build(self) -> Scrape { + self.response.into() } } -#[derive(Debug, Error)] -pub enum BencodeParseError { - #[error("Invalid Value in Dictionary: {value:?}")] - InvalidValueExpectedDict { value: Value }, - #[error("Invalid Value in Integer: {value:?}")] - InvalidValueExpectedInt { value: Value }, - #[error("Invalid File Field: {value:?}")] - InvalidFileField { value: Value }, - #[error("Missing File Field: {field_name}")] - MissingFileField { field_name: String }, -} - /// It parses a bencoded scrape response into a `Response` struct. /// /// For example: @@ -145,9 +145,16 @@ fn parse_bencoded_response(value: &Value) -> Result let info_hash_byte_vec = file_element.0; let file_value = file_element.1; - let file = parse_bencoded_file(file_value).unwrap(); + let file = parse_bencoded_file(file_value)?; - files.insert(InfoHash::new(info_hash_byte_vec).bytes(), file); + files.insert( + InfoHash::try_from(info_hash_byte_vec.clone()) + .map_err(|_| BencodeParseError::InvalidFileField { + value: Value::Bytes(info_hash_byte_vec.clone()), + })? + .bytes(), + file, + ); } } _ => return Err(BencodeParseError::InvalidValueExpectedDict { value: value.clone() }), diff --git a/src/shared/bit_torrent/tracker/http/mod.rs b/src/shared/bit_torrent/tracker/http/mod.rs index 15723c1b7..2ae8f1285 100644 --- a/src/shared/bit_torrent/tracker/http/mod.rs +++ b/src/shared/bit_torrent/tracker/http/mod.rs @@ -1,26 +1,9 @@ pub mod client; use percent_encoding::NON_ALPHANUMERIC; - -pub type ByteArray20 = [u8; 20]; +use torrust_tracker_primitives::info_hash::ByteArray20; #[must_use] pub fn percent_encode_byte_array(bytes: &ByteArray20) -> String { percent_encoding::percent_encode(bytes, NON_ALPHANUMERIC).to_string() } - -pub struct InfoHash(ByteArray20); - -impl InfoHash { - #[must_use] - pub fn new(vec: &[u8]) -> Self { - let mut byte_array_20: ByteArray20 = Default::default(); - byte_array_20.clone_from_slice(vec); - Self(byte_array_20) - } - - #[must_use] - pub fn bytes(&self) -> ByteArray20 { - self.0 - } -} From 93b8b2a1456418c65984bdab983448db8514776f Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Fri, 29 Mar 2024 15:01:07 +0800 Subject: [PATCH 003/217] dev: use shared client requests and responses for testing --- packages/primitives/src/announce_event.rs | 7 +- packages/primitives/src/info_hash.rs | 4 +- packages/primitives/src/peer.rs | 7 +- .../tests/repository/mod.rs | 2 +- src/console/clients/http/mod.rs | 7 +- .../tracker/http/client/requests/announce.rs | 87 ++-- .../tracker/http/client/requests/scrape.rs | 22 +- .../tracker/http/client/responses/announce.rs | 26 +- .../tracker/http/client/responses/mod.rs | 8 +- .../servers/api/v1/contract/context/stats.rs | 2 +- .../api/v1/contract/context/torrent.rs | 18 +- tests/servers/http/asserts.rs | 31 +- tests/servers/http/client.rs | 14 +- tests/servers/http/mod.rs | 25 +- tests/servers/http/requests/announce.rs | 260 ----------- tests/servers/http/requests/mod.rs | 2 - tests/servers/http/requests/scrape.rs | 118 ----- tests/servers/http/responses/announce.rs | 115 ----- tests/servers/http/responses/error.rs | 7 - tests/servers/http/responses/mod.rs | 3 - tests/servers/http/responses/scrape.rs | 200 --------- tests/servers/http/v1/contract.rs | 417 ++++++++---------- 22 files changed, 311 insertions(+), 1071 deletions(-) delete mode 100644 tests/servers/http/requests/announce.rs delete mode 100644 tests/servers/http/requests/mod.rs delete mode 100644 tests/servers/http/requests/scrape.rs delete mode 100644 tests/servers/http/responses/announce.rs delete mode 100644 tests/servers/http/responses/error.rs delete mode 100644 tests/servers/http/responses/mod.rs delete mode 100644 tests/servers/http/responses/scrape.rs diff --git a/packages/primitives/src/announce_event.rs b/packages/primitives/src/announce_event.rs index 3bd560084..f6af4146c 100644 --- a/packages/primitives/src/announce_event.rs +++ b/packages/primitives/src/announce_event.rs @@ -2,19 +2,24 @@ //! //! Distributed under Apache 2.0 license +use derive_more::Display; use serde::{Deserialize, Serialize}; /// Announce events. Described on the /// [BEP 3. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) -#[derive(Hash, Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Hash, Clone, Copy, Debug, Display, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AnnounceEvent { /// The peer has started downloading the torrent. + #[display(fmt = "started")] Started, /// The peer has ceased downloading the torrent. + #[display(fmt = "stopped")] Stopped, /// The peer has completed downloading the torrent. + #[display(fmt = "completed")] Completed, /// This is one of the announcements done at regular intervals. + #[display(fmt = "")] None, } diff --git a/packages/primitives/src/info_hash.rs b/packages/primitives/src/info_hash.rs index 8059ba047..faa5236f2 100644 --- a/packages/primitives/src/info_hash.rs +++ b/packages/primitives/src/info_hash.rs @@ -94,8 +94,8 @@ impl std::convert::From<&DefaultHasher> for InfoHash { } } -impl std::convert::From<&i32> for InfoHash { - fn from(n: &i32) -> InfoHash { +impl std::convert::From for InfoHash { + fn from(n: i32) -> InfoHash { let n = n.to_le_bytes(); InfoHash([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, n[0], n[1], n[2], n[3]]) } diff --git a/packages/primitives/src/peer.rs b/packages/primitives/src/peer.rs index f5b009f2a..785818a0f 100644 --- a/packages/primitives/src/peer.rs +++ b/packages/primitives/src/peer.rs @@ -392,13 +392,14 @@ pub mod fixture { #[allow(dead_code)] #[must_use] - pub fn build(self) -> Peer { - self.into() + pub fn with_event(mut self, event: AnnounceEvent) -> Self { + self.peer.event = event; + self } #[allow(dead_code)] #[must_use] - pub fn into(self) -> Peer { + pub fn build(self) -> Peer { self.peer } } diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 7ffe17dd7..ed48c062f 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -115,7 +115,7 @@ fn many_out_of_order() -> Entries { let mut entry = EntrySingle::default(); entry.insert_or_update_peer(&a_started_peer(i)); - entries.insert((InfoHash::from(&i), entry)); + entries.insert((InfoHash::from(i), entry)); } // we keep the random order from the hashed set for the vector. diff --git a/src/console/clients/http/mod.rs b/src/console/clients/http/mod.rs index b70e7b60a..8fda21f1d 100644 --- a/src/console/clients/http/mod.rs +++ b/src/console/clients/http/mod.rs @@ -3,6 +3,7 @@ use std::time::Duration; use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::peer; use url::Url; use crate::shared::bit_torrent::tracker::http::client::requests::{announce, scrape}; @@ -35,11 +36,7 @@ pub async fn check_http_announce(url: &Url, timeout: Duration, info_hash: InfoHa let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; let response = client - .announce( - &announce::QueryBuilder::with_default_values() - .with_info_hash(&info_hash) - .build(), - ) + .announce(&announce::QueryBuilder::new(info_hash, peer::Id::from(1), 17548).build()) .await .map_err(|err| Error::HttpClientError { err })?; diff --git a/src/shared/bit_torrent/tracker/http/client/requests/announce.rs b/src/shared/bit_torrent/tracker/http/client/requests/announce.rs index 4fbbb617d..3c8583ef5 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/announce.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/announce.rs @@ -1,8 +1,8 @@ use std::fmt; -use std::net::{IpAddr, Ipv4Addr}; -use std::str::FromStr; +use std::net::IpAddr; use serde_repr::Serialize_repr; +use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; @@ -12,13 +12,13 @@ use crate::shared::bit_torrent::tracker::http::{percent_encode_byte_array, ByteA #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(super) struct Query { pub info_hash: ByteArray20, - pub peer_addr: IpAddr, - pub downloaded: BaseTenASCII, - pub uploaded: BaseTenASCII, + pub peer_addr: Option, + pub downloaded: Option, + pub uploaded: Option, pub peer_id: ByteArray20, pub port: PortNumber, - pub left: BaseTenASCII, - pub event: Option, + pub left: Option, + pub event: Option, pub compact: Option, } @@ -31,23 +31,6 @@ impl fmt::Display for Query { pub type BaseTenASCII = u64; pub type PortNumber = u16; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum Event { - //Started, - //Stopped, - Completed, -} - -impl fmt::Display for Event { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - //Event::Started => write!(f, "started"), - //Event::Stopped => write!(f, "stopped"), - Event::Completed => write!(f, "completed"), - } - } -} - #[derive(Serialize_repr, PartialEq, Eq, Debug, Clone, Copy)] #[repr(u8)] pub enum Compact { @@ -64,6 +47,7 @@ impl fmt::Display for Compact { } } +#[derive(Debug)] pub struct QueryBuilder { query: Query, } @@ -73,49 +57,43 @@ impl QueryBuilder { /// /// Will panic if the default info-hash value is not a valid info-hash. #[must_use] - pub fn with_default_values() -> QueryBuilder { + pub fn new(info_hash: InfoHash, peer_id: peer::Id, port: u16) -> QueryBuilder { Self { query: Query { - info_hash: InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0, // # DevSkim: ignore DS173237 - peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)), - downloaded: 0, - uploaded: 0, - peer_id: peer::Id(*b"-qB00000000000000001").0, - port: 17548, - left: 0, - event: Some(Event::Completed), - compact: Some(Compact::NotAccepted), + info_hash: info_hash.0, + peer_addr: None, + downloaded: None, + uploaded: None, + peer_id: peer_id.0, + port, + left: None, + event: None, + compact: None, }, } } #[must_use] - pub fn with_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.query.info_hash = info_hash.0; + pub fn with_event(mut self, event: AnnounceEvent) -> Self { + self.query.event = Some(event); self } #[must_use] - pub fn with_peer_id(mut self, peer_id: &peer::Id) -> Self { - self.query.peer_id = peer_id.0; + pub fn with_compact(mut self) -> Self { + self.query.compact = Some(Compact::Accepted); self } #[must_use] - pub fn with_compact(mut self, compact: Compact) -> Self { - self.query.compact = Some(compact); + pub fn without_compact(mut self) -> Self { + self.query.compact = Some(Compact::NotAccepted); self } #[must_use] pub fn with_peer_addr(mut self, peer_addr: &IpAddr) -> Self { - self.query.peer_addr = *peer_addr; - self - } - - #[must_use] - pub fn without_compact(mut self) -> Self { - self.query.compact = None; + self.query.peer_addr = Some(*peer_addr); self } @@ -212,19 +190,16 @@ impl From<&Query> for QueryParams { fn from(value: &Query) -> Self { let query = value; - let event = query.event.as_ref().map(std::string::ToString::to_string); - let compact = query.compact.as_ref().map(std::string::ToString::to_string); - Self { info_hash: Some(percent_encode_byte_array(&query.info_hash)), - peer_addr: Some(query.peer_addr.to_string()), - downloaded: Some(query.downloaded.to_string()), - uploaded: Some(query.uploaded.to_string()), + peer_addr: query.peer_addr.as_ref().map(std::string::ToString::to_string), + downloaded: query.downloaded.as_ref().map(std::string::ToString::to_string), + uploaded: query.uploaded.as_ref().map(std::string::ToString::to_string), peer_id: Some(percent_encode_byte_array(&query.peer_id)), port: Some(query.port.to_string()), - left: Some(query.left.to_string()), - event, - compact, + left: query.left.as_ref().map(std::string::ToString::to_string), + event: query.event.as_ref().map(std::string::ToString::to_string), + compact: query.compact.as_ref().map(std::string::ToString::to_string), } } } diff --git a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs index aa44ebd2a..1c884cd03 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs @@ -90,33 +90,37 @@ impl Query { #[derive(Default)] pub struct QueryBuilder { - scrape_query: Query, + query: Query, } impl FromIterator for QueryBuilder { fn from_iter>(iter: T) -> Self { Self { - scrape_query: Query::from_iter(iter), + query: Query::from_iter(iter), } } } -impl QueryBuilder { - #[must_use] - pub fn with_one_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.infohashes = [info_hash.0].to_vec(); - self +impl From<&InfoHash> for QueryBuilder { + fn from(value: &InfoHash) -> Self { + Self { + query: Query { + infohashes: [value.0].to_vec(), + }, + } } +} +impl QueryBuilder { #[must_use] pub fn add_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.infohashes.push(info_hash.0); + self.query.infohashes.push(info_hash.0); self } #[must_use] pub fn build(self) -> Scrape { - self.scrape_query.into() + self.query.into() } } diff --git a/src/shared/bit_torrent/tracker/http/client/responses/announce.rs b/src/shared/bit_torrent/tracker/http/client/responses/announce.rs index 5861df10a..7955a82f2 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/announce.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/announce.rs @@ -16,11 +16,35 @@ pub(super) struct Response { pub peers: Vec, // Peers using IPV4 and IPV6 } -#[derive(Default)] pub struct ResponseBuilder { response: Response, } +impl ResponseBuilder { + #[must_use] + pub fn new(policy: &torrust_tracker_configuration::AnnouncePolicy) -> Self { + Self { + response: Response { + interval: policy.interval, + min_interval: policy.interval_min, + ..Default::default() + }, + } + } + + #[must_use] + pub fn with_complete(mut self, complete: u32) -> Self { + self.response.complete = complete; + self + } + + #[must_use] + pub fn with_peers(mut self, peers: Vec) -> Self { + self.response.peers = peers; + self + } +} + impl TryFrom<&Bytes> for ResponseBuilder { type Error = BencodeParseError; diff --git a/src/shared/bit_torrent/tracker/http/client/responses/mod.rs b/src/shared/bit_torrent/tracker/http/client/responses/mod.rs index 64fe646a0..0f66a54c0 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/mod.rs @@ -1,20 +1,22 @@ use std::sync::Arc; use derive_more::From; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod announce; pub mod error; pub mod scrape; -#[derive(Serialize, Debug, From, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, From, PartialEq, Eq, Clone)] pub struct Announce { + #[serde(flatten)] response: announce::Response, } -#[derive(Serialize, Debug, From, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, From, PartialEq, Eq, Clone)] pub struct Scrape { + #[serde(flatten)] response: scrape::Response, } diff --git a/tests/servers/api/v1/contract/context/stats.rs b/tests/servers/api/v1/contract/context/stats.rs index af6587673..5794c6139 100644 --- a/tests/servers/api/v1/contract/context/stats.rs +++ b/tests/servers/api/v1/contract/context/stats.rs @@ -16,7 +16,7 @@ async fn should_allow_getting_tracker_statistics() { env.add_torrent_peer( &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), - &PeerBuilder::default().into(), + &PeerBuilder::default().build(), ) .await; diff --git a/tests/servers/api/v1/contract/context/torrent.rs b/tests/servers/api/v1/contract/context/torrent.rs index d54935f80..5e693b730 100644 --- a/tests/servers/api/v1/contract/context/torrent.rs +++ b/tests/servers/api/v1/contract/context/torrent.rs @@ -24,7 +24,7 @@ async fn should_allow_getting_all_torrents() { let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; + env.add_torrent_peer(&info_hash, &PeerBuilder::default().build()).await; let response = Client::new(env.get_connection_info()).get_torrents(Query::empty()).await; @@ -50,8 +50,8 @@ async fn should_allow_limiting_the_torrents_in_the_result() { let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await; - env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await; + env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().build()).await; + env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().build()).await; let response = Client::new(env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec())) @@ -79,8 +79,8 @@ async fn should_allow_the_torrents_result_pagination() { let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await; - env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await; + env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().build()).await; + env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().build()).await; let response = Client::new(env.get_connection_info()) .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec())) @@ -107,8 +107,8 @@ async fn should_allow_getting_a_list_of_torrents_providing_infohashes() { let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); // DevSkim: ignore DS173237 let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); // DevSkim: ignore DS173237 - env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()).await; - env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()).await; + env.add_torrent_peer(&info_hash_1, &PeerBuilder::default().build()).await; + env.add_torrent_peer(&info_hash_2, &PeerBuilder::default().build()).await; let response = Client::new(env.get_connection_info()) .get_torrents(Query::params( @@ -222,7 +222,7 @@ async fn should_allow_getting_a_torrent_info() { let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - let peer = PeerBuilder::default().into(); + let peer = PeerBuilder::default().build(); env.add_torrent_peer(&info_hash, &peer).await; @@ -285,7 +285,7 @@ async fn should_not_allow_getting_a_torrent_info_for_unauthenticated_users() { let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - env.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; + env.add_torrent_peer(&info_hash, &PeerBuilder::default().build()).await; let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str())) .get_torrent(&info_hash.to_string()) diff --git a/tests/servers/http/asserts.rs b/tests/servers/http/asserts.rs index 3a2e67bf0..8dc09101a 100644 --- a/tests/servers/http/asserts.rs +++ b/tests/servers/http/asserts.rs @@ -1,10 +1,9 @@ use std::panic::Location; use reqwest::Response; - -use super::responses::announce::{Announce, Compact, DeserializedCompact}; -use super::responses::scrape; -use crate::servers::http::responses::error::Error; +use torrust_tracker::shared::bit_torrent::tracker::http::client::responses; +use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::error::Error; +use torrust_tracker_configuration::AnnouncePolicy; pub fn assert_bencoded_error(response_text: &String, expected_failure_reason: &str, location: &'static Location<'static>) { let error_failure_reason = serde_bencode::from_str::(response_text) @@ -22,36 +21,36 @@ pub fn assert_bencoded_error(response_text: &String, expected_failure_reason: &s ); } -pub async fn assert_empty_announce_response(response: Response) { +pub async fn assert_empty_announce_response(response: Response, policy: &AnnouncePolicy) { assert_eq!(response.status(), 200); - let announce_response: Announce = serde_bencode::from_str(&response.text().await.unwrap()).unwrap(); - assert!(announce_response.peers.is_empty()); + let announce_response: responses::Announce = serde_bencode::from_str(&response.text().await.unwrap()).unwrap(); + assert_eq!(announce_response, responses::announce::ResponseBuilder::new(policy).build()); } -pub async fn assert_announce_response(response: Response, expected_announce_response: &Announce) { +pub async fn assert_announce_response(response: Response, expected_announce_response: &responses::Announce) { assert_eq!(response.status(), 200); let body = response.bytes().await.unwrap(); - let announce_response: Announce = serde_bencode::from_bytes(&body) + let announce_response: responses::Announce = serde_bencode::from_bytes(&body) .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body)); assert_eq!(announce_response, *expected_announce_response); } -pub async fn assert_compact_announce_response(response: Response, expected_response: &Compact) { +pub async fn assert_compact_announce_response(response: Response, expected_response: &responses::announce::Compact) { assert_eq!(response.status(), 200); let bytes = response.bytes().await.unwrap(); - let compact_announce = DeserializedCompact::from_bytes(&bytes).unwrap_or_else(|_| { + let compact_announce = responses::announce::DeserializedCompact::from_bytes(&bytes).unwrap_or_else(|_| { panic!( "response body should be a valid compact announce response, got \"{:?}\"", &bytes ) }); - let actual_response = Compact::from(compact_announce); + let actual_response = responses::announce::Compact::from(compact_announce); assert_eq!(actual_response, *expected_response); } @@ -61,10 +60,12 @@ pub async fn assert_compact_announce_response(response: Response, expected_respo /// ```text /// b"d5:filesd20:\x9c8B\"\x13\xe3\x0b\xff!+0\xc3`\xd2o\x9a\x02\x13d\"d8:completei1e10:downloadedi0e10:incompletei0eeee" /// ``` -pub async fn assert_scrape_response(response: Response, expected_response: &scrape::Response) { +pub async fn assert_scrape_response(response: Response, expected_response: &responses::Scrape) { assert_eq!(response.status(), 200); - let scrape_response = scrape::Response::try_from_bencoded(&response.bytes().await.unwrap()).unwrap(); + let scrape_response = responses::scrape::ResponseBuilder::try_from(&response.bytes().await.unwrap()) + .unwrap() + .build(); assert_eq!(scrape_response, *expected_response); } @@ -72,7 +73,7 @@ pub async fn assert_scrape_response(response: Response, expected_response: &scra pub async fn assert_is_announce_response(response: Response) { assert_eq!(response.status(), 200); let body = response.text().await.unwrap(); - let _announce_response: Announce = serde_bencode::from_str(&body) + let _announce_response: responses::Announce = serde_bencode::from_str(&body) .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{}\"", &body)); } diff --git a/tests/servers/http/client.rs b/tests/servers/http/client.rs index 288987c55..eeaeddc6c 100644 --- a/tests/servers/http/client.rs +++ b/tests/servers/http/client.rs @@ -2,9 +2,7 @@ use std::net::IpAddr; use reqwest::{Client as ReqwestClient, Response}; use torrust_tracker::core::auth::Key; - -use super::requests::announce::{self, Query}; -use super::requests::scrape; +use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; /// HTTP Tracker Client pub struct Client { @@ -47,15 +45,15 @@ impl Client { } } - pub async fn announce(&self, query: &announce::Query) -> Response { + pub async fn announce(&self, query: &requests::Announce) -> Response { self.get(&self.build_announce_path_and_query(query)).await } - pub async fn scrape(&self, query: &scrape::Query) -> Response { + pub async fn scrape(&self, query: &requests::Scrape) -> Response { self.get(&self.build_scrape_path_and_query(query)).await } - pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Response { + pub async fn announce_with_header(&self, query: &requests::Announce, key: &str, value: &str) -> Response { self.get_with_header(&self.build_announce_path_and_query(query), key, value) .await } @@ -77,11 +75,11 @@ impl Client { .unwrap() } - fn build_announce_path_and_query(&self, query: &announce::Query) -> String { + fn build_announce_path_and_query(&self, query: &requests::Announce) -> String { format!("{}?{query}", self.build_path("announce")) } - fn build_scrape_path_and_query(&self, query: &scrape::Query) -> String { + fn build_scrape_path_and_query(&self, query: &requests::Scrape) -> String { format!("{}?{query}", self.build_path("scrape")) } diff --git a/tests/servers/http/mod.rs b/tests/servers/http/mod.rs index 65affc433..7e23468b2 100644 --- a/tests/servers/http/mod.rs +++ b/tests/servers/http/mod.rs @@ -1,31 +1,10 @@ pub mod asserts; pub mod client; pub mod environment; -pub mod requests; -pub mod responses; pub mod v1; -pub type Started = environment::Environment; - -use percent_encoding::NON_ALPHANUMERIC; use torrust_tracker::servers::http::server; -pub type ByteArray20 = [u8; 20]; - -pub fn percent_encode_byte_array(bytes: &ByteArray20) -> String { - percent_encoding::percent_encode(bytes, NON_ALPHANUMERIC).to_string() -} - -pub struct InfoHash(ByteArray20); - -impl InfoHash { - pub fn new(vec: &[u8]) -> Self { - let mut byte_array_20: ByteArray20 = Default::default(); - byte_array_20.clone_from_slice(vec); - Self(byte_array_20) - } +pub type Started = environment::Environment; - pub fn bytes(&self) -> ByteArray20 { - self.0 - } -} +//pub(crate) const TIMEOUT: Duration = Duration::from_secs(5); diff --git a/tests/servers/http/requests/announce.rs b/tests/servers/http/requests/announce.rs deleted file mode 100644 index 061990621..000000000 --- a/tests/servers/http/requests/announce.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::fmt; -use std::net::{IpAddr, Ipv4Addr}; -use std::str::FromStr; - -use serde_repr::Serialize_repr; -use torrust_tracker_primitives::info_hash::InfoHash; -use torrust_tracker_primitives::peer; - -use crate::servers::http::{percent_encode_byte_array, ByteArray20}; - -pub struct Query { - pub info_hash: ByteArray20, - pub peer_addr: IpAddr, - pub downloaded: BaseTenASCII, - pub uploaded: BaseTenASCII, - pub peer_id: ByteArray20, - pub port: PortNumber, - pub left: BaseTenASCII, - pub event: Option, - pub compact: Option, -} - -impl fmt::Display for Query { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.build()) - } -} - -/// HTTP Tracker Announce Request: -/// -/// -/// -/// Some parameters in the specification are not implemented in this tracker yet. -impl Query { - /// It builds the URL query component for the announce request. - /// - /// This custom URL query params encoding is needed because `reqwest` does not allow - /// bytes arrays in query parameters. More info on this issue: - /// - /// - pub fn build(&self) -> String { - self.params().to_string() - } - - pub fn params(&self) -> QueryParams { - QueryParams::from(self) - } -} - -pub type BaseTenASCII = u64; -pub type PortNumber = u16; - -pub enum Event { - //Started, - //Stopped, - Completed, -} - -impl fmt::Display for Event { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - //Event::Started => write!(f, "started"), - //Event::Stopped => write!(f, "stopped"), - Event::Completed => write!(f, "completed"), - } - } -} - -#[derive(Serialize_repr, PartialEq, Debug)] -#[repr(u8)] -pub enum Compact { - Accepted = 1, - NotAccepted = 0, -} - -impl fmt::Display for Compact { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Compact::Accepted => write!(f, "1"), - Compact::NotAccepted => write!(f, "0"), - } - } -} - -pub struct QueryBuilder { - announce_query: Query, -} - -impl QueryBuilder { - pub fn default() -> QueryBuilder { - let default_announce_query = Query { - info_hash: InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0, - peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)), - downloaded: 0, - uploaded: 0, - peer_id: peer::Id(*b"-qB00000000000000001").0, - port: 17548, - left: 0, - event: Some(Event::Completed), - compact: Some(Compact::NotAccepted), - }; - Self { - announce_query: default_announce_query, - } - } - - pub fn with_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.announce_query.info_hash = info_hash.0; - self - } - - pub fn with_peer_id(mut self, peer_id: &peer::Id) -> Self { - self.announce_query.peer_id = peer_id.0; - self - } - - pub fn with_compact(mut self, compact: Compact) -> Self { - self.announce_query.compact = Some(compact); - self - } - - pub fn with_peer_addr(mut self, peer_addr: &IpAddr) -> Self { - self.announce_query.peer_addr = *peer_addr; - self - } - - pub fn without_compact(mut self) -> Self { - self.announce_query.compact = None; - self - } - - pub fn query(self) -> Query { - self.announce_query - } -} - -/// It contains all the GET parameters that can be used in a HTTP Announce request. -/// -/// Sample Announce URL with all the GET parameters (mandatory and optional): -/// -/// ```text -/// http://127.0.0.1:7070/announce? -/// info_hash=%9C8B%22%13%E3%0B%FF%21%2B0%C3%60%D2o%9A%02%13d%22 (mandatory) -/// peer_addr=192.168.1.88 -/// downloaded=0 -/// uploaded=0 -/// peer_id=%2DqB00000000000000000 (mandatory) -/// port=17548 (mandatory) -/// left=0 -/// event=completed -/// compact=0 -/// ``` -pub struct QueryParams { - pub info_hash: Option, - pub peer_addr: Option, - pub downloaded: Option, - pub uploaded: Option, - pub peer_id: Option, - pub port: Option, - pub left: Option, - pub event: Option, - pub compact: Option, -} - -impl std::fmt::Display for QueryParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut params = vec![]; - - if let Some(info_hash) = &self.info_hash { - params.push(("info_hash", info_hash)); - } - if let Some(peer_addr) = &self.peer_addr { - params.push(("peer_addr", peer_addr)); - } - if let Some(downloaded) = &self.downloaded { - params.push(("downloaded", downloaded)); - } - if let Some(uploaded) = &self.uploaded { - params.push(("uploaded", uploaded)); - } - if let Some(peer_id) = &self.peer_id { - params.push(("peer_id", peer_id)); - } - if let Some(port) = &self.port { - params.push(("port", port)); - } - if let Some(left) = &self.left { - params.push(("left", left)); - } - if let Some(event) = &self.event { - params.push(("event", event)); - } - if let Some(compact) = &self.compact { - params.push(("compact", compact)); - } - - let query = params - .iter() - .map(|param| format!("{}={}", param.0, param.1)) - .collect::>() - .join("&"); - - write!(f, "{query}") - } -} - -impl QueryParams { - pub fn from(announce_query: &Query) -> Self { - let event = announce_query.event.as_ref().map(std::string::ToString::to_string); - let compact = announce_query.compact.as_ref().map(std::string::ToString::to_string); - - Self { - info_hash: Some(percent_encode_byte_array(&announce_query.info_hash)), - peer_addr: Some(announce_query.peer_addr.to_string()), - downloaded: Some(announce_query.downloaded.to_string()), - uploaded: Some(announce_query.uploaded.to_string()), - peer_id: Some(percent_encode_byte_array(&announce_query.peer_id)), - port: Some(announce_query.port.to_string()), - left: Some(announce_query.left.to_string()), - event, - compact, - } - } - - pub fn remove_optional_params(&mut self) { - // todo: make them optional with the Option<...> in the AnnounceQuery struct - // if they are really optional. So that we can crete a minimal AnnounceQuery - // instead of removing the optional params afterwards. - // - // The original specification on: - // - // says only `ip` and `event` are optional. - // - // On - // says only `ip`, `numwant`, `key` and `trackerid` are optional. - // - // but the server is responding if all these params are not included. - self.peer_addr = None; - self.downloaded = None; - self.uploaded = None; - self.left = None; - self.event = None; - self.compact = None; - } - - pub fn set(&mut self, param_name: &str, param_value: &str) { - match param_name { - "info_hash" => self.info_hash = Some(param_value.to_string()), - "peer_addr" => self.peer_addr = Some(param_value.to_string()), - "downloaded" => self.downloaded = Some(param_value.to_string()), - "uploaded" => self.uploaded = Some(param_value.to_string()), - "peer_id" => self.peer_id = Some(param_value.to_string()), - "port" => self.port = Some(param_value.to_string()), - "left" => self.left = Some(param_value.to_string()), - "event" => self.event = Some(param_value.to_string()), - "compact" => self.compact = Some(param_value.to_string()), - &_ => panic!("Invalid param name for announce query"), - } - } -} diff --git a/tests/servers/http/requests/mod.rs b/tests/servers/http/requests/mod.rs deleted file mode 100644 index 776d2dfbf..000000000 --- a/tests/servers/http/requests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod announce; -pub mod scrape; diff --git a/tests/servers/http/requests/scrape.rs b/tests/servers/http/requests/scrape.rs deleted file mode 100644 index f66605855..000000000 --- a/tests/servers/http/requests/scrape.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::fmt; -use std::str::FromStr; - -use torrust_tracker_primitives::info_hash::InfoHash; - -use crate::servers::http::{percent_encode_byte_array, ByteArray20}; - -pub struct Query { - pub info_hash: Vec, -} - -impl fmt::Display for Query { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.build()) - } -} - -/// HTTP Tracker Scrape Request: -/// -/// -impl Query { - /// It builds the URL query component for the scrape request. - /// - /// This custom URL query params encoding is needed because `reqwest` does not allow - /// bytes arrays in query parameters. More info on this issue: - /// - /// - pub fn build(&self) -> String { - self.params().to_string() - } - - pub fn params(&self) -> QueryParams { - QueryParams::from(self) - } -} - -pub struct QueryBuilder { - scrape_query: Query, -} - -impl QueryBuilder { - pub fn default() -> QueryBuilder { - let default_scrape_query = Query { - info_hash: [InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap().0].to_vec(), - }; - Self { - scrape_query: default_scrape_query, - } - } - - pub fn with_one_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.info_hash = [info_hash.0].to_vec(); - self - } - - pub fn add_info_hash(mut self, info_hash: &InfoHash) -> Self { - self.scrape_query.info_hash.push(info_hash.0); - self - } - - pub fn query(self) -> Query { - self.scrape_query - } -} - -/// It contains all the GET parameters that can be used in a HTTP Scrape request. -/// -/// The `info_hash` param is the percent encoded of the the 20-byte array info hash. -/// -/// Sample Scrape URL with all the GET parameters: -/// -/// For `IpV4`: -/// -/// ```text -/// http://127.0.0.1:7070/scrape?info_hash=%9C8B%22%13%E3%0B%FF%21%2B0%C3%60%D2o%9A%02%13d%22 -/// ``` -/// -/// For `IpV6`: -/// -/// ```text -/// http://[::1]:7070/scrape?info_hash=%9C8B%22%13%E3%0B%FF%21%2B0%C3%60%D2o%9A%02%13d%22 -/// ``` -/// -/// You can add as many info hashes as you want, just adding the same param again. -pub struct QueryParams { - pub info_hash: Vec, -} - -impl QueryParams { - pub fn set_one_info_hash_param(&mut self, info_hash: &str) { - self.info_hash = vec![info_hash.to_string()]; - } -} - -impl std::fmt::Display for QueryParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let query = self - .info_hash - .iter() - .map(|info_hash| format!("info_hash={}", &info_hash)) - .collect::>() - .join("&"); - - write!(f, "{query}") - } -} - -impl QueryParams { - pub fn from(scrape_query: &Query) -> Self { - let info_hashes = scrape_query - .info_hash - .iter() - .map(percent_encode_byte_array) - .collect::>(); - - Self { info_hash: info_hashes } - } -} diff --git a/tests/servers/http/responses/announce.rs b/tests/servers/http/responses/announce.rs deleted file mode 100644 index 2b49b4405..000000000 --- a/tests/servers/http/responses/announce.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - -use serde::{Deserialize, Serialize}; -use torrust_tracker_primitives::peer; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Announce { - pub complete: u32, - pub incomplete: u32, - pub interval: u32, - #[serde(rename = "min interval")] - pub min_interval: u32, - pub peers: Vec, // Peers using IPV4 and IPV6 -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct DictionaryPeer { - pub ip: String, - #[serde(rename = "peer id")] - #[serde(with = "serde_bytes")] - pub peer_id: Vec, - pub port: u16, -} - -impl From for DictionaryPeer { - fn from(peer: peer::Peer) -> Self { - DictionaryPeer { - peer_id: peer.peer_id.to_bytes().to_vec(), - ip: peer.peer_addr.ip().to_string(), - port: peer.peer_addr.port(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct DeserializedCompact { - pub complete: u32, - pub incomplete: u32, - pub interval: u32, - #[serde(rename = "min interval")] - pub min_interval: u32, - #[serde(with = "serde_bytes")] - pub peers: Vec, -} - -impl DeserializedCompact { - pub fn from_bytes(bytes: &[u8]) -> Result { - serde_bencode::from_bytes::(bytes) - } -} - -#[derive(Debug, PartialEq)] -pub struct Compact { - // code-review: there could be a way to deserialize this struct directly - // by using serde instead of doing it manually. Or at least using a custom deserializer. - pub complete: u32, - pub incomplete: u32, - pub interval: u32, - pub min_interval: u32, - pub peers: CompactPeerList, -} - -#[derive(Debug, PartialEq)] -pub struct CompactPeerList { - peers: Vec, -} - -impl CompactPeerList { - pub fn new(peers: Vec) -> Self { - Self { peers } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CompactPeer { - ip: Ipv4Addr, - port: u16, -} - -impl CompactPeer { - pub fn new(socket_addr: &SocketAddr) -> Self { - match socket_addr.ip() { - IpAddr::V4(ip) => Self { - ip, - port: socket_addr.port(), - }, - IpAddr::V6(_ip) => panic!("IPV6 is not supported for compact peer"), - } - } - - pub fn new_from_bytes(bytes: &[u8]) -> Self { - Self { - ip: Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]), - port: u16::from_be_bytes([bytes[4], bytes[5]]), - } - } -} - -impl From for Compact { - fn from(compact_announce: DeserializedCompact) -> Self { - let mut peers = vec![]; - - for peer_bytes in compact_announce.peers.chunks_exact(6) { - peers.push(CompactPeer::new_from_bytes(peer_bytes)); - } - - Self { - complete: compact_announce.complete, - incomplete: compact_announce.incomplete, - interval: compact_announce.interval, - min_interval: compact_announce.min_interval, - peers: CompactPeerList::new(peers), - } - } -} diff --git a/tests/servers/http/responses/error.rs b/tests/servers/http/responses/error.rs deleted file mode 100644 index 00befdb54..000000000 --- a/tests/servers/http/responses/error.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Error { - #[serde(rename = "failure reason")] - pub failure_reason: String, -} diff --git a/tests/servers/http/responses/mod.rs b/tests/servers/http/responses/mod.rs deleted file mode 100644 index bdc689056..000000000 --- a/tests/servers/http/responses/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod announce; -pub mod error; -pub mod scrape; diff --git a/tests/servers/http/responses/scrape.rs b/tests/servers/http/responses/scrape.rs deleted file mode 100644 index fc741cbf4..000000000 --- a/tests/servers/http/responses/scrape.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::collections::HashMap; -use std::str; - -use serde::{Deserialize, Serialize}; -use serde_bencode::value::Value; - -use crate::servers::http::{ByteArray20, InfoHash}; - -#[derive(Debug, PartialEq, Default)] -pub struct Response { - pub files: HashMap, -} - -impl Response { - pub fn with_one_file(info_hash_bytes: ByteArray20, file: File) -> Self { - let mut files: HashMap = HashMap::new(); - files.insert(info_hash_bytes, file); - Self { files } - } - - pub fn try_from_bencoded(bytes: &[u8]) -> Result { - let scrape_response: DeserializedResponse = serde_bencode::from_bytes(bytes).unwrap(); - Self::try_from(scrape_response) - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] -pub struct File { - pub complete: i64, // The number of active peers that have completed downloading - pub downloaded: i64, // The number of peers that have ever completed downloading - pub incomplete: i64, // The number of active peers that have not completed downloading -} - -impl File { - pub fn zeroed() -> Self { - Self::default() - } -} - -impl TryFrom for Response { - type Error = BencodeParseError; - - fn try_from(scrape_response: DeserializedResponse) -> Result { - parse_bencoded_response(&scrape_response.files) - } -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -struct DeserializedResponse { - pub files: Value, -} - -pub struct ResponseBuilder { - response: Response, -} - -impl ResponseBuilder { - pub fn default() -> Self { - Self { - response: Response::default(), - } - } - - pub fn add_file(mut self, info_hash_bytes: ByteArray20, file: File) -> Self { - self.response.files.insert(info_hash_bytes, file); - self - } - - pub fn build(self) -> Response { - self.response - } -} - -#[derive(Debug)] -pub enum BencodeParseError { - #[allow(dead_code)] - InvalidValueExpectedDict { value: Value }, - #[allow(dead_code)] - InvalidValueExpectedInt { value: Value }, - #[allow(dead_code)] - InvalidFileField { value: Value }, - #[allow(dead_code)] - MissingFileField { field_name: String }, -} - -/// It parses a bencoded scrape response into a `Response` struct. -/// -/// For example: -/// -/// ```text -/// d5:filesd20:xxxxxxxxxxxxxxxxxxxxd8:completei11e10:downloadedi13772e10:incompletei19e -/// 20:yyyyyyyyyyyyyyyyyyyyd8:completei21e10:downloadedi206e10:incompletei20eee -/// ``` -/// -/// Response (JSON encoded for readability): -/// -/// ```text -/// { -/// 'files': { -/// 'xxxxxxxxxxxxxxxxxxxx': {'complete': 11, 'downloaded': 13772, 'incomplete': 19}, -/// 'yyyyyyyyyyyyyyyyyyyy': {'complete': 21, 'downloaded': 206, 'incomplete': 20} -/// } -/// } -fn parse_bencoded_response(value: &Value) -> Result { - let mut files: HashMap = HashMap::new(); - - match value { - Value::Dict(dict) => { - for file_element in dict { - let info_hash_byte_vec = file_element.0; - let file_value = file_element.1; - - let file = parse_bencoded_file(file_value).unwrap(); - - files.insert(InfoHash::new(info_hash_byte_vec).bytes(), file); - } - } - _ => return Err(BencodeParseError::InvalidValueExpectedDict { value: value.clone() }), - } - - Ok(Response { files }) -} - -/// It parses a bencoded dictionary into a `File` struct. -/// -/// For example: -/// -/// -/// ```text -/// d8:completei11e10:downloadedi13772e10:incompletei19ee -/// ``` -/// -/// into: -/// -/// ```text -/// File { -/// complete: 11, -/// downloaded: 13772, -/// incomplete: 19, -/// } -/// ``` -fn parse_bencoded_file(value: &Value) -> Result { - let file = match &value { - Value::Dict(dict) => { - let mut complete = None; - let mut downloaded = None; - let mut incomplete = None; - - for file_field in dict { - let field_name = file_field.0; - - let field_value = match file_field.1 { - Value::Int(number) => Ok(*number), - _ => Err(BencodeParseError::InvalidValueExpectedInt { - value: file_field.1.clone(), - }), - }?; - - if field_name == b"complete" { - complete = Some(field_value); - } else if field_name == b"downloaded" { - downloaded = Some(field_value); - } else if field_name == b"incomplete" { - incomplete = Some(field_value); - } else { - return Err(BencodeParseError::InvalidFileField { - value: file_field.1.clone(), - }); - } - } - - if complete.is_none() { - return Err(BencodeParseError::MissingFileField { - field_name: "complete".to_string(), - }); - } - - if downloaded.is_none() { - return Err(BencodeParseError::MissingFileField { - field_name: "downloaded".to_string(), - }); - } - - if incomplete.is_none() { - return Err(BencodeParseError::MissingFileField { - field_name: "incomplete".to_string(), - }); - } - - File { - complete: complete.unwrap(), - downloaded: downloaded.unwrap(), - incomplete: incomplete.unwrap(), - } - } - _ => return Err(BencodeParseError::InvalidValueExpectedDict { value: value.clone() }), - }; - - Ok(file) -} diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs index a7962db0f..d2c2f8574 100644 --- a/tests/servers/http/v1/contract.rs +++ b/tests/servers/http/v1/contract.rs @@ -9,6 +9,8 @@ async fn environment_should_be_started_and_stopped() { env.stop().await; } +pub(crate) const PORT: u16 = 17548; + mod for_all_config_modes { use torrust_tracker::servers::http::v1::handlers::health_check::{Report, Status}; @@ -31,11 +33,15 @@ mod for_all_config_modes { } mod and_running_on_reverse_proxy { + + use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; + use torrust_tracker_primitives::info_hash::InfoHash; + use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; use crate::servers::http::client::Client; - use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::v1::contract::PORT; use crate::servers::http::Started; #[tokio::test] @@ -45,7 +51,8 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let params = QueryBuilder::default().query().params(); + let query = requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build(); + let params: requests::announce::QueryParams = (&query).into(); let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; @@ -58,7 +65,8 @@ mod for_all_config_modes { async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let params = QueryBuilder::default().query().params(); + let query = requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build(); + let params: requests::announce::QueryParams = (&query).into(); let response = Client::new(*env.bind_address()) .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") @@ -89,6 +97,8 @@ mod for_all_config_modes { use local_ip_address::local_ip; use reqwest::{Response, StatusCode}; use tokio::net::TcpListener; + use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; + use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; @@ -101,9 +111,20 @@ mod for_all_config_modes { assert_is_announce_response, assert_missing_query_params_for_announce_request_error_response, }; use crate::servers::http::client::Client; - use crate::servers::http::requests::announce::{Compact, QueryBuilder}; - use crate::servers::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; - use crate::servers::http::{responses, Started}; + use crate::servers::http::v1::contract::PORT; + use crate::servers::http::Started; + + pub(crate) fn default_query_builder() -> requests::announce::QueryBuilder { + requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT) + } + + pub(crate) fn default_prams() -> requests::announce::QueryParams { + requests::announce::QueryParams::from(&default_query_builder().build()) + } + + pub(crate) fn with_infohash(info_hash: &str) -> requests::Announce { + requests::announce::QueryBuilder::new(info_hash.parse().expect("invalid infohash"), peer::Id::from(1), PORT).build() + } #[tokio::test] async fn it_should_start_and_stop() { @@ -115,7 +136,7 @@ mod for_all_config_modes { async fn should_respond_if_only_the_mandatory_fields_are_provided() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); params.remove_optional_params(); @@ -158,7 +179,7 @@ mod for_all_config_modes { // Without `info_hash` param - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); params.info_hash = None; @@ -168,7 +189,7 @@ mod for_all_config_modes { // Without `peer_id` param - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); params.peer_id = None; @@ -178,7 +199,7 @@ mod for_all_config_modes { // Without `port` param - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); params.port = None; @@ -189,11 +210,24 @@ mod for_all_config_modes { env.stop().await; } + #[tokio::test] + async fn it_should_return_an_empty_response_when_announcing_a_stopped_peer() { + let env = Started::new(&configuration::ephemeral_mode_public().into()).await; + + let query = default_query_builder().with_event(AnnounceEvent::Stopped).build(); + + let response = Client::new(*env.bind_address()).announce(&query).await; + + assert_empty_announce_response(response, &env.tracker.get_announce_policy()).await; + + env.stop().await; + } + #[tokio::test] async fn should_fail_when_the_info_hash_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); for invalid_value in &invalid_info_hashes() { params.set("info_hash", invalid_value); @@ -215,7 +249,7 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); @@ -230,7 +264,7 @@ mod for_all_config_modes { async fn should_fail_when_the_downloaded_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = ["-1", "1.1", "a"]; @@ -249,7 +283,7 @@ mod for_all_config_modes { async fn should_fail_when_the_uploaded_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = ["-1", "1.1", "a"]; @@ -268,7 +302,7 @@ mod for_all_config_modes { async fn should_fail_when_the_peer_id_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = [ "0", @@ -294,7 +328,7 @@ mod for_all_config_modes { async fn should_fail_when_the_port_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = ["-1", "1.1", "a"]; @@ -313,7 +347,7 @@ mod for_all_config_modes { async fn should_fail_when_the_left_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = ["-1", "1.1", "a"]; @@ -332,7 +366,7 @@ mod for_all_config_modes { async fn should_fail_when_the_event_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = [ "0", @@ -359,7 +393,7 @@ mod for_all_config_modes { async fn should_fail_when_the_compact_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = default_prams(); let invalid_values = ["-1", "1.1", "a"]; @@ -378,27 +412,16 @@ mod for_all_config_modes { async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let response = Client::new(*env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) - .query(), - ) - .await; + let announce = with_infohash("9c38422213e30bff212b30c360d26f9a02136422"); - let announce_policy = env.tracker.get_announce_policy(); + let response = Client::new(*env.bind_address()).announce(&announce).await; - assert_announce_response( - response, - &Announce { - complete: 1, // the peer for this test - incomplete: 0, - interval: announce_policy.interval, - min_interval: announce_policy.interval_min, - peers: vec![], - }, - ) - .await; + let expected_response = responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) + // the peer for this test + .with_complete(1) + .build(); + + assert_announce_response(response, &expected_response).await; env.stop().await; } @@ -417,30 +440,19 @@ mod for_all_config_modes { // Add the Peer 1 env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT).build(); + // Announce the new Peer 2. This new peer is non included on the response peer list - let response = Client::new(*env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .query(), - ) - .await; + let response = Client::new(*env.bind_address()).announce(&query).await; - let announce_policy = env.tracker.get_announce_policy(); + let expected_response = responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) + // the peer for this test + .with_complete(2) + .with_peers(vec![responses::announce::DictionaryPeer::from(previously_announced_peer)]) + .build(); // It should only contain the previously announced peer - assert_announce_response( - response, - &Announce { - complete: 2, - incomplete: 0, - interval: announce_policy.interval, - min_interval: announce_policy.interval_min, - peers: vec![DictionaryPeer::from(previously_announced_peer)], - }, - ) - .await; + assert_announce_response(response, &expected_response).await; env.stop().await; } @@ -468,31 +480,22 @@ mod for_all_config_modes { .build(); env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; - // Announce the new Peer. - let response = Client::new(*env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000003")) - .query(), - ) - .await; + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000003"), PORT).build(); - let announce_policy = env.tracker.get_announce_policy(); + // Announce the new Peer. + let response = Client::new(*env.bind_address()).announce(&query).await; // The newly announced peer is not included on the response peer list, // but all the previously announced peers should be included regardless the IP version they are using. - assert_announce_response( - response, - &Announce { - complete: 3, - incomplete: 0, - interval: announce_policy.interval, - min_interval: announce_policy.interval_min, - peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], - }, - ) - .await; + let expected_response = responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) + .with_complete(3) + .with_peers(vec![ + responses::announce::DictionaryPeer::from(peer_using_ipv4), + responses::announce::DictionaryPeer::from(peer_using_ipv6), + ]) + .build(); + + assert_announce_response(response, &expected_response).await; env.stop().await; } @@ -507,16 +510,17 @@ mod for_all_config_modes { // Add a peer env.add_torrent_peer(&info_hash, &peer).await; - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer.peer_id) - .query(); + let query = requests::announce::QueryBuilder::new(info_hash, peer.peer_id, PORT).build(); - assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); + let response = Client::new(*env.bind_address()).announce(&query).await; - let response = Client::new(*env.bind_address()).announce(&announce_query).await; - - assert_empty_announce_response(response).await; + assert_announce_response( + response, + &responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) + .with_complete(1) + .build(), + ) + .await; env.stop().await; } @@ -538,23 +542,21 @@ mod for_all_config_modes { // Add the Peer 1 env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT) + .with_compact() + .build(); + // Announce the new Peer 2 accepting compact responses - let response = Client::new(*env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_compact(Compact::Accepted) - .query(), - ) - .await; + let response = Client::new(*env.bind_address()).announce(&query).await; let expected_response = responses::announce::Compact { complete: 2, incomplete: 0, interval: 120, min_interval: 120, - peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), + peers: responses::announce::CompactPeerList::new( + [responses::announce::CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec(), + ), }; assert_compact_announce_response(response, &expected_response).await; @@ -582,15 +584,9 @@ mod for_all_config_modes { // Announce the new Peer 2 without passing the "compact" param // By default it should respond with the compact peer list // https://www.bittorrent.org/beps/bep_0023.html - let response = Client::new(*env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .without_compact() - .query(), - ) - .await; + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT).build(); + + let response = Client::new(*env.bind_address()).announce(&query).await; assert!(!is_a_compact_announce_response(response).await); @@ -608,7 +604,7 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; Client::new(*env.bind_address()) - .announce(&QueryBuilder::default().query()) + .announce(&default_query_builder().build()) .await; let stats = env.tracker.get_stats().await; @@ -632,7 +628,7 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_ipv6().into()).await; Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) + .announce(&default_query_builder().build()) .await; let stats = env.tracker.get_stats().await; @@ -652,9 +648,9 @@ mod for_all_config_modes { Client::new(*env.bind_address()) .announce( - &QueryBuilder::default() + &default_query_builder() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), + .build(), ) .await; @@ -672,7 +668,7 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; Client::new(*env.bind_address()) - .announce(&QueryBuilder::default().query()) + .announce(&default_query_builder().build()) .await; let stats = env.tracker.get_stats().await; @@ -696,7 +692,7 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_ipv6().into()).await; Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) + .announce(&default_query_builder().build()) .await; let stats = env.tracker.get_stats().await; @@ -716,9 +712,9 @@ mod for_all_config_modes { Client::new(*env.bind_address()) .announce( - &QueryBuilder::default() + &default_query_builder() .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), + .build(), ) .await; @@ -738,14 +734,13 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let client_ip = local_ip().unwrap(); - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + .build(); { let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&announce_query).await.status(); + let status = client.announce(&query).await.status(); assert_eq!(status, StatusCode::OK); } @@ -774,14 +769,13 @@ mod for_all_config_modes { let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); let client_ip = loopback_ip; - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + .build(); { let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&announce_query).await.status(); + let status = client.announce(&query).await.status(); assert_eq!(status, StatusCode::OK); } @@ -814,14 +808,13 @@ mod for_all_config_modes { let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); let client_ip = loopback_ip; - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + .build(); { let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&announce_query).await.status(); + let status = client.announce(&query).await.status(); assert_eq!(status, StatusCode::OK); } @@ -846,15 +839,15 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash: InfoHash = "9c38422213e30bff212b30c360d26f9a02136422".parse().expect("it should parse"); - let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); + let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build(); { let client = Client::new(*env.bind_address()); let status = client .announce_with_header( - &announce_query, + &query, "X-Forwarded-For", "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", ) @@ -887,6 +880,8 @@ mod for_all_config_modes { use std::str::FromStr; use tokio::net::TcpListener; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape::File; + use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; @@ -898,9 +893,7 @@ mod for_all_config_modes { assert_scrape_response, }; use crate::servers::http::client::Client; - use crate::servers::http::requests::scrape::QueryBuilder; - use crate::servers::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::servers::http::{requests, Started}; + use crate::servers::http::Started; //#[tokio::test] #[allow(dead_code)] @@ -917,7 +910,7 @@ mod for_all_config_modes { async fn should_fail_when_the_info_hash_param_is_invalid() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params: requests::scrape::QueryParams = requests::scrape::QueryBuilder::default().build().into(); for invalid_value in &invalid_info_hashes() { params.set_one_info_hash_param(invalid_value); @@ -946,22 +939,18 @@ mod for_all_config_modes { .await; let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file( + let expected_scrape_response = responses::scrape::ResponseBuilder::default() + .add_file(( info_hash.bytes(), File { complete: 0, downloaded: 0, incomplete: 1, }, - ) + )) .build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -985,22 +974,18 @@ mod for_all_config_modes { .await; let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file( + let expected_scrape_response = responses::scrape::ResponseBuilder::default() + .add_file(( info_hash.bytes(), File { complete: 1, downloaded: 0, incomplete: 0, }, - ) + )) .build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1015,14 +1000,12 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; + let expected_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); + + assert_scrape_response(response, &expected_response).await; env.stop().await; } @@ -1039,13 +1022,13 @@ mod for_all_config_modes { &requests::scrape::QueryBuilder::default() .add_info_hash(&info_hash1) .add_info_hash(&info_hash2) - .query(), + .build(), ) .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file(info_hash1.bytes(), File::zeroed()) - .add_file(info_hash2.bytes(), File::zeroed()) + let expected_scrape_response = responses::scrape::ResponseBuilder::default() + .add_file((info_hash1.bytes(), File::zeroed())) + .add_file((info_hash2.bytes(), File::zeroed())) .build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1060,11 +1043,7 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; let stats = env.tracker.get_stats().await; @@ -1090,11 +1069,7 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; let stats = env.tracker.get_stats().await; @@ -1113,12 +1088,14 @@ mod configured_as_whitelisted { mod and_receiving_an_announce_request { use std::str::FromStr; + use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; use torrust_tracker_primitives::info_hash::InfoHash; + use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; use crate::servers::http::client::Client; - use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::v1::contract::PORT; use crate::servers::http::Started; #[tokio::test] @@ -1128,7 +1105,7 @@ mod configured_as_whitelisted { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let response = Client::new(*env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) .await; assert_torrent_not_in_whitelist_error_response(response).await; @@ -1148,7 +1125,7 @@ mod configured_as_whitelisted { .expect("should add the torrent to the whitelist"); let response = Client::new(*env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) .await; assert_is_announce_response(response).await; @@ -1160,6 +1137,8 @@ mod configured_as_whitelisted { mod receiving_an_scrape_request { use std::str::FromStr; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape::File; + use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; @@ -1167,8 +1146,7 @@ mod configured_as_whitelisted { use crate::servers::http::asserts::assert_scrape_response; use crate::servers::http::client::Client; - use crate::servers::http::responses::scrape::{File, ResponseBuilder}; - use crate::servers::http::{requests, Started}; + use crate::servers::http::Started; #[tokio::test] async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { @@ -1186,14 +1164,10 @@ mod configured_as_whitelisted { .await; let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1221,23 +1195,18 @@ mod configured_as_whitelisted { .expect("should add the torrent to the whitelist"); let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); + let expected_scrape_response = responses::scrape::ResponseBuilder::from(( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + )) + .build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1253,12 +1222,14 @@ mod configured_as_private { use std::time::Duration; use torrust_tracker::core::auth::Key; + use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; use torrust_tracker_primitives::info_hash::InfoHash; + use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; use crate::servers::http::client::Client; - use crate::servers::http::requests::announce::QueryBuilder; + use crate::servers::http::v1::contract::PORT; use crate::servers::http::Started; #[tokio::test] @@ -1268,7 +1239,7 @@ mod configured_as_private { let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); let response = Client::authenticated(*env.bind_address(), expiring_key.key()) - .announce(&QueryBuilder::default().query()) + .announce(&requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build()) .await; assert_is_announce_response(response).await; @@ -1283,7 +1254,7 @@ mod configured_as_private { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let response = Client::new(*env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) .await; assert_authentication_error_response(response).await; @@ -1299,7 +1270,7 @@ mod configured_as_private { let response = Client::new(*env.bind_address()) .get(&format!( - "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0" + "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port={PORT}&left=0&event=completed&compact=0" )) .await; @@ -1314,7 +1285,7 @@ mod configured_as_private { let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); let response = Client::authenticated(*env.bind_address(), unregistered_key) - .announce(&QueryBuilder::default().query()) + .announce(&requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build()) .await; assert_authentication_error_response(response).await; @@ -1329,6 +1300,8 @@ mod configured_as_private { use std::time::Duration; use torrust_tracker::core::auth::Key; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape::File; + use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; @@ -1336,8 +1309,7 @@ mod configured_as_private { use crate::servers::http::asserts::{assert_authentication_error_response, assert_scrape_response}; use crate::servers::http::client::Client; - use crate::servers::http::responses::scrape::{File, ResponseBuilder}; - use crate::servers::http::{requests, Started}; + use crate::servers::http::Started; #[tokio::test] async fn should_fail_if_the_key_query_param_cannot_be_parsed() { @@ -1370,14 +1342,10 @@ mod configured_as_private { .await; let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1402,23 +1370,18 @@ mod configured_as_private { let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); let response = Client::authenticated(*env.bind_address(), expiring_key.key()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); + let expected_scrape_response = responses::scrape::ResponseBuilder::from(( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + )) + .build(); assert_scrape_response(response, &expected_scrape_response).await; @@ -1446,14 +1409,10 @@ mod configured_as_private { let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); let response = Client::authenticated(*env.bind_address(), false_key) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) + .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) .await; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); assert_scrape_response(response, &expected_scrape_response).await; From 052d0681a11232a6e994fdabdbe4ab0805d5ead7 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Sat, 30 Mar 2024 00:26:23 +0800 Subject: [PATCH 004/217] dev: use shared client for testing --- .../bit_torrent/tracker/http/client/mod.rs | 2 +- tests/servers/http/client.rs | 101 ----- tests/servers/http/mod.rs | 5 +- tests/servers/http/v1/contract.rs | 410 ++++++++---------- tests/servers/http/v1/mod.rs | 96 ++++ 5 files changed, 278 insertions(+), 336 deletions(-) delete mode 100644 tests/servers/http/client.rs diff --git a/src/shared/bit_torrent/tracker/http/client/mod.rs b/src/shared/bit_torrent/tracker/http/client/mod.rs index 506126427..a1d6090a4 100644 --- a/src/shared/bit_torrent/tracker/http/client/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/mod.rs @@ -75,7 +75,7 @@ impl Client { /// # Errors /// /// This method fails if the client builder fails. - pub fn authenticated(base_url: Url, key: Key, timeout: Duration) -> Result { + pub fn authenticated(base_url: Url, timeout: Duration, key: Key) -> Result { let client = reqwest::Client::builder() .timeout(timeout) .build() diff --git a/tests/servers/http/client.rs b/tests/servers/http/client.rs deleted file mode 100644 index eeaeddc6c..000000000 --- a/tests/servers/http/client.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::net::IpAddr; - -use reqwest::{Client as ReqwestClient, Response}; -use torrust_tracker::core::auth::Key; -use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; - -/// HTTP Tracker Client -pub struct Client { - server_addr: std::net::SocketAddr, - reqwest: ReqwestClient, - key: Option, -} - -/// URL components in this context: -/// -/// ```text -/// http://127.0.0.1:62304/announce/YZ....rJ?info_hash=%9C8B%22%13%E3%0B%FF%21%2B0%C3%60%D2o%9A%02%13d%22 -/// \_____________________/\_______________/ \__________________________________________________________/ -/// | | | -/// base url path query -/// ``` -impl Client { - pub fn new(server_addr: std::net::SocketAddr) -> Self { - Self { - server_addr, - reqwest: reqwest::Client::builder().build().unwrap(), - key: None, - } - } - - /// Creates the new client binding it to an specific local address - pub fn bind(server_addr: std::net::SocketAddr, local_address: IpAddr) -> Self { - Self { - server_addr, - reqwest: reqwest::Client::builder().local_address(local_address).build().unwrap(), - key: None, - } - } - - pub fn authenticated(server_addr: std::net::SocketAddr, key: Key) -> Self { - Self { - server_addr, - reqwest: reqwest::Client::builder().build().unwrap(), - key: Some(key), - } - } - - pub async fn announce(&self, query: &requests::Announce) -> Response { - self.get(&self.build_announce_path_and_query(query)).await - } - - pub async fn scrape(&self, query: &requests::Scrape) -> Response { - self.get(&self.build_scrape_path_and_query(query)).await - } - - pub async fn announce_with_header(&self, query: &requests::Announce, key: &str, value: &str) -> Response { - self.get_with_header(&self.build_announce_path_and_query(query), key, value) - .await - } - - pub async fn health_check(&self) -> Response { - self.get(&self.build_path("health_check")).await - } - - pub async fn get(&self, path: &str) -> Response { - self.reqwest.get(self.build_url(path)).send().await.unwrap() - } - - pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Response { - self.reqwest - .get(self.build_url(path)) - .header(key, value) - .send() - .await - .unwrap() - } - - fn build_announce_path_and_query(&self, query: &requests::Announce) -> String { - format!("{}?{query}", self.build_path("announce")) - } - - fn build_scrape_path_and_query(&self, query: &requests::Scrape) -> String { - format!("{}?{query}", self.build_path("scrape")) - } - - fn build_path(&self, path: &str) -> String { - match &self.key { - Some(key) => format!("{path}/{key}"), - None => path.to_string(), - } - } - - fn build_url(&self, path: &str) -> String { - let base_url = self.base_url(); - format!("{base_url}{path}") - } - - fn base_url(&self) -> String { - format!("http://{}/", &self.server_addr) - } -} diff --git a/tests/servers/http/mod.rs b/tests/servers/http/mod.rs index 7e23468b2..5a40cfcdb 100644 --- a/tests/servers/http/mod.rs +++ b/tests/servers/http/mod.rs @@ -1,10 +1,11 @@ pub mod asserts; -pub mod client; pub mod environment; pub mod v1; +use std::time::Duration; + use torrust_tracker::servers::http::server; pub type Started = environment::Environment; -//pub(crate) const TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const TIMEOUT: Duration = Duration::from_secs(5); diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs index d2c2f8574..5f71eaf90 100644 --- a/tests/servers/http/v1/contract.rs +++ b/tests/servers/http/v1/contract.rs @@ -9,21 +9,22 @@ async fn environment_should_be_started_and_stopped() { env.stop().await; } -pub(crate) const PORT: u16 = 17548; - mod for_all_config_modes { use torrust_tracker::servers::http::v1::handlers::health_check::{Report, Status}; use torrust_tracker_test_helpers::configuration; - use crate::servers::http::client::Client; + use crate::servers::http::v1::create_default_client; use crate::servers::http::Started; #[tokio::test] async fn health_check_endpoint_should_return_ok_if_the_http_tracker_is_running() { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let response = Client::new(*env.bind_address()).health_check().await; + let response = create_default_client(&env) + .health_check() + .await + .expect("it should get a response"); assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -35,13 +36,10 @@ mod for_all_config_modes { mod and_running_on_reverse_proxy { use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; - use torrust_tracker_primitives::info_hash::InfoHash; - use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; - use crate::servers::http::client::Client; - use crate::servers::http::v1::contract::PORT; + use crate::servers::http::v1::{create_announce_query, create_client_response, create_default_client}; use crate::servers::http::Started; #[tokio::test] @@ -51,10 +49,10 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let query = requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build(); + let query = create_announce_query(1, 1).build(); let params: requests::announce::QueryParams = (&query).into(); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; @@ -65,12 +63,13 @@ mod for_all_config_modes { async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await; - let query = requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build(); + let query = create_announce_query(1, 1).build(); let params: requests::announce::QueryParams = (&query).into(); - let response = Client::new(*env.bind_address()) + let response = create_default_client(&env) .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") - .await; + .await + .expect("it should get a response"); assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; @@ -97,7 +96,7 @@ mod for_all_config_modes { use local_ip_address::local_ip; use reqwest::{Response, StatusCode}; use tokio::net::TcpListener; - use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; @@ -110,22 +109,12 @@ mod for_all_config_modes { assert_cannot_parse_query_params_error_response, assert_compact_announce_response, assert_empty_announce_response, assert_is_announce_response, assert_missing_query_params_for_announce_request_error_response, }; - use crate::servers::http::client::Client; - use crate::servers::http::v1::contract::PORT; + use crate::servers::http::v1::{ + create_announce_query, create_bonded_client_announce_response, create_client_announce_response, + create_client_response, create_default_announce_prams, create_default_client, + }; use crate::servers::http::Started; - pub(crate) fn default_query_builder() -> requests::announce::QueryBuilder { - requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT) - } - - pub(crate) fn default_prams() -> requests::announce::QueryParams { - requests::announce::QueryParams::from(&default_query_builder().build()) - } - - pub(crate) fn with_infohash(info_hash: &str) -> requests::Announce { - requests::announce::QueryBuilder::new(info_hash.parse().expect("invalid infohash"), peer::Id::from(1), PORT).build() - } - #[tokio::test] async fn it_should_start_and_stop() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; @@ -136,11 +125,11 @@ mod for_all_config_modes { async fn should_respond_if_only_the_mandatory_fields_are_provided() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); params.remove_optional_params(); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_is_announce_response(response).await; @@ -151,7 +140,7 @@ mod for_all_config_modes { async fn should_fail_when_the_url_query_component_is_empty() { let env = Started::new(&configuration::ephemeral().into()).await; - let response = Client::new(*env.bind_address()).get("announce").await; + let response = create_client_response(&env, "announce").await; assert_missing_query_params_for_announce_request_error_response(response).await; @@ -164,9 +153,7 @@ mod for_all_config_modes { let invalid_query_param = "a=b=c"; - let response = Client::new(*env.bind_address()) - .get(&format!("announce?{invalid_query_param}")) - .await; + let response = create_client_response(&env, &format!("announce?{invalid_query_param}")).await; assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await; @@ -179,31 +166,31 @@ mod for_all_config_modes { // Without `info_hash` param - let mut params = default_prams(); + let mut params = create_default_announce_prams(); params.info_hash = None; - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "missing param info_hash").await; // Without `peer_id` param - let mut params = default_prams(); + let mut params = create_default_announce_prams(); params.peer_id = None; - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "missing param peer_id").await; // Without `port` param - let mut params = default_prams(); + let mut params = create_default_announce_prams(); params.port = None; - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "missing param port").await; @@ -214,9 +201,9 @@ mod for_all_config_modes { async fn it_should_return_an_empty_response_when_announcing_a_stopped_peer() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let query = default_query_builder().with_event(AnnounceEvent::Stopped).build(); + let query = create_announce_query(1, 1).with_event(AnnounceEvent::Stopped).build(); - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; assert_empty_announce_response(response, &env.tracker.get_announce_policy()).await; @@ -227,12 +214,12 @@ mod for_all_config_modes { async fn should_fail_when_the_info_hash_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); for invalid_value in &invalid_info_hashes() { params.set("info_hash", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_cannot_parse_query_params_error_response(response, "").await; } @@ -249,11 +236,11 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_is_announce_response(response).await; @@ -264,14 +251,14 @@ mod for_all_config_modes { async fn should_fail_when_the_downloaded_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = ["-1", "1.1", "a"]; for invalid_value in invalid_values { params.set("downloaded", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -283,14 +270,14 @@ mod for_all_config_modes { async fn should_fail_when_the_uploaded_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = ["-1", "1.1", "a"]; for invalid_value in invalid_values { params.set("uploaded", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -302,7 +289,7 @@ mod for_all_config_modes { async fn should_fail_when_the_peer_id_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = [ "0", @@ -316,7 +303,7 @@ mod for_all_config_modes { for invalid_value in invalid_values { params.set("peer_id", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -328,14 +315,14 @@ mod for_all_config_modes { async fn should_fail_when_the_port_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = ["-1", "1.1", "a"]; for invalid_value in invalid_values { params.set("port", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -347,14 +334,14 @@ mod for_all_config_modes { async fn should_fail_when_the_left_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = ["-1", "1.1", "a"]; for invalid_value in invalid_values { params.set("left", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -366,7 +353,7 @@ mod for_all_config_modes { async fn should_fail_when_the_event_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = [ "0", @@ -381,7 +368,7 @@ mod for_all_config_modes { for invalid_value in invalid_values { params.set("event", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -393,14 +380,14 @@ mod for_all_config_modes { async fn should_fail_when_the_compact_param_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; - let mut params = default_prams(); + let mut params = create_default_announce_prams(); let invalid_values = ["-1", "1.1", "a"]; for invalid_value in invalid_values { params.set("compact", invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_bad_announce_request_error_response(response, "invalid param value").await; } @@ -412,9 +399,11 @@ mod for_all_config_modes { async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let announce = with_infohash("9c38422213e30bff212b30c360d26f9a02136422"); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + let announce = create_announce_query(info_hash, 1).build(); - let response = Client::new(*env.bind_address()).announce(&announce).await; + let response = create_client_announce_response(&env, &announce).await; let expected_response = responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) // the peer for this test @@ -440,10 +429,10 @@ mod for_all_config_modes { // Add the Peer 1 env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT).build(); + let query = create_announce_query(info_hash, peer::Id(*b"-qB00000000000000002")).build(); // Announce the new Peer 2. This new peer is non included on the response peer list - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; let expected_response = responses::announce::ResponseBuilder::new(&env.tracker.get_announce_policy()) // the peer for this test @@ -480,10 +469,10 @@ mod for_all_config_modes { .build(); env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000003"), PORT).build(); + let query = create_announce_query(info_hash, peer::Id(*b"-qB00000000000000003")).build(); // Announce the new Peer. - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; // The newly announced peer is not included on the response peer list, // but all the previously announced peers should be included regardless the IP version they are using. @@ -510,9 +499,9 @@ mod for_all_config_modes { // Add a peer env.add_torrent_peer(&info_hash, &peer).await; - let query = requests::announce::QueryBuilder::new(info_hash, peer.peer_id, PORT).build(); + let query = create_announce_query(info_hash, peer.peer_id).build(); - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; assert_announce_response( response, @@ -542,12 +531,12 @@ mod for_all_config_modes { // Add the Peer 1 env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT) + let query = create_announce_query(info_hash, peer::Id(*b"-qB00000000000000002")) .with_compact() .build(); // Announce the new Peer 2 accepting compact responses - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; let expected_response = responses::announce::Compact { complete: 2, @@ -584,9 +573,9 @@ mod for_all_config_modes { // Announce the new Peer 2 without passing the "compact" param // By default it should respond with the compact peer list // https://www.bittorrent.org/beps/bep_0023.html - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id(*b"-qB00000000000000002"), PORT).build(); + let query = create_announce_query(info_hash, peer::Id(*b"-qB00000000000000002")).build(); - let response = Client::new(*env.bind_address()).announce(&query).await; + let response = create_client_announce_response(&env, &query).await; assert!(!is_a_compact_announce_response(response).await); @@ -603,37 +592,13 @@ mod for_all_config_modes { async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - Client::new(*env.bind_address()) - .announce(&default_query_builder().build()) - .await; - - let stats = env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_connections_handled, 1); - - drop(stats); - - env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { - if TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0)) - .await - .is_err() - { - return; // we cannot bind to a ipv6 socket, so we will skip this test - } - - let env = Started::new(&configuration::ephemeral_ipv6().into()).await; + let query = create_announce_query(1, 1).build(); - Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&default_query_builder().build()) - .await; + drop(create_client_announce_response(&env, &query).await); let stats = env.tracker.get_stats().await; - assert_eq!(stats.tcp6_connections_handled, 1); + assert_eq!(stats.tcp4_connections_handled, 1); drop(stats); @@ -646,34 +611,15 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - Client::new(*env.bind_address()) - .announce( - &default_query_builder() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .build(), - ) - .await; - - let stats = env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_connections_handled, 0); - - drop(stats); - - env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { - let env = Started::new(&configuration::ephemeral_mode_public().into()).await; + let query = create_announce_query(1, 1) + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .build(); - Client::new(*env.bind_address()) - .announce(&default_query_builder().build()) - .await; + drop(create_client_announce_response(&env, &query).await); let stats = env.tracker.get_stats().await; - assert_eq!(stats.tcp4_announces_handled, 1); + assert_eq!(stats.tcp6_connections_handled, 0); drop(stats); @@ -691,9 +637,9 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_ipv6().into()).await; - Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&default_query_builder().build()) - .await; + let query = create_announce_query(1, 1).build(); + + drop(create_client_announce_response(&env, &query).await); let stats = env.tracker.get_stats().await; @@ -710,13 +656,11 @@ mod for_all_config_modes { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - Client::new(*env.bind_address()) - .announce( - &default_query_builder() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .build(), - ) - .await; + let query = create_announce_query(1, 1) + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .build(); + + drop(create_client_announce_response(&env, &query).await); let stats = env.tracker.get_stats().await; @@ -734,15 +678,13 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let client_ip = local_ip().unwrap(); - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) + let query = create_announce_query(info_hash, 1) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) .build(); { - let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&query).await.status(); - - assert_eq!(status, StatusCode::OK); + let response = create_bonded_client_announce_response(&env, client_ip, &query).await; + assert_eq!(response.status(), StatusCode::OK); } let peers = env.tracker.get_torrent_peers(&info_hash); @@ -769,15 +711,13 @@ mod for_all_config_modes { let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); let client_ip = loopback_ip; - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) + let query = create_announce_query(info_hash, 1) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) .build(); { - let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&query).await.status(); - - assert_eq!(status, StatusCode::OK); + let response = create_bonded_client_announce_response(&env, client_ip, &query).await; + assert_eq!(response.status(), StatusCode::OK); } let peers = env.tracker.get_torrent_peers(&info_hash); @@ -808,15 +748,13 @@ mod for_all_config_modes { let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); let client_ip = loopback_ip; - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT) + let query = create_announce_query(info_hash, 1) .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) .build(); { - let client = Client::bind(*env.bind_address(), client_ip); - let status = client.announce(&query).await.status(); - - assert_eq!(status, StatusCode::OK); + let response = create_bonded_client_announce_response(&env, client_ip, &query).await; + assert_eq!(response.status(), StatusCode::OK); } let peers = env.tracker.get_torrent_peers(&info_hash); @@ -841,20 +779,19 @@ mod for_all_config_modes { let info_hash: InfoHash = "9c38422213e30bff212b30c360d26f9a02136422".parse().expect("it should parse"); - let query = requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build(); + let query = create_announce_query(info_hash, 1).build(); { - let client = Client::new(*env.bind_address()); - let status = client + let client = create_default_client(&env) .announce_with_header( &query, "X-Forwarded-For", "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", ) .await - .status(); + .expect("it should get a response"); - assert_eq!(status, StatusCode::OK); + assert_eq!(client.status(), StatusCode::OK); } let peers = env.tracker.get_torrent_peers(&info_hash); @@ -892,14 +829,16 @@ mod for_all_config_modes { assert_cannot_parse_query_params_error_response, assert_missing_query_params_for_scrape_request_error_response, assert_scrape_response, }; - use crate::servers::http::client::Client; + use crate::servers::http::v1::{ + create_bonded_client_scrape_response, create_client_response, create_client_scrape_response, create_scrape_query, + }; use crate::servers::http::Started; //#[tokio::test] #[allow(dead_code)] async fn should_fail_when_the_request_is_empty() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let response = Client::new(*env.bind_address()).get("scrape").await; + let response = create_client_response(&env, "scrape").await; assert_missing_query_params_for_scrape_request_error_response(response).await; @@ -910,12 +849,14 @@ mod for_all_config_modes { async fn should_fail_when_the_info_hash_param_is_invalid() { let env = Started::new(&configuration::ephemeral_mode_public().into()).await; - let mut params: requests::scrape::QueryParams = requests::scrape::QueryBuilder::default().build().into(); + let query = create_scrape_query::<&InfoHash>(None).build(); + + let mut params = requests::scrape::QueryParams::from(query); for invalid_value in &invalid_info_hashes() { params.set_one_info_hash_param(invalid_value); - let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + let response = create_client_response(&env, &format!("announce?{params}")).await; assert_cannot_parse_query_params_error_response(response, "").await; } @@ -938,9 +879,9 @@ mod for_all_config_modes { ) .await; - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::default() .add_file(( @@ -973,9 +914,9 @@ mod for_all_config_modes { ) .await; - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::default() .add_file(( @@ -999,9 +940,9 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); @@ -1017,14 +958,12 @@ mod for_all_config_modes { let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); - let response = Client::new(*env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .add_info_hash(&info_hash1) - .add_info_hash(&info_hash2) - .build(), - ) - .await; + let query = requests::scrape::QueryBuilder::default() + .add_info_hash(&info_hash1) + .add_info_hash(&info_hash2) + .build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::default() .add_file((info_hash1.bytes(), File::zeroed())) @@ -1042,9 +981,9 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + drop(create_client_scrape_response(&env, &query).await); let stats = env.tracker.get_stats().await; @@ -1068,9 +1007,9 @@ mod for_all_config_modes { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + drop(create_bonded_client_scrape_response(&env, IpAddr::from_str("::1").unwrap(), &query).await); let stats = env.tracker.get_stats().await; @@ -1088,14 +1027,11 @@ mod configured_as_whitelisted { mod and_receiving_an_announce_request { use std::str::FromStr; - use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; use torrust_tracker_primitives::info_hash::InfoHash; - use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; - use crate::servers::http::client::Client; - use crate::servers::http::v1::contract::PORT; + use crate::servers::http::v1::{create_announce_query, create_client_announce_response}; use crate::servers::http::Started; #[tokio::test] @@ -1104,9 +1040,9 @@ mod configured_as_whitelisted { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*env.bind_address()) - .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) - .await; + let query = create_announce_query(info_hash, 1).build(); + + let response = create_client_announce_response(&env, &query).await; assert_torrent_not_in_whitelist_error_response(response).await; @@ -1124,9 +1060,9 @@ mod configured_as_whitelisted { .await .expect("should add the torrent to the whitelist"); - let response = Client::new(*env.bind_address()) - .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) - .await; + let query = create_announce_query(info_hash, 1).build(); + + let response = create_client_announce_response(&env, &query).await; assert_is_announce_response(response).await; @@ -1137,15 +1073,15 @@ mod configured_as_whitelisted { mod receiving_an_scrape_request { use std::str::FromStr; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses; use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape::File; - use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::assert_scrape_response; - use crate::servers::http::client::Client; + use crate::servers::http::v1::{create_client_scrape_response, create_scrape_query}; use crate::servers::http::Started; #[tokio::test] @@ -1163,9 +1099,9 @@ mod configured_as_whitelisted { ) .await; - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); @@ -1194,9 +1130,9 @@ mod configured_as_whitelisted { .await .expect("should add the torrent to the whitelist"); - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::from(( info_hash.bytes(), @@ -1222,14 +1158,13 @@ mod configured_as_private { use std::time::Duration; use torrust_tracker::core::auth::Key; - use torrust_tracker::shared::bit_torrent::tracker::http::client::requests; use torrust_tracker_primitives::info_hash::InfoHash; - use torrust_tracker_primitives::peer; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; - use crate::servers::http::client::Client; - use crate::servers::http::v1::contract::PORT; + use crate::servers::http::v1::{ + create_announce_query, create_authenticated_client, create_client_announce_response, create_client_response, + }; use crate::servers::http::Started; #[tokio::test] @@ -1238,9 +1173,12 @@ mod configured_as_private { let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(*env.bind_address(), expiring_key.key()) - .announce(&requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build()) - .await; + let query = create_announce_query(1, 1).build(); + + let response = create_authenticated_client(&env, expiring_key.key()) + .announce(&query) + .await + .expect("it should get a response"); assert_is_announce_response(response).await; @@ -1253,9 +1191,9 @@ mod configured_as_private { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*env.bind_address()) - .announce(&requests::announce::QueryBuilder::new(info_hash, peer::Id::from(1), PORT).build()) - .await; + let query = create_announce_query(info_hash, 1).build(); + + let response = create_client_announce_response(&env, &query).await; assert_authentication_error_response(response).await; @@ -1268,11 +1206,12 @@ mod configured_as_private { let invalid_key = "INVALID_KEY"; - let response = Client::new(*env.bind_address()) - .get(&format!( - "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port={PORT}&left=0&event=completed&compact=0" - )) - .await; + let path = format!( + "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port={}&left=0&event=completed&compact=0", + crate::servers::http::v1::PORT + ); + + let response = create_client_response(&env, &path).await; assert_authentication_error_response(response).await; } @@ -1284,9 +1223,10 @@ mod configured_as_private { // The tracker does not have this key let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); - let response = Client::authenticated(*env.bind_address(), unregistered_key) - .announce(&requests::announce::QueryBuilder::new(InfoHash::from(1), peer::Id::from(1), PORT).build()) - .await; + let response = create_authenticated_client(&env, unregistered_key) + .announce(&create_announce_query(1, 1).build()) + .await + .expect("it should get a response"); assert_authentication_error_response(response).await; @@ -1300,15 +1240,17 @@ mod configured_as_private { use std::time::Duration; use torrust_tracker::core::auth::Key; + use torrust_tracker::shared::bit_torrent::tracker::http::client::responses; use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape::File; - use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, responses}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use torrust_tracker_primitives::peer::fixture::PeerBuilder; use torrust_tracker_test_helpers::configuration; use crate::servers::http::asserts::{assert_authentication_error_response, assert_scrape_response}; - use crate::servers::http::client::Client; + use crate::servers::http::v1::{ + create_authenticated_client, create_client_response, create_client_scrape_response, create_scrape_query, + }; use crate::servers::http::Started; #[tokio::test] @@ -1317,11 +1259,9 @@ mod configured_as_private { let invalid_key = "INVALID_KEY"; - let response = Client::new(*env.bind_address()) - .get(&format!( - "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0" - )) - .await; + let path = &format!("scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"); + + let response = create_client_response(&env, path).await; assert_authentication_error_response(response).await; } @@ -1341,9 +1281,9 @@ mod configured_as_private { ) .await; - let response = Client::new(*env.bind_address()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_client_scrape_response(&env, &query).await; let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); @@ -1369,9 +1309,12 @@ mod configured_as_private { let expiring_key = env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(*env.bind_address(), expiring_key.key()) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_authenticated_client(&env, expiring_key.key()) + .scrape(&query) + .await + .expect("it should get a response"); let expected_scrape_response = responses::scrape::ResponseBuilder::from(( info_hash.bytes(), @@ -1408,9 +1351,12 @@ mod configured_as_private { let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); - let response = Client::authenticated(*env.bind_address(), false_key) - .scrape(&requests::scrape::QueryBuilder::from(&info_hash).build()) - .await; + let query = create_scrape_query(Some(&info_hash)).build(); + + let response = create_authenticated_client(&env, false_key) + .scrape(&query) + .await + .expect("it should get a response"); let expected_scrape_response = responses::scrape::ResponseBuilder::from((info_hash.bytes(), File::zeroed())).build(); diff --git a/tests/servers/http/v1/mod.rs b/tests/servers/http/v1/mod.rs index 2943dbb50..f70f72f5d 100644 --- a/tests/servers/http/v1/mod.rs +++ b/tests/servers/http/v1/mod.rs @@ -1 +1,97 @@ +use std::net::IpAddr; + +use reqwest::Response; +use torrust_tracker::core::auth::Key; +use torrust_tracker::servers::http::server::Running; +use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, Client}; +use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::peer; + +use super::environment::Environment; +use super::TIMEOUT; + pub mod contract; + +pub(crate) const PORT: u16 = 17548; + +pub(crate) fn create_default_client(env: &Environment) -> Client { + let url: url::Url = format!("http://{}/", &env.bind_address()) + .parse() + .expect("it should make a valid url"); + Client::new(url, TIMEOUT).expect("it should make a client") +} + +pub(crate) fn create_bonded_client(env: &Environment, local_address: IpAddr) -> Client { + let url: url::Url = format!("http://{}/", &env.bind_address()) + .parse() + .expect("it should make a valid url"); + Client::bind(url, TIMEOUT, local_address).expect("it should make a client") +} + +pub(crate) fn create_authenticated_client(env: &Environment, key: Key) -> Client { + let url: url::Url = format!("http://{}/", &env.bind_address()) + .parse() + .expect("it should make a valid url"); + Client::authenticated(url, TIMEOUT, key).expect("it should make a client") +} + +pub(crate) async fn create_client_response(env: &Environment, path: &str) -> Response { + create_default_client(env).get(path).await.expect("it should get a response") +} + +pub(crate) async fn create_client_announce_response(env: &Environment, query: &requests::Announce) -> Response { + create_default_client(env) + .announce(query) + .await + .expect("it should get a response") +} + +pub(crate) async fn create_client_scrape_response(env: &Environment, query: &requests::Scrape) -> Response { + create_default_client(env) + .scrape(query) + .await + .expect("it should get a response") +} + +pub(crate) async fn create_bonded_client_announce_response( + env: &Environment, + local_address: IpAddr, + query: &requests::Announce, +) -> Response { + create_bonded_client(env, local_address) + .announce(query) + .await + .expect("it should get a response") +} + +pub(crate) async fn create_bonded_client_scrape_response( + env: &Environment, + local_address: IpAddr, + query: &requests::Scrape, +) -> Response { + create_bonded_client(env, local_address) + .scrape(query) + .await + .expect("it should get a response") +} + +pub(crate) fn create_announce_query, P: Into>( + info_hash: I, + peer_id: P, +) -> requests::announce::QueryBuilder { + requests::announce::QueryBuilder::new(info_hash.into(), peer_id.into(), PORT) +} + +pub(crate) fn create_scrape_query>( + info_hash: Option, +) -> requests::scrape::QueryBuilder { + if let Some(info_hash) = info_hash { + info_hash.into() + } else { + requests::scrape::QueryBuilder::default() + } +} + +pub(crate) fn create_default_announce_prams() -> requests::announce::QueryParams { + (&create_announce_query(1, 1).build()).into() +} From 2a72445f1004f4d417ed1b41ebec15881cdfa4d4 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Sat, 30 Mar 2024 11:07:32 +0800 Subject: [PATCH 005/217] dev: udp client timeouts and errors --- Cargo.toml | 8 +- packages/configuration/src/lib.rs | 14 + packages/torrent-repository/Cargo.toml | 10 +- src/console/clients/checker/checks/udp.rs | 159 +++++--- src/console/clients/checker/service.rs | 6 +- src/console/clients/udp/app.rs | 19 +- src/console/clients/udp/checker.rs | 182 +++------ src/console/clients/udp/mod.rs | 24 ++ src/servers/udp/server.rs | 14 +- src/shared/bit_torrent/tracker/udp/client.rs | 374 ++++++++++++------- src/shared/bit_torrent/tracker/udp/mod.rs | 55 ++- tests/servers/health_check_api/contract.rs | 13 +- tests/servers/udp/contract.rs | 81 ++-- 13 files changed, 556 insertions(+), 403 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 623971552..512fd8c22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,9 +62,9 @@ serde_bytes = "0" serde_json = "1" serde_repr = "0" thiserror = "1" -tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } -torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "packages/clock" } +torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = "contrib/bencode" } torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "packages/located-error" } torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" } @@ -89,7 +89,7 @@ members = [ "packages/located-error", "packages/primitives", "packages/test-helpers", - "packages/torrent-repository" + "packages/torrent-repository", ] [profile.dev] @@ -104,4 +104,4 @@ opt-level = 3 [target.x86_64-unknown-linux-gnu] linker = "/usr/bin/clang" -rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] \ No newline at end of file +rustflags = ["-Clink-arg=-Wl,--no-rosegment", "-Clink-arg=-fuse-ld=lld"] diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index ca873f3cd..4d05a6c23 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -233,6 +233,7 @@ use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; use std::sync::Arc; +use std::time::Duration; use std::{env, fs}; use config::{Config, ConfigError, File, FileFormat}; @@ -246,6 +247,19 @@ use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; +/// Client Timeout +pub const CLIENT_TIMEOUT_DEFAULT: Duration = Duration::from_secs(5); + +/// The maximum number of bytes in a UDP packet. +pub const MAX_PACKET_SIZE: usize = 1496; + +/// The a free port is dynamically chosen by the operating system. +pub const PORT_ASSIGNED_BY_OS: u16 = 0; + +/// A magic 64-bit integer constant defined in the protocol that is used to +/// identify the protocol. +pub const PROTOCOL_ID: i64 = 0x0417_2710_1980; + #[derive(Copy, Clone, Debug, PartialEq, Constructor)] pub struct TrackerPolicy { pub remove_peerless_torrents: bool, diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 4cea8767f..8614be496 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -1,6 +1,6 @@ [package] description = "A library that provides a repository of torrents files and their peers." -keywords = ["torrents", "repository", "library"] +keywords = ["library", "repository", "torrents"] name = "torrust-tracker-torrent-repository" readme = "README.md" @@ -17,15 +17,15 @@ version.workspace = true [dependencies] futures = "0.3.29" -tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } -torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } -torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } +torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } +torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } [dev-dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } criterion = { version = "0", features = ["async_tokio"] } rstest = "0" -async-std = {version = "1", features = ["attributes", "tokio1"] } [[bench]] harness = false diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index fae489f44..6404e92ef 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use std::time::Duration; -use aquatic_udp_protocol::{Port, TransactionId}; use colored::Colorize; use hex_literal::hex; use log::debug; @@ -10,83 +9,121 @@ use torrust_tracker_primitives::info_hash::InfoHash; use crate::console::clients::checker::console::Console; use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; -use crate::console::clients::udp::checker; - -const ASSIGNED_BY_OS: u16 = 0; -const RANDOM_TRANSACTION_ID: i32 = -888_840_697; +use crate::console::clients::udp::checker::{self, Client}; +use crate::console::clients::udp::Error; pub async fn run(udp_trackers: Vec, _: Duration, console: Console) -> Vec { let mut check_results = Vec::default(); console.println("UDP trackers ..."); - for ref udp_tracker in udp_trackers { - debug!("UDP tracker: {:?}", udp_tracker); - - let colored_tracker_url = udp_tracker.to_string().yellow(); - - let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); - - let mut client = checker::Client::default(); - - debug!("Bind and connect"); + let info_hash = InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422")); // # DevSkim: ignore DS173237 + + for ref addr in udp_trackers { + debug!("UDP tracker: {:?}", addr); + + let colored_addr = addr.to_string().yellow(); + + // Setup Connection + let Ok((client, ctx)) = ({ + let res = setup_connection(addr).await; + + check_results.push(match res { + Ok(_) => { + console.println(&format!("{} - Setup of {} is OK", "✓".green(), colored_addr)); + Ok(()) + } + Err(ref e) => { + console.println(&format!("{} - Setup of {} is failing", "✗".red(), colored_addr)); + Err(CheckError::UdpCheckError { + addr: *addr, + err: e.clone(), + }) + } + }); + + res + }) else { + break; + }; - let Ok(bound_to) = client.bind_and_connect(ASSIGNED_BY_OS, udp_tracker).await else { - check_results.push(Err(CheckError::UdpError { - socket_addr: *udp_tracker, - })); - console.println(&format!("{} - Can't connect to socket {}", "✗".red(), colored_tracker_url)); + // Do Announce + if { + let res = check_udp_announce(&client, &ctx, info_hash).await; + + check_results.push(match res { + Ok(_) => { + console.println(&format!("{} - Announce of {} is OK", "✓".green(), colored_addr)); + Ok(()) + } + Err(ref e) => { + console.println(&format!("{} - Announce of {} is failing", "✗".red(), colored_addr)); + Err(CheckError::UdpCheckError { + addr: *addr, + err: e.clone(), + }) + } + }); + + res + } + .is_err() + { break; }; - debug!("Send connection request"); - - let Ok(connection_id) = client.send_connection_request(transaction_id).await else { - check_results.push(Err(CheckError::UdpError { - socket_addr: *udp_tracker, - })); - console.println(&format!( - "{} - Can't make tracker connection request to {}", - "✗".red(), - colored_tracker_url - )); + // Do Scrape + if { + let res = check_udp_scrape(&client, &ctx, vec![info_hash]).await; + + check_results.push(match res { + Ok(_) => { + console.println(&format!("{} - Announce of {} is OK", "✓".green(), colored_addr)); + Ok(()) + } + Err(ref e) => { + console.println(&format!("{} - Announce of {} is failing", "✗".red(), colored_addr)); + Err(CheckError::UdpCheckError { + addr: *addr, + err: e.clone(), + }) + } + }); + res + } + .is_err() + { break; }; + } - let info_hash = InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422")); // # DevSkim: ignore DS173237 + check_results +} - debug!("Send announce request"); +async fn setup_connection(addr: &SocketAddr) -> Result<(Client, aquatic_udp_protocol::ConnectResponse), Error> { + let client = checker::Client::bind_and_connect(addr).await?; - if (client - .send_announce_request(connection_id, transaction_id, info_hash, Port(bound_to.port())) - .await) - .is_ok() - { - check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); - } else { - let err = CheckError::UdpError { - socket_addr: *udp_tracker, - }; - check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); - } + let transaction_id = aquatic_udp_protocol::TransactionId(rand::Rng::gen(&mut rand::thread_rng())); - debug!("Send scrape request"); + let ctx = client.send_connection_request(transaction_id).await?; - let info_hashes = vec![InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422"))]; // # DevSkim: ignore DS173237 + Ok((client, ctx)) +} - if (client.send_scrape_request(connection_id, transaction_id, info_hashes).await).is_ok() { - check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); - } else { - let err = CheckError::UdpError { - socket_addr: *udp_tracker, - }; - check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); - } - } +async fn check_udp_announce( + client: &Client, + ctx: &aquatic_udp_protocol::ConnectResponse, + info_hash: InfoHash, +) -> Result { + client + .send_announce_request(ctx, info_hash, aquatic_udp_protocol::Port(client.local_addr()?.port())) + .await +} - check_results +async fn check_udp_scrape( + client: &Client, + ctx: &aquatic_udp_protocol::ConnectResponse, + infohashes: Vec, +) -> Result { + client.send_scrape_request(ctx, infohashes).await } diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index 8ec5a4623..f47051046 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -10,7 +10,7 @@ use super::checks; use super::config::Configuration; use super::console::Console; use crate::console::clients::checker::printer::Printer; -use crate::console::clients::http; +use crate::console::clients::{http, udp}; pub struct Service { pub(crate) config: Arc, @@ -21,8 +21,8 @@ pub type CheckResult = Result<(), CheckError>; #[derive(Debug, Clone, Error)] pub enum CheckError { - #[error("Error In Udp: socket: {socket_addr:?}")] - UdpError { socket_addr: SocketAddr }, + #[error("Error In Udp: {addr:?}")] + UdpCheckError { addr: SocketAddr, err: udp::Error }, #[error("Error In Http: url: {url:?}")] HttpCheckError { url: Url, err: http::Error }, #[error("Error In HeathCheck: url: {url:?}")] diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 540a25f30..983d3f4ea 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -70,7 +70,6 @@ use url::Url; use crate::console::clients::udp::checker; use crate::console::clients::udp::responses::{AnnounceResponseDto, ScrapeResponseDto}; -const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; #[derive(Parser, Debug)] @@ -144,29 +143,29 @@ fn setup_logging(level: LevelFilter) { async fn handle_announce(tracker_socket_addr: &SocketAddr, info_hash: &TorrustInfoHash) -> anyhow::Result { let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); - let mut client = checker::Client::default(); + let client = checker::Client::bind_and_connect(tracker_socket_addr).await?; - let bound_to = client.bind_and_connect(ASSIGNED_BY_OS, tracker_socket_addr).await?; + let bound_to = client.client.local_addr()?; - let connection_id = client.send_connection_request(transaction_id).await?; + let ctx = client.send_connection_request(transaction_id).await?; client - .send_announce_request(connection_id, transaction_id, *info_hash, Port(bound_to.port())) + .send_announce_request(&ctx, *info_hash, Port(bound_to.port())) .await + .context("failed to handle announce") } async fn handle_scrape(tracker_socket_addr: &SocketAddr, info_hashes: &[TorrustInfoHash]) -> anyhow::Result { let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); - let mut client = checker::Client::default(); + let client = checker::Client::bind_and_connect(tracker_socket_addr).await?; - let _bound_to = client.bind_and_connect(ASSIGNED_BY_OS, tracker_socket_addr).await?; - - let connection_id = client.send_connection_request(transaction_id).await?; + let ctx = client.send_connection_request(transaction_id).await?; client - .send_scrape_request(connection_id, transaction_id, info_hashes.to_vec()) + .send_scrape_request(&ctx, info_hashes.to_vec()) .await + .context("failed to handle scrape") } fn print_response(response: Response) -> anyhow::Result<()> { diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index 12b8d764c..d280d1ea7 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -1,42 +1,20 @@ use std::net::{Ipv4Addr, SocketAddr}; -use anyhow::Context; use aquatic_udp_protocol::common::InfoHash; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, + AnnounceEvent, AnnounceRequest, ConnectResponse, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, ScrapeRequest, TransactionId, }; use log::debug; -use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; -use crate::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient}; - -#[derive(Error, Debug)] -pub enum ClientError { - #[error("Local socket address is not bound yet. Try binding before connecting.")] - NotBound, - #[error("Not connected to remote tracker UDP socket. Try connecting before making requests.")] - NotConnected, - #[error("Unexpected response while connecting the the remote server.")] - UnexpectedConnectionResponse, -} +use super::Error; +use crate::shared::bit_torrent::tracker::udp; /// A UDP Tracker client to make test requests (checks). -#[derive(Debug, Default)] +#[derive(Debug, Clone)] pub struct Client { - /// Local UDP socket. It could be 0 to assign a free port. - local_binding_address: Option, - - /// Local UDP socket after binding. It's equals to binding address if a - /// non- zero port was used. - local_bound_address: Option, - - /// Remote UDP tracker socket - remote_socket: Option, - - /// The client used to make UDP requests to the tracker. - udp_tracker_client: Option, + pub client: udp::client::Client, } impl Client { @@ -48,87 +26,35 @@ impl Client { /// /// - It can't bound to the local socket address. /// - It can't make a connection request successfully to the remote UDP server. - pub async fn bind_and_connect(&mut self, local_port: u16, remote_socket_addr: &SocketAddr) -> anyhow::Result { - let bound_to = self.bind(local_port).await?; - self.connect(remote_socket_addr).await?; - Ok(bound_to) - } - - /// Binds local client socket. - /// - /// # Errors - /// - /// Will return an error if it can't bound to the local address. - async fn bind(&mut self, local_port: u16) -> anyhow::Result { - let local_bind_to = format!("0.0.0.0:{local_port}"); - let binding_address = local_bind_to.parse().context("binding local address")?; - - debug!("Binding to: {local_bind_to}"); - let udp_client = UdpClient::bind(&local_bind_to).await; - - let bound_to = udp_client.socket.local_addr().context("bound local address")?; - debug!("Bound to: {bound_to}"); + pub async fn bind_and_connect(addr: &SocketAddr) -> Result { + let client = udp::client::Client::connect(*addr) + .await + .map_err(|err| Error::UnableToBindAndConnect { addr: *addr, err })?; - self.local_binding_address = Some(binding_address); - self.local_bound_address = Some(bound_to); - - self.udp_tracker_client = Some(UdpTrackerClient { udp_client }); - - Ok(bound_to) + Ok(Self { client }) } - /// Connects to the remote server socket. + /// Returns the local address of this [`Client`]. /// /// # Errors /// - /// Will return and error if it can't make a connection request successfully - /// to the remote UDP server. - async fn connect(&mut self, tracker_socket_addr: &SocketAddr) -> anyhow::Result<()> { - debug!("Connecting to tracker: udp://{tracker_socket_addr}"); - - match &self.udp_tracker_client { - Some(client) => { - client.udp_client.connect(&tracker_socket_addr.to_string()).await; - self.remote_socket = Some(*tracker_socket_addr); - Ok(()) - } - None => Err(ClientError::NotBound.into()), - } + /// This function errors if the underlying call fails. + /// + pub fn local_addr(&self) -> Result { + self.client.local_addr().map_err(|err| Error::UnableToGetLocalAddr { err }) } /// Sends a connection request to the UDP Tracker server. /// /// # Errors /// - /// Will return and error if + /// Will return and error if unable to get a successful response. /// - /// - It can't connect to the remote UDP socket. - /// - It can't make a connection request successfully to the remote UDP - /// server (after successfully connecting to the remote UDP socket). - /// - /// # Panics - /// - /// Will panic if it receives an unexpected response. - pub async fn send_connection_request(&self, transaction_id: TransactionId) -> anyhow::Result { - debug!("Sending connection request with transaction id: {transaction_id:#?}"); - - let connect_request = ConnectRequest { transaction_id }; - - match &self.udp_tracker_client { - Some(client) => { - client.send(connect_request.into()).await; - - let response = client.receive().await; - - debug!("connection request response:\n{response:#?}"); - - match response { - Response::Connect(connect_response) => Ok(connect_response.connection_id), - _ => Err(ClientError::UnexpectedConnectionResponse.into()), - } - } - None => Err(ClientError::NotConnected.into()), - } + pub async fn send_connection_request(&self, transaction_id: TransactionId) -> Result { + self.client + .do_connection_request(transaction_id) + .await + .map_err(|err| Error::UnexpectedConnectionResponse { err }) } /// Sends an announce request to the UDP Tracker server. @@ -139,16 +65,15 @@ impl Client { /// before calling this function. pub async fn send_announce_request( &self, - connection_id: ConnectionId, - transaction_id: TransactionId, + ctx: &ConnectResponse, info_hash: TorrustInfoHash, client_port: Port, - ) -> anyhow::Result { - debug!("Sending announce request with transaction id: {transaction_id:#?}"); + ) -> Result { + debug!("Sending announce request with transaction id: {:#?}", ctx.transaction_id); let announce_request = AnnounceRequest { - connection_id, - transaction_id, + connection_id: ctx.connection_id, + transaction_id: ctx.transaction_id, info_hash: InfoHash(info_hash.bytes()), peer_id: PeerId(*b"-qB00000000000000001"), bytes_downloaded: NumberOfBytes(0i64), @@ -161,18 +86,21 @@ impl Client { port: client_port, }; - match &self.udp_tracker_client { - Some(client) => { - client.send(announce_request.into()).await; + let _ = self + .client + .send_request(announce_request.into()) + .await + .map_err(|err| Error::UnableToSendRequest { err })?; - let response = client.receive().await; + let response = self + .client + .receive_response() + .await + .map_err(|err| Error::UnableToReceiveResponse { err })?; - debug!("announce request response:\n{response:#?}"); + debug!("announce request response:\n{response:#?}"); - Ok(response) - } - None => Err(ClientError::NotConnected.into()), - } + Ok(response) } /// Sends a scrape request to the UDP Tracker server. @@ -181,34 +109,32 @@ impl Client { /// /// Will return and error if the client is not connected. You have to connect /// before calling this function. - pub async fn send_scrape_request( - &self, - connection_id: ConnectionId, - transaction_id: TransactionId, - info_hashes: Vec, - ) -> anyhow::Result { - debug!("Sending scrape request with transaction id: {transaction_id:#?}"); + pub async fn send_scrape_request(&self, ctx: &ConnectResponse, info_hashes: Vec) -> Result { + debug!("Sending scrape request with transaction id: {:#?}", ctx.transaction_id); let scrape_request = ScrapeRequest { - connection_id, - transaction_id, + connection_id: ctx.connection_id, + transaction_id: ctx.transaction_id, info_hashes: info_hashes .iter() .map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes())) .collect(), }; - match &self.udp_tracker_client { - Some(client) => { - client.send(scrape_request.into()).await; + let _ = self + .client + .send_request(scrape_request.into()) + .await + .map_err(|err| Error::UnableToSendRequest { err })?; - let response = client.receive().await; + let response = self + .client + .receive_response() + .await + .map_err(|err| Error::UnableToReceiveResponse { err })?; - debug!("scrape request response:\n{response:#?}"); + debug!("scrape request response:\n{response:#?}"); - Ok(response) - } - None => Err(ClientError::NotConnected.into()), - } + Ok(response) } } diff --git a/src/console/clients/udp/mod.rs b/src/console/clients/udp/mod.rs index 2fcb26ed0..52931d1d8 100644 --- a/src/console/clients/udp/mod.rs +++ b/src/console/clients/udp/mod.rs @@ -1,3 +1,27 @@ +use std::net::SocketAddr; + +use thiserror::Error; + +use crate::shared::bit_torrent::tracker::udp; + pub mod app; pub mod checker; pub mod responses; + +#[derive(Error, Debug, Clone)] +pub enum Error { + #[error("Failed to Connect to: {addr}, with error: {err}")] + UnableToBindAndConnect { addr: SocketAddr, err: udp::Error }, + + #[error("Failed to receive a response, with error: {err}")] + UnableToReceiveResponse { err: udp::Error }, + + #[error("Failed to send a request, with error: {err}")] + UnableToSendRequest { err: udp::Error }, + + #[error("Failed to get local address for connection: {err}")] + UnableToGetLocalAddr { err: udp::Error }, + + #[error("Failed to get a successful connection response: {err}")] + UnexpectedConnectionResponse { err: udp::Error }, +} diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 7086b6ab7..77432a957 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -23,12 +23,14 @@ use std::sync::Arc; use aquatic_udp_protocol::Response; use derive_more::Constructor; +use futures::TryFutureExt; use log::{debug, error, info, trace}; use ringbuf::{Rb, StaticRb}; use tokio::net::UdpSocket; use tokio::sync::oneshot; use tokio::task::{AbortHandle, JoinHandle}; use tokio::{select, task}; +use torrust_tracker_configuration::MAX_PACKET_SIZE; use super::UdpRequest; use crate::bootstrap::jobs::Started; @@ -36,8 +38,7 @@ use crate::core::Tracker; use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; use crate::servers::signals::{shutdown_signal_with_message, Halted}; use crate::servers::udp::handlers; -use crate::shared::bit_torrent::tracker::udp::client::check; -use crate::shared::bit_torrent::tracker::udp::MAX_PACKET_SIZE; +use crate::shared::bit_torrent::tracker::udp::client::Client; /// Error that can occur when starting or stopping the UDP server. /// @@ -373,13 +374,12 @@ impl Udp { drop(socket.send_to(payload, remote_addr).await); } - fn check(binding: &SocketAddr) -> ServiceHealthCheckJob { - let binding = *binding; - let info = format!("checking the udp tracker health check at: {binding}"); + fn check(&addr: &SocketAddr) -> ServiceHealthCheckJob { + let info = format!("checking the udp tracker health check at: {addr}"); - let job = tokio::spawn(async move { check(&binding).await }); + let job = tokio::spawn(Client::connect(addr).and_then(Client::check).map_err(|e| e.to_string())); - ServiceHealthCheckJob::new(binding, info, job) + ServiceHealthCheckJob::new(addr, info, job) } } diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 11c8d8f62..ac79a59d2 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -3,209 +3,313 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; -use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId}; +use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Request, Response, TransactionId}; +use derive_more::{AsRef, Constructor, From, Into}; use log::debug; use tokio::net::UdpSocket; use tokio::time; +use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE, PORT_ASSIGNED_BY_OS}; -use crate::shared::bit_torrent::tracker::udp::{source_address, MAX_PACKET_SIZE}; +use super::{source_address, Error}; -/// Default timeout for sending and receiving packets. And waiting for sockets -/// to be readable and writable. -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); +#[derive(From, Into, AsRef, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct LocalSocketAddr(SocketAddr); +impl Default for LocalSocketAddr { + fn default() -> Self { + Self(source_address(PORT_ASSIGNED_BY_OS)) + } +} + +#[derive(From, Into, AsRef, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct Timeout(Duration); + +impl Default for Timeout { + fn default() -> Self { + Self(CLIENT_TIMEOUT_DEFAULT) + } +} + +#[derive(Constructor, Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Launcher { + bind_to: LocalSocketAddr, + timeout: Duration, +} -#[allow(clippy::module_name_repetitions)] #[derive(Debug)] -pub struct UdpClient { - /// The socket to connect to - pub socket: Arc, +pub struct Bound { + launcher: Launcher, + sock: UdpSocket, +} - /// Timeout for sending and receiving packets - pub timeout: Duration, +#[derive(Debug, Clone)] +pub struct Client { + launcher: Launcher, + sock: Arc, } -impl UdpClient { - /// # Panics +impl Launcher { + /// # Errors + /// + /// Will error if the local address can't be bound. + pub async fn bind(&self) -> Result { + let sock = UdpSocket::bind(self.bind_to.as_ref()) + .await + .map_err(|e| Error::ClientBuildingError { err: e.into() })?; + + Ok(Bound::new(*self, sock)) + } +} + +impl Bound { + fn new(launcher: Launcher, sock: UdpSocket) -> Self { + Self { launcher, sock } + } + + /// Returns the local address of this [`Bound`]. + /// + /// # Errors + /// + /// This function errors if underlying function fails. + pub fn local_addr(&self) -> Result { + self.sock + .local_addr() + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() }) + } + + /// # Errors /// - /// Will panic if the local address can't be bound. - pub async fn bind(local_address: &str) -> Self { - let valid_socket_addr = local_address - .parse::() - .unwrap_or_else(|_| panic!("{local_address} is not a valid socket address")); + /// Will error if can't connect to the socket. + pub async fn connect(self, addr: SocketAddr) -> Result { + let () = self + .sock + .connect(addr) + .await + .map_err(|e| Error::UnableToConnectToRemote { err: e.into() })?; + + Ok(Client::new(self)) + } +} + +impl Client { + /// Creates a new `UdpTrackerClient` connected a remote `addr`. + /// + /// # Errors + /// + /// This function returns and error if the the binding fails. + pub async fn connect(addr: SocketAddr) -> Result { + let launcher = Launcher::default(); + let bound = launcher.bind().await?; - let socket = UdpSocket::bind(valid_socket_addr).await.unwrap(); + bound.connect(addr).await + } + fn new(bound: Bound) -> Self { Self { - socket: Arc::new(socket), - timeout: DEFAULT_TIMEOUT, + sock: bound.sock.into(), + launcher: bound.launcher, } } - /// # Panics + #[must_use] + pub fn timeout(&self) -> Duration { + self.launcher.timeout + } + + /// Returns the local address of this [`Client`]. /// - /// Will panic if can't connect to the socket. - pub async fn connect(&self, remote_address: &str) { - let valid_socket_addr = remote_address - .parse::() - .unwrap_or_else(|_| panic!("{remote_address} is not a valid socket address")); + /// # Errors + /// + /// This function errors if underlying function fails. + pub fn local_addr(&self) -> Result { + self.sock + .local_addr() + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() }) + } - match self.socket.connect(valid_socket_addr).await { - Ok(()) => debug!("Connected successfully"), - Err(e) => panic!("Failed to connect: {e:?}"), - } + /// Returns the peer address of this [`Client`]. + /// + /// # Errors + /// + /// This function errors if underlying function fails. + pub fn peer_addr(&self) -> Result { + self.sock + .peer_addr() + .map_err(|e| Error::UnableToGetRemoteAddress { err: e.into() }) } - /// # Panics + /// # Errors /// - /// Will panic if: + /// Will error if: /// /// - Can't write to the socket. /// - Can't send data. - pub async fn send(&self, bytes: &[u8]) -> usize { + pub async fn send(&self, bytes: &[u8]) -> Result { debug!(target: "UDP client", "sending {bytes:?} ..."); - match time::timeout(self.timeout, self.socket.writable()).await { - Ok(writable_result) => match writable_result { - Ok(()) => (), - Err(e) => panic!("{}", format!("IO error waiting for the socket to become readable: {e:?}")), - }, - Err(e) => panic!("{}", format!("Timeout waiting for the socket to become readable: {e:?}")), - }; - - match time::timeout(self.timeout, self.socket.send(bytes)).await { - Ok(send_result) => match send_result { - Ok(size) => size, - Err(e) => panic!("{}", format!("IO error during send: {e:?}")), - }, - Err(e) => panic!("{}", format!("Send operation timed out: {e:?}")), - } + time::timeout(self.timeout(), self.sock.writable()) + .await + .map_err(|_| Error::TimedOut { + context: "Get Writable Socket".into(), + })? + .map_err(|e| Error::UnableToGetWriteable { err: e.into() })?; + + time::timeout(self.timeout(), self.sock.send(bytes)) + .await + .map_err(|_| Error::TimedOut { + context: "Send To Socket".into(), + })? + .map_err(|e| Error::UnableToSendToSocket { err: e.into() }) } - /// # Panics + /// # Errors /// - /// Will panic if: + /// Will error if: /// /// - Can't read from the socket. /// - Can't receive data. - pub async fn receive(&self, bytes: &mut [u8]) -> usize { + pub async fn receive(&self, bytes: &mut [u8]) -> Result { debug!(target: "UDP client", "receiving ..."); - match time::timeout(self.timeout, self.socket.readable()).await { - Ok(readable_result) => match readable_result { - Ok(()) => (), - Err(e) => panic!("{}", format!("IO error waiting for the socket to become readable: {e:?}")), - }, - Err(e) => panic!("{}", format!("Timeout waiting for the socket to become readable: {e:?}")), - }; + let () = time::timeout(self.timeout(), self.sock.readable()) + .await + .map_err(|_| Error::TimedOut { + context: "Get Readable Socket".into(), + })? + .map_err(|e| Error::UnableToGetReadable { err: e.into() })?; - let size = match time::timeout(self.timeout, self.socket.recv(bytes)).await { - Ok(recv_result) => match recv_result { - Ok(size) => size, - Err(e) => panic!("{}", format!("IO error during send: {e:?}")), - }, - Err(e) => panic!("{}", format!("Receive operation timed out: {e:?}")), - }; + let size = time::timeout(self.timeout(), self.sock.recv(bytes)) + .await + .map_err(|_| Error::TimedOut { + context: "Read From Socket".into(), + })? + .map_err(|e| Error::UnableToReadFromSocket { err: e.into() })?; debug!(target: "UDP client", "{size} bytes received {bytes:?}"); - size + Ok(size) } -} - -/// Creates a new `UdpClient` connected to a Udp server -pub async fn new_udp_client_connected(remote_address: &str) -> UdpClient { - let port = 0; // Let OS choose an unused port. - let client = UdpClient::bind(&source_address(port)).await; - client.connect(remote_address).await; - client -} - -#[allow(clippy::module_name_repetitions)] -#[derive(Debug)] -pub struct UdpTrackerClient { - pub udp_client: UdpClient, -} -impl UdpTrackerClient { - /// # Panics + /// # Errors /// - /// Will panic if can't write request to bytes. - pub async fn send(&self, request: Request) -> usize { + /// Will errors if can't write request to bytes. + pub async fn send_request(&self, request: Request) -> Result { debug!(target: "UDP tracker client", "send request {request:?}"); // Write request into a buffer let request_buffer = vec![0u8; MAX_PACKET_SIZE]; let mut cursor = Cursor::new(request_buffer); - let request_data = match request.write(&mut cursor) { - Ok(()) => { - #[allow(clippy::cast_possible_truncation)] - let position = cursor.position() as usize; - let inner_request_buffer = cursor.get_ref(); - // Return slice which contains written request data - &inner_request_buffer[..position] - } - Err(e) => panic!("could not write request to bytes: {e}."), + request + .write(&mut cursor) + .map_err(|e| Error::UnableToWriteToRequestBuffer { err: e.into() })?; + + let request_data = { + #[allow(clippy::cast_possible_truncation)] + let position = cursor.position() as usize; + let inner_request_buffer = cursor.get_ref(); + // Return slice which contains written request data + &inner_request_buffer[..position] }; - self.udp_client.send(request_data).await + self.send(request_data).await } - /// # Panics + /// # Errors /// - /// Will panic if can't create response from the received payload (bytes buffer). - pub async fn receive(&self) -> Response { + /// Will error if can't create response from the received payload (bytes buffer). + pub async fn receive_response(&self) -> Result { let mut response_buffer = [0u8; MAX_PACKET_SIZE]; - let payload_size = self.udp_client.receive(&mut response_buffer).await; + let payload_size = self.receive(&mut response_buffer).await?; debug!(target: "UDP tracker client", "received {payload_size} bytes. Response {response_buffer:?}"); - Response::from_bytes(&response_buffer[..payload_size], true).unwrap() + Response::from_bytes(&response_buffer[..payload_size], true) + .map_err(|e| Error::UnableToGetResponseFromBuffer { err: e.into() }) } -} -/// Creates a new `UdpTrackerClient` connected to a Udp Tracker server -pub async fn new_udp_tracker_client_connected(remote_address: &str) -> UdpTrackerClient { - let udp_client = new_udp_client_connected(remote_address).await; - UdpTrackerClient { udp_client } -} + /// Completes a connection request to the UDP Tracker server. + /// + /// # Errors + /// + /// Will return and error if + /// + /// - It can't connect to the remote UDP socket. + /// - It can't make a connection request successfully to the remote UDP + /// server (after successfully connecting to the remote UDP socket). + /// + pub async fn do_connection_request(&self, transaction_id: TransactionId) -> Result { + debug!("Sending connection request with transaction id: {transaction_id:#?}"); -/// Helper Function to Check if a UDP Service is Connectable -/// -/// # Errors -/// -/// It will return an error if unable to connect to the UDP service. -/// -/// # Panics -pub async fn check(binding: &SocketAddr) -> Result { - debug!("Checking Service (detail): {binding:?}."); + let connect_request = ConnectRequest { transaction_id }; - let client = new_udp_tracker_client_connected(binding.to_string().as_str()).await; + let _ = self.send_request(connect_request.clone().into()).await?; - let connect_request = ConnectRequest { - transaction_id: TransactionId(123), - }; + let response = self.receive_response().await?; - client.send(connect_request.into()).await; + debug!("connection request response:\n{response:#?}"); - let process = move |response| { - if matches!(response, Response::Connect(_connect_response)) { - Ok("Connected".to_string()) - } else { - Err("Did not Connect".to_string()) - } - }; + check_connect_response(&response, &connect_request) + } + + /// Helper Function to Check if a UDP Service is Connectable + /// + /// # Errors + /// + /// It will return an error if unable to connect to the UDP service. + /// + /// # Panics + pub async fn check(self) -> Result { + let connect_request = ConnectRequest { + transaction_id: TransactionId(123), + }; + + let _ = self.send_request(connect_request.clone().into()).await?; + + let process = move |response: Result, connect_request: ConnectRequest| -> Result { + check_connect_response(&response?, &connect_request).map(|id| { + format!( + "Connected with, transaction_id: {} and connection_id: {}", + id.transaction_id.0, id.connection_id.0 + ) + }) + }; - let sleep = time::sleep(Duration::from_millis(2000)); - tokio::pin!(sleep); + let sleep = time::sleep(Duration::from_millis(2000)); + tokio::pin!(sleep); - tokio::select! { - () = &mut sleep => { - Err("Timed Out".to_string()) + tokio::select! { + () = &mut sleep => { + Err(Error::TimedOut { context: "Receive Connect Response".into() }) + } + response = self.receive_response() => { + process(response, connect_request) + } } - response = client.receive() => { - process(response) + } +} + +/// Checks the Connect Response Against the Request +/// +/// # Errors +/// +/// If the [`Response`] is not a [`ConnectResponse`] +/// or if the [`TransactionId`] dose not match. +/// +pub fn check_connect_response(response: &Response, connect_request: &ConnectRequest) -> Result { + match response { + Response::Connect(connect) => { + if connect.transaction_id == connect_request.transaction_id { + Ok(connect.clone()) + } else { + Err(Error::UnexpectedTransactionId { + expected: connect.transaction_id, + received: connect_request.transaction_id, + }) + } } + response => Err(Error::UnexpectedResponse { + response: response.clone(), + }), } } diff --git a/src/shared/bit_torrent/tracker/udp/mod.rs b/src/shared/bit_torrent/tracker/udp/mod.rs index 9322ef045..41ebeafc4 100644 --- a/src/shared/bit_torrent/tracker/udp/mod.rs +++ b/src/shared/bit_torrent/tracker/udp/mod.rs @@ -1,12 +1,53 @@ +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; + +use thiserror::Error; + pub mod client; -/// The maximum number of bytes in a UDP packet. -pub const MAX_PACKET_SIZE: usize = 1496; -/// A magic 64-bit integer constant defined in the protocol that is used to -/// identify the protocol. -pub const PROTOCOL_ID: i64 = 0x0417_2710_1980; +#[derive(Debug, Clone, Error)] +pub enum Error { + #[error("Timed Out: \"{context}\".")] + TimedOut { context: String }, + + #[error("Failed to Get Writeable Socket: {err:?}")] + UnableToGetWriteable { err: Arc }, + + #[error("Error when sending to socket: {err:?}")] + UnableToSendToSocket { err: Arc }, + + #[error("Failed to Get Readable Socket: {err:?}")] + UnableToGetReadable { err: Arc }, + + #[error("Error when reading from socket: {err:?}")] + UnableToReadFromSocket { err: Arc }, + + #[error("Error when writing to buffer: {err:?}")] + UnableToWriteToRequestBuffer { err: Arc }, + + #[error("Error when writing to buffer: {err:?}")] + UnableToGetResponseFromBuffer { err: Arc }, + + #[error("Received an unexpected response: {response:?}")] + UnexpectedResponse { response: aquatic_udp_protocol::Response }, + + #[error("Received an unexpected TransactionId: Expected: {expected:?}, Received: {received:?}")] + UnexpectedTransactionId { + expected: aquatic_udp_protocol::TransactionId, + received: aquatic_udp_protocol::TransactionId, + }, + + #[error("Failed to bind the Client: {err:?}")] + ClientBuildingError { err: Arc }, + #[error("Failed to get the bound local socket: {err:?}")] + UnableToGetLocalAddress { err: Arc }, + #[error("Failed to connect to remote: {err:?}")] + UnableToConnectToRemote { err: Arc }, + #[error("Failed to get the connected socket: {err:?}")] + UnableToGetRemoteAddress { err: Arc }, +} /// Generates the source address for the UDP client -fn source_address(port: u16) -> String { - format!("127.0.0.1:{port}") +fn source_address(port: u16) -> SocketAddr { + SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port) } diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs index 3c3c13151..a9ed7f31b 100644 --- a/tests/servers/health_check_api/contract.rs +++ b/tests/servers/health_check_api/contract.rs @@ -273,7 +273,12 @@ mod udp { let details = report.details.first().expect("it should have some details"); assert_eq!(details.binding, service.bind_address()); - assert_eq!(details.result, Ok("Connected".to_string())); + + assert!( + details.result.clone().is_ok_and(|e| e.contains("Connected")), + "Expected Okay, but Got: {:?}", + details.result + ); assert_eq!( details.info, @@ -318,7 +323,11 @@ mod udp { let details = report.details.first().expect("it should have some details"); assert_eq!(details.binding, binding); - assert_eq!(details.result, Err("Timed Out".to_string())); + assert!( + details.result.clone().is_err_and(|e| e.contains("Timed Out")), + "Expected Error, Got: {:?}", + details.result + ); assert_eq!(details.info, format!("checking the udp tracker health check at: {binding}")); env.stop().await.expect("it should stop the service"); diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index 91dca4d42..c706cf00f 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -3,11 +3,9 @@ // BEP 15. UDP Tracker Protocol for BitTorrent // https://www.bittorrent.org/beps/bep_0015.html -use core::panic; - -use aquatic_udp_protocol::{ConnectRequest, ConnectionId, Response, TransactionId}; -use torrust_tracker::shared::bit_torrent::tracker::udp::client::{new_udp_client_connected, UdpTrackerClient}; -use torrust_tracker::shared::bit_torrent::tracker::udp::MAX_PACKET_SIZE; +use aquatic_udp_protocol::Response; +use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; +use torrust_tracker_configuration::MAX_PACKET_SIZE; use torrust_tracker_test_helpers::configuration; use crate::servers::udp::asserts::is_error_response; @@ -21,30 +19,18 @@ fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { [0; MAX_PACKET_SIZE] } -async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { - let connect_request = ConnectRequest { transaction_id }; - - client.send(connect_request.into()).await; - - let response = client.receive().await; - - match response { - Response::Connect(connect_response) => connect_response.connection_id, - _ => panic!("error connecting to udp server {:?}", response), - } -} - #[tokio::test] async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_client_connected(&env.bind_address().to_string()).await; + let client = Client::connect(env.bind_address()).await.expect("it should connect"); - client.send(&empty_udp_request()).await; + client.send(&empty_udp_request()).await.expect("it should send request"); let mut buffer = empty_buffer(); - client.receive(&mut buffer).await; - let response = Response::from_bytes(&buffer, true).unwrap(); + client.receive(&mut buffer).await.expect("it should receive a response"); + + let response = Response::from_bytes(&buffer, true).expect("it should parse a response"); assert!(is_error_response(&response, "bad request")); @@ -53,7 +39,7 @@ async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_req mod receiving_a_connection_request { use aquatic_udp_protocol::{ConnectRequest, TransactionId}; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::new_udp_tracker_client_connected; + use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; use torrust_tracker_test_helpers::configuration; use crate::servers::udp::asserts::is_connect_response; @@ -63,15 +49,18 @@ mod receiving_a_connection_request { async fn should_return_a_connect_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = Client::connect(env.bind_address()).await.expect("it should connect"); let connect_request = ConnectRequest { transaction_id: TransactionId(123), }; - client.send(connect_request.into()).await; + client + .send_request(connect_request.into()) + .await + .expect("it should send request"); - let response = client.receive().await; + let response = client.receive_response().await.expect("it should get response"); assert!(is_connect_response(&response, TransactionId(123))); @@ -86,25 +75,27 @@ mod receiving_an_announce_request { AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, TransactionId, }; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::new_udp_tracker_client_connected; + use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; use torrust_tracker_test_helpers::configuration; use crate::servers::udp::asserts::is_ipv4_announce_response; - use crate::servers::udp::contract::send_connection_request; use crate::servers::udp::Started; #[tokio::test] async fn should_return_an_announce_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = Client::connect(env.bind_address()).await.expect("it should connect"); - let connection_id = send_connection_request(TransactionId(123), &client).await; + let ctx = client + .do_connection_request(TransactionId(123)) + .await + .expect("it should do connection"); // Send announce request let announce_request = AnnounceRequest { - connection_id: ConnectionId(connection_id.0), + connection_id: ConnectionId(ctx.connection_id.0), transaction_id: TransactionId(123i32), info_hash: InfoHash([0u8; 20]), peer_id: PeerId([255u8; 20]), @@ -115,12 +106,15 @@ mod receiving_an_announce_request { ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), key: PeerKey(0u32), peers_wanted: NumberOfPeers(1i32), - port: Port(client.udp_client.socket.local_addr().unwrap().port()), + port: Port(client.local_addr().expect("it should get the local address").port()), }; - client.send(announce_request.into()).await; + client + .send_request(announce_request.into()) + .await + .expect("it should send a request"); - let response = client.receive().await; + let response = client.receive_response().await.expect("it should receive a response"); println!("test response {response:?}"); @@ -132,20 +126,22 @@ mod receiving_an_announce_request { mod receiving_an_scrape_request { use aquatic_udp_protocol::{ConnectionId, InfoHash, ScrapeRequest, TransactionId}; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::new_udp_tracker_client_connected; + use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; use torrust_tracker_test_helpers::configuration; use crate::servers::udp::asserts::is_scrape_response; - use crate::servers::udp::contract::send_connection_request; use crate::servers::udp::Started; #[tokio::test] async fn should_return_a_scrape_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = Client::connect(env.bind_address()).await.expect("it should connect"); - let connection_id = send_connection_request(TransactionId(123), &client).await; + let ctx = client + .do_connection_request(TransactionId(123)) + .await + .expect("it should connect"); // Send scrape request @@ -154,14 +150,17 @@ mod receiving_an_scrape_request { let info_hashes = vec![InfoHash([0u8; 20])]; let scrape_request = ScrapeRequest { - connection_id: ConnectionId(connection_id.0), + connection_id: ConnectionId(ctx.connection_id.0), transaction_id: TransactionId(123i32), info_hashes, }; - client.send(scrape_request.into()).await; + client + .send_request(scrape_request.into()) + .await + .expect("it should send a request"); - let response = client.receive().await; + let response = client.receive_response().await.expect("it should receive a response"); assert!(is_scrape_response(&response)); From 3ac4aa5516269fdf7b2258efdfd56a99d120d508 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 1 Apr 2024 13:35:12 +0100 Subject: [PATCH 006/217] docs: [#768] udpate profiling docs --- .cargo/config.toml | 1 + Cargo.toml | 6 +- cSpell.json | 8 +- docs/media/flamegraph.svg | 4 +- .../flamegraph_generated_withput_sudo.svg | 491 ++++++++++++++++++ docs/profiling.md | 75 ++- flamegraph_generated_withput_sudo.svg | 491 ++++++++++++++++++ 7 files changed, 1069 insertions(+), 7 deletions(-) create mode 100644 docs/media/flamegraph_generated_withput_sudo.svg create mode 100644 flamegraph_generated_withput_sudo.svg diff --git a/.cargo/config.toml b/.cargo/config.toml index a88db5f38..34d6230b9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -23,3 +23,4 @@ rustflags = [ "-D", "unused", ] + diff --git a/Cargo.toml b/Cargo.toml index 99b7a334a..d045b945a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,6 @@ debug = 1 lto = "fat" opt-level = 3 -[target.x86_64-unknown-linux-gnu] -linker = "/usr/bin/clang" -rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] \ No newline at end of file +[profile.release-debug] +inherits = "release" +debug = true \ No newline at end of file diff --git a/cSpell.json b/cSpell.json index bbcba98a7..0ee2f8306 100644 --- a/cSpell.json +++ b/cSpell.json @@ -21,6 +21,7 @@ "bools", "Bragilevsky", "bufs", + "buildid", "Buildx", "byteorder", "callgrind", @@ -69,9 +70,12 @@ "infoschema", "Intermodal", "intervali", - "kcachegrind", "Joakim", + "kallsyms", + "kcachegrind", + "kexec", "keyout", + "kptr", "lcov", "leecher", "leechers", @@ -83,6 +87,7 @@ "matchmakes", "metainfo", "middlewares", + "misresolved", "mockall", "multimap", "myacicontext", @@ -152,6 +157,7 @@ "Vagaa", "valgrind", "Vitaly", + "vmlinux", "Vuze", "Weidendorfer", "Werror", diff --git a/docs/media/flamegraph.svg b/docs/media/flamegraph.svg index 34e7146f9..58387ee06 100644 --- a/docs/media/flamegraph.svg +++ b/docs/media/flamegraph.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/docs/media/flamegraph_generated_withput_sudo.svg b/docs/media/flamegraph_generated_withput_sudo.svg new file mode 100644 index 000000000..84c00ffe3 --- /dev/null +++ b/docs/media/flamegraph_generated_withput_sudo.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file diff --git a/docs/profiling.md b/docs/profiling.md index 7c28367ce..26e5b786e 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -2,12 +2,85 @@ ## Using flamegraph +### Requirements + +You need to install some dependencies. For Ubuntu you can run: + ```console -TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" cargo flamegraph --bin=profiling -- 60 +sudo apt-get install clang lld +``` + +You also need to uncomment these lines in the cargo [config.toml](./../.cargo/config.toml) file. + +```toml +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] +``` + +Follow the [flamegraph](https://github.com/flamegraph-rs/flamegraph) instructions for installation. + +Apart from running the tracker you will need to run some request if you want to profile services while they are processing requests. + +You can use the aquatic [UDP load test](https://github.com/greatest-ape/aquatic/tree/master/crates/udp_load_test) script. + +### Generate flamegraph + +To generate the graph you will need to: + +1. Build the tracker for profiling. +2. Run the aquatic UDP load test. +3. Run the tracker with flamegraph and profiling configuration. + +```console +cargo build --profile=release-debug --bin=profiling +./target/release/aquatic_udp_load_test -c "load-test-config.toml" +sudo TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 +``` + +__NOTICE__: You need to install the `aquatic_udp_load_test` program. + +The output should be like the following: + +```output +Loading configuration file: `./share/default/config/tracker.udp.benchmarking.toml` ... +Torrust successfully shutdown. +[ perf record: Woken up 23377 times to write data ] +Warning: +Processed 533730 events and lost 3 chunks! + +Check IO/CPU overload! + +[ perf record: Captured and wrote 5899.806 MB perf.data (373239 samples) ] +writing flamegraph to "flamegraph.svg" ``` ![flamegraph](./media/flamegraph.svg) +__NOTICE__: You need to provide the absolute path for the installed `flamegraph` app if you use sudo. Replace `/home/USER/.cargo/bin/flamegraph` with the location of your installed `flamegraph` app. You can run it without sudo but you can get a warning message like the following: + +```output +WARNING: Kernel address maps (/proc/{kallsyms,modules}) are restricted, +check /proc/sys/kernel/kptr_restrict and /proc/sys/kernel/perf_event_paranoid. + +Samples in kernel functions may not be resolved if a suitable vmlinux +file is not found in the buildid cache or in the vmlinux path. + +Samples in kernel modules won't be resolved at all. + +If some relocation was applied (e.g. kexec) symbols may be misresolved +even with a suitable vmlinux or kallsyms file. + +Couldn't record kernel reference relocation symbol +Symbol resolution may be skewed if relocation was used (e.g. kexec). +Check /proc/kallsyms permission or run as root. +Loading configuration file: `./share/default/config/tracker.udp.benchmarking.toml` ... +``` + +And some bars in the graph will have the `unknown` label. + +![flamegraph generated without sudo](./media/flamegraph_generated_withput_sudo.svg) + ## Using valgrind and kcachegrind You need to: diff --git a/flamegraph_generated_withput_sudo.svg b/flamegraph_generated_withput_sudo.svg new file mode 100644 index 000000000..84c00ffe3 --- /dev/null +++ b/flamegraph_generated_withput_sudo.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file From a4b6bed2e8118da70366aa3ac98d61aa93ee2b2e Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Thu, 4 Apr 2024 13:33:06 +0800 Subject: [PATCH 007/217] dev: launching services work --- Cargo.lock | 548 ++++-------------- Cargo.toml | 7 +- packages/configuration/src/lib.rs | 45 +- packages/located-error/Cargo.toml | 2 +- packages/located-error/src/lib.rs | 2 +- packages/torrent-repository/Cargo.toml | 1 - .../src/repository/rw_lock_std_mutex_tokio.rs | 9 +- src/app.rs | 54 +- src/bin/http_health_check.rs | 3 + src/bootstrap/app.rs | 41 +- src/bootstrap/jobs/health_check_api.rs | 118 +++- src/bootstrap/jobs/http_tracker.rs | 41 +- src/bootstrap/jobs/mod.rs | 63 +- src/bootstrap/jobs/torrent_cleanup.rs | 3 +- src/bootstrap/jobs/tracker_apis.rs | 29 +- src/bootstrap/jobs/udp_tracker.rs | 62 +- src/bootstrap/logging.rs | 60 -- src/bootstrap/mod.rs | 1 - src/console/ci/e2e/docker.rs | 2 +- src/console/ci/e2e/runner.rs | 25 +- src/console/ci/e2e/tracker_checker.rs | 2 +- src/console/ci/e2e/tracker_container.rs | 2 +- src/console/clients/checker/app.rs | 25 +- src/console/clients/checker/checks/health.rs | 4 +- src/console/clients/checker/checks/http.rs | 10 +- src/console/clients/checker/checks/udp.rs | 19 +- src/console/clients/checker/config.rs | 6 +- src/console/clients/http/app.rs | 57 +- src/console/clients/http/mod.rs | 4 +- src/console/clients/mod.rs | 83 +++ src/console/clients/udp/app.rs | 126 +--- src/console/clients/udp/checker.rs | 22 +- src/console/profiling.rs | 7 +- src/core/auth.rs | 2 +- src/core/databases/mod.rs | 3 +- src/core/databases/mysql.rs | 3 +- src/core/databases/sqlite.rs | 1 + src/core/mod.rs | 14 +- src/core/statistics.rs | 9 +- src/main.rs | 8 +- src/servers/apis/routes.rs | 3 +- src/servers/apis/server.rs | 319 ++++------ .../apis/v1/context/torrent/handlers.rs | 2 +- src/servers/apis/v1/middlewares/auth.rs | 2 +- src/servers/health_check_api/handlers.rs | 6 +- src/servers/health_check_api/mod.rs | 6 + src/servers/health_check_api/server.rs | 196 +++++-- src/servers/http/mod.rs | 2 +- src/servers/http/server.rs | 257 -------- src/servers/http/service.rs | 178 ++++++ src/servers/http/v1/handlers/announce.rs | 2 +- src/servers/http/v1/handlers/scrape.rs | 2 +- src/servers/http/v1/routes.rs | 6 +- src/servers/http/v1/services/scrape.rs | 4 +- src/servers/mod.rs | 36 ++ src/servers/registar.rs | 38 +- src/servers/service.rs | 256 ++++++++ src/servers/signals.rs | 22 +- src/servers/udp/handle.rs | 167 ++++++ src/servers/udp/handlers.rs | 6 +- src/servers/udp/mod.rs | 22 +- src/servers/udp/server.rs | 532 ++++++++--------- src/shared/bit_torrent/tracker/udp/client.rs | 12 +- src/shared/bit_torrent/tracker/udp/mod.rs | 4 +- tests/clients/mod.rs | 1 + tests/clients/udp/mod.rs | 40 ++ tests/common/udp.rs | 41 -- tests/{servers => common}/udp/environment.rs | 67 +-- tests/common/udp/mod.rs | 25 + tests/integration.rs | 1 + tests/servers/api/environment.rs | 57 +- tests/servers/api/mod.rs | 6 +- tests/servers/health_check_api/contract.rs | 40 +- tests/servers/health_check_api/environment.rs | 119 ++-- tests/servers/health_check_api/mod.rs | 2 +- tests/servers/http/environment.rs | 44 +- tests/servers/http/mod.rs | 11 +- tests/servers/http/v1/mod.rs | 33 +- tests/servers/udp/contract.rs | 37 +- tests/servers/udp/mod.rs | 5 - 80 files changed, 2120 insertions(+), 2012 deletions(-) delete mode 100644 src/bootstrap/logging.rs delete mode 100644 src/servers/http/server.rs create mode 100644 src/servers/http/service.rs create mode 100644 src/servers/service.rs create mode 100644 src/servers/udp/handle.rs create mode 100644 tests/clients/mod.rs create mode 100644 tests/clients/udp/mod.rs delete mode 100644 tests/common/udp.rs rename tests/{servers => common}/udp/environment.rs (53%) create mode 100644 tests/common/udp/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d61ac26d3..f4cedd763 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,40 +167,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" -dependencies = [ - "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compression" version = "0.4.6" @@ -217,128 +183,6 @@ dependencies = [ "zstd-safe", ] -[[package]] -name = "async-executor" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" -dependencies = [ - "async-lock 3.3.0", - "async-task", - "concurrent-queue", - "fastrand 2.0.2", - "futures-lite 2.3.0", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.2.0", - "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", - "blocking", - "futures-lite 2.3.0", - "once_cell", - "tokio", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" -dependencies = [ - "async-lock 3.3.0", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.3.0", - "parking", - "polling 3.6.0", - "rustix 0.38.32", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" -dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" - [[package]] name = "async-trait" version = "0.1.79" @@ -350,12 +194,6 @@ dependencies = [ "syn 2.0.55", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.1.0" @@ -581,22 +419,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" -dependencies = [ - "async-channel 2.2.0", - "async-lock 3.3.0", - "async-task", - "fastrand 2.0.2", - "futures-io", - "futures-lite 2.3.0", - "piper", - "tracing", -] - [[package]] name = "borsh" version = "1.3.1" @@ -841,15 +663,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "concurrent-queue" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" version = "0.14.0" @@ -1184,54 +997,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" -dependencies = [ - "event-listener 5.2.0", - "pin-project-lite", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1244,30 +1009,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - [[package]] name = "flate2" version = "1.0.28" @@ -1431,34 +1178,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand 2.0.2", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.30" @@ -1539,18 +1258,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "h2" version = "0.4.3" @@ -1743,7 +1450,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", "tower", "tower-service", @@ -1811,15 +1518,6 @@ dependencies = [ "serde", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-enum" version = "1.1.3" @@ -1829,17 +1527,6 @@ dependencies = [ "derive_utils", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -1910,15 +1597,6 @@ dependencies = [ "serde", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -2048,12 +1726,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2087,9 +1759,6 @@ name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -dependencies = [ - "value-bag", -] [[package]] name = "lru" @@ -2100,6 +1769,15 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -2201,7 +1879,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "socket2 0.5.6", + "socket2", "twox-hash", "url", ] @@ -2330,6 +2008,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -2451,10 +2139,10 @@ dependencies = [ ] [[package]] -name = "parking" -version = "2.2.0" +name = "overload" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" @@ -2616,17 +2304,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand 2.0.2", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -2661,37 +2338,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 0.38.32", - "tracing", - "windows-sys 0.52.0", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2916,8 +2562,17 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -2928,9 +2583,15 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" @@ -3149,20 +2810,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.32" @@ -3172,7 +2819,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -3476,6 +3123,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3518,16 +3174,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.6" @@ -3669,8 +3315,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", - "rustix 0.38.32", + "fastrand", + "rustix", "windows-sys 0.52.0", ] @@ -3709,6 +3355,16 @@ dependencies = [ "syn 2.0.55", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.34" @@ -3787,8 +3443,9 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -3909,13 +3566,11 @@ dependencies = [ "colored", "config", "derive_more", - "fern", "futures", "hex-literal", "hyper", "lazy_static", "local-ip-address", - "log", "mockall", "multimap", "percent-encoding", @@ -3925,6 +3580,7 @@ dependencies = [ "rand", "reqwest", "ringbuf", + "rstest", "serde", "serde_bencode", "serde_bytes", @@ -3941,6 +3597,8 @@ dependencies = [ "torrust-tracker-torrent-repository", "tower-http", "tracing", + "tracing-subscriber", + "tracing-test", "url", "uuid", ] @@ -3981,8 +3639,8 @@ dependencies = [ name = "torrust-tracker-located-error" version = "3.0.0-alpha.12-develop" dependencies = [ - "log", "thiserror", + "tracing", ] [[package]] @@ -4010,7 +3668,6 @@ dependencies = [ name = "torrust-tracker-torrent-repository" version = "3.0.0-alpha.12-develop" dependencies = [ - "async-std", "criterion", "futures", "rstest", @@ -4100,6 +3757,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +dependencies = [ + "lazy_static", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +dependencies = [ + "lazy_static", + "quote", + "syn 1.0.109", ] [[package]] @@ -4192,10 +3902,10 @@ dependencies = [ ] [[package]] -name = "value-bag" -version = "1.8.1" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "vcpkg" @@ -4209,12 +3919,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 512fd8c22..a676e223d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,12 +42,10 @@ clap = { version = "4", features = ["derive", "env"] } colored = "2" config = "0" derive_more = "0" -fern = "0" futures = "0" hex-literal = "0" hyper = "1" lazy_static = "1" -log = { version = "0", features = ["release_max_level_info"] } multimap = "0" percent-encoding = "2" r2d2 = "0" @@ -62,7 +60,7 @@ serde_bytes = "0" serde_json = "1" serde_repr = "0" thiserror = "1" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "tracing"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "packages/clock" } torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = "contrib/bencode" } @@ -71,6 +69,8 @@ torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packa torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path = "packages/torrent-repository" } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } tracing = "0" +tracing-subscriber = "0" +tracing-test = "0" url = "2" uuid = { version = "1", features = ["v4"] } @@ -80,6 +80,7 @@ ignored = ["serde_bytes"] [dev-dependencies] local-ip-address = "0" mockall = "0" +rstest = "0" torrust-tracker-test-helpers = { version = "3.0.0-alpha.12-develop", path = "packages/test-helpers" } [workspace] diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 4d05a6c23..355d9b1d1 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -237,7 +237,7 @@ use std::time::Duration; use std::{env, fs}; use config::{Config, ConfigError, File, FileFormat}; -use derive_more::Constructor; +use derive_more::{AsMut, AsRef, Constructor, Display}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; @@ -361,7 +361,20 @@ pub struct HttpTracker { pub ssl_key_path: Option, } -pub type AccessTokens = HashMap; +#[derive(Constructor, Serialize, Deserialize, AsRef, AsMut, PartialEq, Eq, Clone, Display)] +#[display(fmt = "AccessTokens (values hidden): {tokens:?}")] +pub struct AccessTokens { + tokens: HashMap, +} + +impl std::fmt::Debug for AccessTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let keys: Vec = self.tokens.keys().cloned().collect(); + f.debug_struct("AccessTokens") + .field("tokens keys", &keys) + .finish_non_exhaustive() + } +} /// Configuration for the HTTP API. #[serde_as] @@ -391,7 +404,9 @@ pub struct HttpApi { impl HttpApi { fn override_admin_token(&mut self, api_admin_token: &str) { - self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); + self.access_tokens + .as_mut() + .insert("admin".to_string(), api_admin_token.to_string()); } } @@ -579,10 +594,12 @@ impl Default for Configuration { ssl_enabled: false, ssl_cert_path: None, ssl_key_path: None, - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), + access_tokens: AccessTokens::new( + [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + ), }, health_check_api: HealthCheckApi { bind_address: String::from("127.0.0.1:1313"), @@ -822,7 +839,17 @@ mod tests { fn http_api_configuration_should_check_if_it_contains_a_token() { let configuration = Configuration::default(); - assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken")); - assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken")); + assert!(configuration + .http_api + .access_tokens + .as_ref() + .values() + .any(|t| t == "MyAccessToken")); + assert!(!configuration + .http_api + .access_tokens + .as_ref() + .values() + .any(|t| t == "NonExistingToken")); } } diff --git a/packages/located-error/Cargo.toml b/packages/located-error/Cargo.toml index fa3d1d76d..637ea3055 100644 --- a/packages/located-error/Cargo.toml +++ b/packages/located-error/Cargo.toml @@ -15,7 +15,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -log = { version = "0", features = ["release_max_level_info"] } +tracing = "0" [dev-dependencies] thiserror = "1" diff --git a/packages/located-error/src/lib.rs b/packages/located-error/src/lib.rs index 49e135600..bfd4d4a86 100644 --- a/packages/located-error/src/lib.rs +++ b/packages/located-error/src/lib.rs @@ -33,7 +33,7 @@ use std::error::Error; use std::panic::Location; use std::sync::Arc; -use log::debug; +use tracing::debug; pub type DynError = Arc; diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 8614be496..70f385b02 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -23,7 +23,6 @@ torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = ".. torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } [dev-dependencies] -async-std = { version = "1", features = ["attributes", "tokio1"] } criterion = { version = "0", features = ["async_tokio"] } rstest = "0" diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 5394abb6a..7b038ad56 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -1,10 +1,9 @@ use std::collections::BTreeMap; use std::iter::zip; -use std::pin::Pin; use std::sync::Arc; -use futures::future::join_all; -use futures::{Future, FutureExt}; +use futures::future::{join_all, BoxFuture}; +use futures::FutureExt as _; use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -112,7 +111,7 @@ where } async fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { - let handles: Vec + Send>>>; + let handles: Vec>; { let db = self.get_torrents(); handles = db @@ -125,7 +124,7 @@ where } async fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { - let handles: Vec> + Send>>>; + let handles: Vec>>; { let db = self.get_torrents(); diff --git a/src/app.rs b/src/app.rs index 8bdc281a6..0d0ddd8a7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,11 +23,12 @@ //! - Tracker REST API: the tracker API can be enabled/disabled. use std::sync::Arc; -use log::warn; use tokio::task::JoinHandle; use torrust_tracker_configuration::Configuration; +use tracing::warn; use crate::bootstrap::jobs::{health_check_api, http_tracker, torrent_cleanup, tracker_apis, udp_tracker}; +use crate::servers::health_check_api::Version; use crate::servers::registar::Registar; use crate::{core, servers}; @@ -37,7 +38,7 @@ use crate::{core, servers}; /// /// - Can't retrieve tracker keys from database. /// - Can't load whitelist from database. -pub async fn start(config: &Configuration, tracker: Arc) -> Vec> { +pub async fn start(tracker_config: &Configuration, tracker: Arc) -> Vec> { let mut jobs: Vec> = Vec::new(); let registar = Registar::default(); @@ -59,43 +60,32 @@ pub async fn start(config: &Configuration, tracker: Arc) -> Vec) -> Vec 0 { - jobs.push(torrent_cleanup::start_job(config, &tracker)); + if tracker_config.inactive_peer_cleanup_interval > 0 { + jobs.push(torrent_cleanup::start_job(tracker_config, &tracker)); } // Start Health Check API - jobs.push(health_check_api::start_job(&config.health_check_api, registar.entries()).await); + jobs.push( + health_check_api::start_job( + &tracker_config.health_check_api, + registar.entries(), + registar.give_form(), + Version::V0, + ) + .await, + ); jobs } diff --git a/src/bin/http_health_check.rs b/src/bin/http_health_check.rs index b7c6dfa41..a9bdb0a58 100644 --- a/src/bin/http_health_check.rs +++ b/src/bin/http_health_check.rs @@ -8,9 +8,12 @@ use std::time::Duration; use std::{env, process}; use reqwest::Client; +use tracing::Level; #[tokio::main] async fn main() { + let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); + let args: Vec = env::args().collect(); if args.len() != 2 { eprintln!("Usage: cargo run --bin http_health_check "); diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index 396e63682..203c9b7fb 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -15,30 +15,35 @@ use std::sync::Arc; use torrust_tracker_clock::static_time; use torrust_tracker_configuration::Configuration; +use tracing::metadata::ParseLevelError; +use tracing::Level; use super::config::initialize_configuration; -use crate::bootstrap; use crate::core::services::tracker_factory; use crate::core::Tracker; use crate::shared::crypto::ephemeral_instance_keys; -/// It loads the configuration from the environment and builds the main domain [`Tracker`] struct. +/// It loads the configuration from the environment and setups up tracing (logging). +/// +/// # Panics +/// +/// It will panic if the tracing level is malformed in the configuration. #[must_use] -pub fn setup() -> (Configuration, Arc) { - let configuration = initialize_configuration(); - let tracker = initialize_with_configuration(&configuration); +pub fn config() -> (Configuration, Level) { + let config = initialize_configuration(); + let level = parse_level_or_default(&config.log_level).expect("its should provide a valid value for the log level"); - (configuration, tracker) + (config, level) } /// It initializes the application with the given configuration. /// /// The configuration may be obtained from the environment (via config file or env vars). #[must_use] -pub fn initialize_with_configuration(configuration: &Configuration) -> Arc { +pub fn tracker(configuration: &Configuration) -> Arc { initialize_static(); - initialize_logging(configuration); - Arc::new(initialize_tracker(configuration)) + + Arc::new(tracker_factory(configuration)) } /// It initializes the application static values. @@ -47,7 +52,7 @@ pub fn initialize_with_configuration(configuration: &Configuration) -> Arc Tracker { - tracker_factory(config) -} - -/// It initializes the log level, format and channel. -/// -/// See [the logging setup](crate::bootstrap::logging::setup) for more info about logging. -pub fn initialize_logging(config: &Configuration) { - bootstrap::logging::setup(config); +fn parse_level_or_default(level: &Option) -> Result { + level.as_deref().unwrap_or("info").parse() } diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index eec4d81a8..5f87b02df 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -14,15 +14,18 @@ //! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) //! for the API configuration options. -use log::info; -use tokio::sync::oneshot; +use std::net::SocketAddr; +use std::time::Duration; + use tokio::task::JoinHandle; use torrust_tracker_configuration::HealthCheckApi; +use tracing::instrument; -use super::Started; -use crate::servers::health_check_api::server; -use crate::servers::registar::ServiceRegistry; -use crate::servers::signals::Halted; +use super::Error; +use crate::servers::health_check_api::server::HealthCheckLauncher; +use crate::servers::health_check_api::Version; +use crate::servers::registar::{ServiceRegistrationForm, ServiceRegistry}; +use crate::servers::service::Service; /// This function starts a new Health Check API server with the provided /// configuration. @@ -34,40 +37,91 @@ use crate::servers::signals::Halted; /// # Panics /// /// It would panic if unable to send the `ApiServerJobStarted` notice. -pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> JoinHandle<()> { - let bind_addr = config - .bind_address - .parse::() - .expect("it should have a valid health check bind address"); +/// +#[allow(clippy::async_yields_async)] +#[instrument(ret, skip(form), fields(registry = %registry))] +pub async fn start_job( + config: &HealthCheckApi, + registry: ServiceRegistry, + form: ServiceRegistrationForm, + version: Version, +) -> JoinHandle<()> { + let addr = config.bind_address.parse().expect("it should parse the binding address"); + + match version { + Version::V0 => start_v0(addr, registry, form).await.expect("it should start the service"), + } +} + +/// Starts the first (un-versioned) tracker service health check. +/// # Panics +/// +/// Panics if something goes wrong... +/// +#[allow(clippy::async_yields_async)] +#[instrument(err, ret, skip(form), fields(registry = %registry))] +async fn start_v0(addr: SocketAddr, registry: ServiceRegistry, form: ServiceRegistrationForm) -> Result, Error> { + let service = Service::new(HealthCheckLauncher::new(addr, registry)); + + let started = service.start().map_err(Error::from)?; - let (tx_start, rx_start) = oneshot::channel::(); - let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); + let () = tokio::time::timeout(Duration::from_secs(5), started.reg_form(form)) + .await + .map_err(Error::from)? + .map_err(Error::from)?; - let protocol = "http"; + let (task, _) = started.run(); - // Run the API server - let join_handle = tokio::spawn(async move { - info!(target: "HEALTH CHECK API", "Starting on: {protocol}://{}", bind_addr); + Ok(tokio::spawn(async move { + let server = task.await.expect("it should shutdown"); + drop(server); + })) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + use tracing_test::traced_test; + + use crate::bootstrap::jobs::health_check_api::{start_job, start_v0}; + use crate::servers::health_check_api::Version; + use crate::servers::registar::Registar; + + #[tokio::test] + #[traced_test] + async fn it_should_start_and_stop_the_job_for_the_health_check_service() { + let cfg = Arc::new(ephemeral_mode_public()); + let config = &cfg.health_check_api; + let version = Version::V0; - let handle = server::start(bind_addr, tx_start, rx_halt, register); + let registar = &Registar::default(); - if let Ok(()) = handle.await { - info!(target: "HEALTH CHECK API", "Stopped server running on: {protocol}://{}", bind_addr); - } - }); + let job = start_job(config, registar.get_registry(), registar.give_form(), version).await; - // Wait until the server sends the started message - match rx_start.await { - Ok(msg) => info!(target: "HEALTH CHECK API", "Started on: {protocol}://{}", msg.address), - Err(e) => panic!("the Health Check API server was dropped: {e}"), + job.abort(); + + drop(job); } - // Wait until the server finishes - tokio::spawn(async move { - assert!(!tx_halt.is_closed(), "Halt channel for Health Check API should be open"); + #[tokio::test] + #[traced_test] + async fn it_should_start_and_stop_the_v0_of_the_health_check_service() { + let addr = ephemeral_mode_public() + .health_check_api + .bind_address + .parse() + .expect("it should parse the binding address"); + + let registar = Registar::default(); - join_handle + let job = start_v0(addr, registar.get_registry(), registar.give_form()) .await - .expect("it should be able to join to the Health Check API server task"); - }) + .expect("it should start"); + + job.abort(); + + drop(job); + } } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 0a0638b78..34687afb6 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -14,15 +14,16 @@ use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; +use tracing::{info, instrument}; use super::make_rust_tls; use crate::core; -use crate::servers::http::server::{HttpServer, Launcher}; +use crate::servers::http::service::HttpLauncher; use crate::servers::http::Version; use crate::servers::registar::ServiceRegistrationForm; +use crate::servers::service::Service; /// It starts a new HTTP server with the provided configuration and version. /// @@ -33,6 +34,8 @@ use crate::servers::registar::ServiceRegistrationForm; /// /// It would panic if the `config::HttpTracker` struct would contain inappropriate values. /// +#[allow(clippy::async_yields_async)] +#[instrument(ret)] pub async fn start_job( config: &HttpTracker, tracker: Arc, @@ -58,27 +61,25 @@ pub async fn start_job( } } +#[allow(clippy::async_yields_async)] +#[instrument(ret)] async fn start_v1( socket: SocketAddr, tls: Option, tracker: Arc, form: ServiceRegistrationForm, ) -> JoinHandle<()> { - let server = HttpServer::new(Launcher::new(socket, tls)) - .start(tracker, form) - .await - .expect("it should be able to start to the http tracker"); + let service = Service::new(HttpLauncher::new(tracker, socket, tls)); + + let started = service.start().expect("it should start"); + + let () = started.reg_form(form).await.expect("it should register"); + + let (task, _) = started.run(); tokio::spawn(async move { - assert!( - !server.state.halt_task.is_closed(), - "Halt channel for HTTP tracker should be open" - ); - server - .state - .task - .await - .expect("it should be able to join to the http tracker task"); + let server = task.await.expect("it should shutdown"); + drop(server); }) } @@ -88,7 +89,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - use crate::bootstrap::app::initialize_with_configuration; + use crate::bootstrap::app::tracker; use crate::bootstrap::jobs::http_tracker::start_job; use crate::servers::http::Version; use crate::servers::registar::Registar; @@ -97,11 +98,15 @@ mod tests { async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); let config = &cfg.http_trackers[0]; - let tracker = initialize_with_configuration(&cfg); + let tracker = tracker(&cfg); let version = Version::V1; - start_job(config, tracker, Registar::default().give_form(), version) + let job = start_job(config, tracker, Registar::default().give_form(), version) .await .expect("it should be able to join to the http tracker start-job"); + + job.abort(); + + drop(job); } } diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 2c12eb40e..47ac139be 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -6,20 +6,49 @@ //! 2. Launch all the application services as concurrent jobs. //! //! This modules contains all the functions needed to start those jobs. + +use std::panic::Location; +use std::sync::Arc; + +use axum_server::tls_rustls::RustlsConfig; +use thiserror::Error; +use torrust_tracker_located_error::{DynError, LocatedError}; +use tracing::{info, instrument}; pub mod health_check_api; pub mod http_tracker; pub mod torrent_cleanup; pub mod tracker_apis; pub mod udp_tracker; -/// This is the message that the "launcher" spawned task sends to the main -/// application process to notify the service was successfully started. -/// -#[derive(Debug)] -pub struct Started { - pub address: std::net::SocketAddr, +#[derive(Error, Debug, Clone)] +pub enum Error { + #[error("Timeout elapsed for Task")] + TimeoutError { err: Arc }, + #[error("Error From Service: {err}")] + ServiceError { err: crate::servers::service::Error }, + + #[error("tls config missing")] + MissingTlsConfig { location: &'static Location<'static> }, + + #[error("bad tls config: {source}")] + BadTlsConfig { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, +} + +impl From for Error { + fn from(e: tokio::time::error::Elapsed) -> Self { + Error::TimeoutError { err: e.into() } + } } +impl From for Error { + fn from(err: crate::servers::service::Error) -> Self { + Error::ServiceError { err } + } +} + +#[instrument(ret)] pub async fn make_rust_tls(enabled: bool, cert: &Option, key: &Option) -> Option> { if !enabled { info!("TLS not enabled"); @@ -72,25 +101,3 @@ mod tests { assert_eq!(err.to_string(), "tls config missing"); } } - -use std::panic::Location; -use std::sync::Arc; - -use axum_server::tls_rustls::RustlsConfig; -use log::info; -use thiserror::Error; -use torrust_tracker_located_error::{DynError, LocatedError}; - -/// Error returned by the Bootstrap Process. -#[derive(Error, Debug)] -pub enum Error { - /// Enabled tls but missing config. - #[error("tls config missing")] - MissingTlsConfig { location: &'static Location<'static> }, - - /// Unable to parse tls Config. - #[error("bad tls config: {source}")] - BadTlsConfig { - source: LocatedError<'static, dyn std::error::Error + Send + Sync>, - }, -} diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs index 300813430..4a6464c4a 100644 --- a/src/bootstrap/jobs/torrent_cleanup.rs +++ b/src/bootstrap/jobs/torrent_cleanup.rs @@ -13,9 +13,9 @@ use std::sync::Arc; use chrono::Utc; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::Configuration; +use tracing::{info, instrument}; use crate::core; @@ -25,6 +25,7 @@ use crate::core; /// /// Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about that option. #[must_use] +#[instrument(ret)] pub fn start_job(config: &Configuration, tracker: &Arc) -> JoinHandle<()> { let weak_tracker = std::sync::Arc::downgrade(tracker); let interval = config.inactive_peer_cleanup_interval; diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index ffd7c7407..a6df7b125 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -24,15 +24,16 @@ use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::{AccessTokens, HttpApi}; +use tracing::{info, instrument}; use super::make_rust_tls; use crate::core; -use crate::servers::apis::server::{ApiServer, Launcher}; +use crate::servers::apis::server::ApiLauncher; use crate::servers::apis::Version; use crate::servers::registar::ServiceRegistrationForm; +use crate::servers::service::Service; /// This is the message that the "launcher" spawned task sends to the main /// application process to notify the API server was successfully started. @@ -53,7 +54,8 @@ pub struct ApiServerJobStarted(); /// /// It would panic if unable to send the `ApiServerJobStarted` notice. /// -/// +#[allow(clippy::async_yields_async)] +#[instrument(ret)] pub async fn start_job( config: &HttpApi, tracker: Arc, @@ -81,6 +83,8 @@ pub async fn start_job( } } +#[allow(clippy::async_yields_async)] +#[instrument(ret)] async fn start_v1( socket: SocketAddr, tls: Option, @@ -88,14 +92,17 @@ async fn start_v1( form: ServiceRegistrationForm, access_tokens: Arc, ) -> JoinHandle<()> { - let server = ApiServer::new(Launcher::new(socket, tls)) - .start(tracker, form, access_tokens) - .await - .expect("it should be able to start to the tracker api"); + let service = Service::new(ApiLauncher::new(tracker, access_tokens, socket, tls)); + + let started = service.start().expect("it should start"); + + let () = started.reg_form(form).await.expect("it should register"); + + let (task, _) = started.run(); tokio::spawn(async move { - assert!(!server.state.halt_task.is_closed(), "Halt channel should be open"); - server.state.task.await.expect("failed to close service"); + let server = task.await.expect("it should shutdown"); + drop(server); }) } @@ -105,7 +112,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - use crate::bootstrap::app::initialize_with_configuration; + use crate::bootstrap::app::tracker; use crate::bootstrap::jobs::tracker_apis::start_job; use crate::servers::apis::Version; use crate::servers::registar::Registar; @@ -114,7 +121,7 @@ mod tests { async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); let config = &cfg.http_api; - let tracker = initialize_with_configuration(&cfg); + let tracker = tracker(&cfg); let version = Version::V1; start_job(config, tracker, Registar::default().give_form(), version) diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index e9e4bc642..c0eea5237 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -6,15 +6,18 @@ //! > **NOTICE**: that the application can launch more than one UDP tracker //! on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) //! for the configuration options. +use std::net::SocketAddr; use std::sync::Arc; -use log::debug; use tokio::task::JoinHandle; use torrust_tracker_configuration::UdpTracker; +use tracing::{info, instrument}; use crate::core; use crate::servers::registar::ServiceRegistrationForm; -use crate::servers::udp::server::{Launcher, UdpServer}; +use crate::servers::service::Service; +use crate::servers::udp::server::UdpLauncher; +use crate::servers::udp::Version; /// It starts a new UDP server with the provided configuration. /// @@ -25,33 +28,44 @@ use crate::servers::udp::server::{Launcher, UdpServer}; /// It will panic if the API binding address is not a valid socket. /// It will panic if it is unable to start the UDP service. /// It will panic if the task did not finish successfully. +/// #[must_use] -pub async fn start_job(config: &UdpTracker, tracker: Arc, form: ServiceRegistrationForm) -> JoinHandle<()> { - let bind_to = config - .bind_address - .parse::() - .expect("it should have a valid udp tracker bind address"); +#[allow(clippy::async_yields_async)] +#[instrument(ret)] +pub async fn start_job( + config: &UdpTracker, + tracker: Arc, + form: ServiceRegistrationForm, + version: Version, +) -> Option> { + if config.enabled { + let addr = config + .bind_address + .parse::() + .expect("it should have a valid http tracker bind address"); - let server = UdpServer::new(Launcher::new(bind_to)) - .start(tracker, form) - .await - .expect("it should be able to start the udp tracker"); + match version { + Version::V0 => Some(start_v0(addr, tracker.clone(), form).await), + } + } else { + info!("Note: Not loading Udp Tracker Service, Not Enabled in Configuration."); + None + } +} - tokio::spawn(async move { - debug!(target: "UDP TRACKER", "Wait for launcher (UDP service) to finish ..."); - debug!(target: "UDP TRACKER", "Is halt channel closed before waiting?: {}", server.state.halt_task.is_closed()); +#[allow(clippy::async_yields_async)] +#[instrument(ret)] +async fn start_v0(socket: SocketAddr, tracker: Arc, form: ServiceRegistrationForm) -> JoinHandle<()> { + let service = Service::new(UdpLauncher::new(tracker, socket)); - assert!( - !server.state.halt_task.is_closed(), - "Halt channel for UDP tracker should be open" - ); + let started = service.start().expect("it should start"); - server - .state - .task - .await - .expect("it should be able to join to the udp tracker task"); + let () = started.reg_form(form).await.expect("it should register"); - debug!(target: "UDP TRACKER", "Is halt channel closed after finishing the server?: {}", server.state.halt_task.is_closed()); + let (task, _) = started.run(); + + tokio::spawn(async move { + let server = task.await.expect("it should shutdown"); + drop(server); }) } diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs deleted file mode 100644 index 97e26919d..000000000 --- a/src/bootstrap/logging.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Setup for the application logging. -//! -//! It redirects the log info to the standard output with the log level defined in the configuration. -//! -//! - `Off` -//! - `Error` -//! - `Warn` -//! - `Info` -//! - `Debug` -//! - `Trace` -//! -//! Refer to the [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) to know how to change log settings. -use std::str::FromStr; -use std::sync::Once; - -use log::{info, LevelFilter}; -use torrust_tracker_configuration::Configuration; - -static INIT: Once = Once::new(); - -/// It redirects the log info to the standard output with the log level defined in the configuration -pub fn setup(cfg: &Configuration) { - let level = config_level_or_default(&cfg.log_level); - - if level == log::LevelFilter::Off { - return; - } - - INIT.call_once(|| { - stdout_config(level); - }); -} - -fn config_level_or_default(log_level: &Option) -> LevelFilter { - match log_level { - None => log::LevelFilter::Info, - Some(level) => LevelFilter::from_str(level).unwrap(), - } -} - -fn stdout_config(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - info!("logging initialized."); -} diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index 22044aafd..2f7909043 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -8,4 +8,3 @@ pub mod app; pub mod config; pub mod jobs; -pub mod logging; diff --git a/src/console/ci/e2e/docker.rs b/src/console/ci/e2e/docker.rs index c024efbae..26b7f7708 100644 --- a/src/console/ci/e2e/docker.rs +++ b/src/console/ci/e2e/docker.rs @@ -4,7 +4,7 @@ use std::process::{Command, Output}; use std::thread::sleep; use std::time::{Duration, Instant}; -use log::{debug, info}; +use tracing::{debug, info}; /// Docker command wrapper. pub struct Docker {} diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index 1a4746800..28f1c4a18 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -3,7 +3,7 @@ //! ```text //! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml //! ``` -use log::{debug, info, LevelFilter}; +use tracing::{info, Level}; use super::tracker_container::TrackerContainer; use crate::console::ci::e2e::docker::RunOptions; @@ -32,7 +32,7 @@ pub struct Arguments { /// /// Will panic if it can't not perform any of the operations. pub fn run() { - setup_runner_logging(LevelFilter::Info); + let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); let args = parse_arguments(); @@ -76,27 +76,6 @@ pub fn run() { info!("Tracker container final state:\n{:#?}", tracker_container); } -fn setup_runner_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); -} - fn parse_arguments() -> Arguments { let args: Vec = std::env::args().collect(); diff --git a/src/console/ci/e2e/tracker_checker.rs b/src/console/ci/e2e/tracker_checker.rs index edc679802..b2fd7df2e 100644 --- a/src/console/ci/e2e/tracker_checker.rs +++ b/src/console/ci/e2e/tracker_checker.rs @@ -1,7 +1,7 @@ use std::io; use std::process::Command; -use log::info; +use tracing::info; /// Runs the Tracker Checker. /// diff --git a/src/console/ci/e2e/tracker_container.rs b/src/console/ci/e2e/tracker_container.rs index 5a4d11d02..0cb4fec7c 100644 --- a/src/console/ci/e2e/tracker_container.rs +++ b/src/console/ci/e2e/tracker_container.rs @@ -1,8 +1,8 @@ use std::time::Duration; -use log::{debug, error, info}; use rand::distributions::Alphanumeric; use rand::Rng; +use tracing::{debug, error, info}; use super::docker::{RunOptions, RunningContainer}; use super::logs_parser::RunningServices; diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index 0c9852557..8902524b0 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; -use log::{debug, LevelFilter}; +use tracing::Level; use super::config::Configuration; use super::console::Console; @@ -40,7 +40,7 @@ struct Args { /// /// Will return an error if the configuration was not provided. pub async fn run() -> Result> { - setup_logging(LevelFilter::Info); + let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); let args = Args::parse(); @@ -56,27 +56,6 @@ pub async fn run() -> Result> { service.run_checks().await } -fn setup_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); -} - fn setup_config(args: Args) -> Result { // If a config is directly supplied, we use it. if let Some(config) = args.config_content { diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index a579ffe1e..def9ac015 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -30,7 +30,7 @@ pub async fn run(health_checks: Vec, timeout: Duration, console: Console) - for url in health_checks { let colored_url = url.to_string().yellow(); - match run_health_check(url.clone(), timeout).await { + match run_health_check(url.clone(), &timeout).await { Ok(response) => { console.println(&format!( "{} - Health API at {} is {}", @@ -52,7 +52,7 @@ pub async fn run(health_checks: Vec, timeout: Duration, console: Console) - check_results } -async fn run_health_check(url: Url, timeout: Duration) -> Result { +async fn run_health_check(url: Url, &timeout: &Duration) -> Result { let client = HttpClient::builder() .timeout(timeout) .build() diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index 61b3f961f..b6bd98372 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -20,7 +20,7 @@ pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) - for ref url in http_trackers { let colored_url = url.to_string().yellow(); - check_results.push(match check_http_announce(url, timeout).await { + check_results.push(match check_http_announce(url, &timeout).await { Ok(_) => { console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_url)); Ok(()) @@ -31,7 +31,7 @@ pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) - } }); - check_results.push(match check_http_scrape(url, timeout).await { + check_results.push(match check_http_scrape(url, &timeout).await { Ok(_) => { console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_url)); Ok(()) @@ -46,14 +46,14 @@ pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) - check_results } -async fn check_http_announce(url: &Url, timeout: Duration) -> Result { +async fn check_http_announce(url: &Url, timeout: &Duration) -> Result { let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237 let info_hash = InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required"); - clients::http::check_http_announce(url, timeout, info_hash).await + clients::http::check_http_announce(url, timeout, &info_hash).await } -async fn check_http_scrape(url: &Url, timeout: Duration) -> Result { +async fn check_http_scrape(url: &Url, timeout: &Duration) -> Result { let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237 let info_hashes = vec![InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required")]; // # DevSkim: ignore DS173237 diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 6404e92ef..4279c3180 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -3,8 +3,8 @@ use std::time::Duration; use colored::Colorize; use hex_literal::hex; -use log::debug; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use crate::console::clients::checker::console::Console; use crate::console::clients::checker::printer::Printer; @@ -12,7 +12,7 @@ use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::console::clients::udp::checker::{self, Client}; use crate::console::clients::udp::Error; -pub async fn run(udp_trackers: Vec, _: Duration, console: Console) -> Vec { +pub async fn run(udp_trackers: Vec, timeout: Duration, console: Console) -> Vec { let mut check_results = Vec::default(); console.println("UDP trackers ..."); @@ -26,7 +26,7 @@ pub async fn run(udp_trackers: Vec, _: Duration, console: Console) - // Setup Connection let Ok((client, ctx)) = ({ - let res = setup_connection(addr).await; + let res = setup_connection(addr, &timeout).await; check_results.push(match res { Ok(_) => { @@ -74,7 +74,7 @@ pub async fn run(udp_trackers: Vec, _: Duration, console: Console) - // Do Scrape if { - let res = check_udp_scrape(&client, &ctx, vec![info_hash]).await; + let res = check_udp_scrape(&client, &ctx, &[info_hash]).await; check_results.push(match res { Ok(_) => { @@ -100,8 +100,11 @@ pub async fn run(udp_trackers: Vec, _: Duration, console: Console) - check_results } -async fn setup_connection(addr: &SocketAddr) -> Result<(Client, aquatic_udp_protocol::ConnectResponse), Error> { - let client = checker::Client::bind_and_connect(addr).await?; +async fn setup_connection( + addr: &SocketAddr, + timeout: &Duration, +) -> Result<(Client, aquatic_udp_protocol::ConnectResponse), Error> { + let client = checker::Client::bind_and_connect(addr, timeout).await?; let transaction_id = aquatic_udp_protocol::TransactionId(rand::Rng::gen(&mut rand::thread_rng())); @@ -123,7 +126,7 @@ async fn check_udp_announce( async fn check_udp_scrape( client: &Client, ctx: &aquatic_udp_protocol::ConnectResponse, - infohashes: Vec, + info_hashes: &[InfoHash], ) -> Result { - client.send_scrape_request(ctx, infohashes).await + client.send_scrape_request(ctx, info_hashes).await } diff --git a/src/console/clients/checker/config.rs b/src/console/clients/checker/config.rs index f3be50da1..fecfd420e 100644 --- a/src/console/clients/checker/config.rs +++ b/src/console/clients/checker/config.rs @@ -5,9 +5,7 @@ use std::time::Duration; use reqwest::Url as ServiceUrl; use serde::Deserialize; - -/// Client Timeout -const TIMEOUT_SEC: Duration = Duration::from_secs(5); +use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; /// It parses the configuration from a JSON format. /// @@ -83,7 +81,7 @@ impl TryFrom for Configuration { udp_trackers, http_trackers, health_checks, - client_timeout: TIMEOUT_SEC, + client_timeout: CLIENT_TIMEOUT_DEFAULT, }) } } diff --git a/src/console/clients/http/app.rs b/src/console/clients/http/app.rs index e184fcd14..002ed0802 100644 --- a/src/console/clients/http/app.rs +++ b/src/console/clients/http/app.rs @@ -13,58 +13,67 @@ //! ```text //! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq //! ``` -use std::str::FromStr; use std::time::Duration; -use anyhow::{bail, Context}; +use anyhow::Context; use clap::{Parser, Subcommand}; use reqwest::Url; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::Level; use crate::console::clients::http::{check_http_announce, check_http_scrape}; +use crate::console::clients::{parse_info_hash, parse_url, DEFAULT_TIMEOUT_SEC}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { #[command(subcommand)] command: Command, - timeout: u64, + + #[arg(value_parser = parse_url, help = "tracker url")] + addr: Url, + + /// Name of the person to greet + #[arg(long, default_value = DEFAULT_TIMEOUT_SEC, help = "connection timeout in seconds")] + timeout_sec: u64, } #[derive(Subcommand, Debug)] enum Command { - Announce { tracker_url: String, info_hash: String }, - Scrape { tracker_url: String, info_hashes: Vec }, + Announce { + #[arg(value_parser = parse_info_hash)] + info_hash: InfoHash, + }, + Scrape { + #[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')] + info_hashes: Vec, + }, } /// # Errors /// /// Will return an error if the command fails. pub async fn run() -> anyhow::Result<()> { + let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); + let args = Args::parse(); - let timeout = Duration::from_secs(args.timeout); + let timeout = Duration::from_secs(args.timeout_sec); match args.command { - Command::Announce { tracker_url, info_hash } => { - announce_command(tracker_url, timeout, info_hash).await?; + Command::Announce { info_hash } => { + announce_command(args.addr, &timeout, &info_hash).await?; } - Command::Scrape { - tracker_url, - info_hashes, - } => { - scrape_command(&tracker_url, timeout, &info_hashes).await?; + Command::Scrape { info_hashes } => { + scrape_command(&args.addr, &timeout, &info_hashes).await?; } } Ok(()) } -async fn announce_command(tracker_url: String, timeout: Duration, info_hash: String) -> anyhow::Result<()> { - let url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; - let info_hash = InfoHash::from_str(&info_hash).context("Unable to parse info_hash provided as a string")?; - - let response = check_http_announce(&url, timeout, info_hash) +async fn announce_command(addr: Url, timeout: &Duration, info_hash: &InfoHash) -> anyhow::Result<()> { + let response = check_http_announce(&addr, timeout, info_hash) .await .context("it should get a announce response")?; @@ -75,16 +84,8 @@ async fn announce_command(tracker_url: String, timeout: Duration, info_hash: Str Ok(()) } -async fn scrape_command(tracker_url: &str, timeout: Duration, info_hashes: &[String]) -> anyhow::Result<()> { - let i = info_hashes.iter().map(|s| InfoHash::from_str(s)); - - if i.clone().any(|i| i.is_err()) { - bail!("supplied bad infohash: {:?}", i); - } - - let url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; - - let response = check_http_scrape(&url, timeout, &i.flatten().collect::>()) +async fn scrape_command(addr: &Url, timeout: &Duration, info_hashes: &[InfoHash]) -> anyhow::Result<()> { + let response = check_http_scrape(addr, timeout, info_hashes) .await .context("it should get the scrape result")?; diff --git a/src/console/clients/http/mod.rs b/src/console/clients/http/mod.rs index 8fda21f1d..730b1decb 100644 --- a/src/console/clients/http/mod.rs +++ b/src/console/clients/http/mod.rs @@ -32,7 +32,7 @@ pub enum Error { /// # Errors /// /// This function will return an error if . -pub async fn check_http_announce(url: &Url, timeout: Duration, info_hash: InfoHash) -> Result { +pub async fn check_http_announce(url: &Url, &timeout: &Duration, &info_hash: &InfoHash) -> Result { let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; let response = client @@ -52,7 +52,7 @@ pub async fn check_http_announce(url: &Url, timeout: Duration, info_hash: InfoHa /// # Errors /// /// This function will return an error if . -pub async fn check_http_scrape(url: &Url, timeout: Duration, info_hashes: &[InfoHash]) -> Result { +pub async fn check_http_scrape(url: &Url, &timeout: &Duration, info_hashes: &[InfoHash]) -> Result { let query = info_hashes.iter().copied().collect::().build(); let client = Client::new(url.clone(), timeout).map_err(|err| Error::HttpClientError { err })?; diff --git a/src/console/clients/mod.rs b/src/console/clients/mod.rs index 8492f8ba5..4b1d4aa79 100644 --- a/src/console/clients/mod.rs +++ b/src/console/clients/mod.rs @@ -1,4 +1,87 @@ //! Console clients. + +use std::net::{SocketAddr, ToSocketAddrs as _}; + +use anyhow::Context as _; +use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; +use url::Url; + pub mod checker; pub mod http; pub mod udp; + +const DEFAULT_TIMEOUT_SEC: &str = "5"; + +fn parse_info_hash(info_hash: &str) -> anyhow::Result { + info_hash + .parse() + .map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash}`: {e:?}"))) +} + +fn parse_url(addr: &str) -> anyhow::Result { + Url::parse(addr).with_context(|| format!("failed to parse URL: `{addr}`")) +} + +fn parse_socket_addr(addr: &str) -> anyhow::Result { + debug!("Tracker socket address: {addr:#?}"); + + // Check if the address is a valid URL. If so, extract the host and port. + let resolved_addr = if let Ok(url) = Url::parse(addr) { + debug!("Tracker socket address URL: {url:?}"); + + let host = url + .host_str() + .with_context(|| format!("invalid host in URL: `{addr}`"))? + .to_owned(); + + let port = url + .port() + .with_context(|| format!("port not found in URL: `{addr}`"))? + .to_owned(); + + (host, port) + } else { + // If not a URL, assume it's a host:port pair. + + let parts: Vec<&str> = addr.split(':').collect(); + + if parts.len() != 2 { + return Err(anyhow::anyhow!( + "invalid address format: `{}`. Expected format is host:port", + addr + )); + } + + let host = parts[0].to_owned(); + + let port = parts[1] + .parse::() + .with_context(|| format!("invalid port: `{}`", parts[1]))? + .to_owned(); + + (host, port) + }; + + debug!("Resolved address: {resolved_addr:#?}"); + + // Perform DNS resolution. + let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect(); + if socket_addrs.is_empty() { + Err(anyhow::anyhow!("DNS resolution failed for `{}`", addr)) + } else { + Ok(socket_addrs[0]) + } +} + +#[cfg(test)] +mod tests { + use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; + + use crate::console::clients::DEFAULT_TIMEOUT_SEC; + + #[test] + fn check_timeout_default_is_same_as_configuration_default() { + assert_eq!(DEFAULT_TIMEOUT_SEC, &CLIENT_TIMEOUT_DEFAULT.as_secs().to_string()); + } +} diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 983d3f4ea..4cc0b24f8 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -56,19 +56,19 @@ //! ``` //! //! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`. -use std::net::{SocketAddr, ToSocketAddrs}; -use std::str::FromStr; +use std::net::SocketAddr; +use std::time::Duration; use anyhow::Context; use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Scrape}; use aquatic_udp_protocol::{Port, TransactionId}; use clap::{Parser, Subcommand}; -use log::{debug, LevelFilter}; -use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; -use url::Url; +use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::Level; use crate::console::clients::udp::checker; use crate::console::clients::udp::responses::{AnnounceResponseDto, ScrapeResponseDto}; +use crate::console::clients::{parse_info_hash, parse_socket_addr, DEFAULT_TIMEOUT_SEC}; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; @@ -77,21 +77,24 @@ const RANDOM_TRANSACTION_ID: i32 = -888_840_697; struct Args { #[command(subcommand)] command: Command, + + #[arg(value_parser = parse_socket_addr, help = "tracker url")] + addr: SocketAddr, + + /// Name of the person to greet + #[arg(long, default_value = DEFAULT_TIMEOUT_SEC, help = "connection timeout in seconds")] + timeout_sec: u64, } #[derive(Subcommand, Debug)] enum Command { Announce { - #[arg(value_parser = parse_socket_addr)] - tracker_socket_addr: SocketAddr, #[arg(value_parser = parse_info_hash)] - info_hash: TorrustInfoHash, + info_hash: InfoHash, }, Scrape { - #[arg(value_parser = parse_socket_addr)] - tracker_socket_addr: SocketAddr, #[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')] - info_hashes: Vec, + info_hashes: Vec, }, } @@ -101,49 +104,24 @@ enum Command { /// /// pub async fn run() -> anyhow::Result<()> { - setup_logging(LevelFilter::Info); + let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); let args = Args::parse(); + let timeout = Duration::from_secs(args.timeout_sec); + let response = match args.command { - Command::Announce { - tracker_socket_addr, - info_hash, - } => handle_announce(&tracker_socket_addr, &info_hash).await?, - Command::Scrape { - tracker_socket_addr, - info_hashes, - } => handle_scrape(&tracker_socket_addr, &info_hashes).await?, + Command::Announce { info_hash } => handle_announce(&args.addr, &timeout, &info_hash).await?, + Command::Scrape { info_hashes } => handle_scrape(&args.addr, &timeout, &info_hashes).await?, }; print_response(response) } -fn setup_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); -} - -async fn handle_announce(tracker_socket_addr: &SocketAddr, info_hash: &TorrustInfoHash) -> anyhow::Result { +async fn handle_announce(addr: &SocketAddr, timeout: &Duration, info_hash: &InfoHash) -> anyhow::Result { let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); - let client = checker::Client::bind_and_connect(tracker_socket_addr).await?; + let client = checker::Client::bind_and_connect(addr, timeout).await?; let bound_to = client.client.local_addr()?; @@ -155,15 +133,15 @@ async fn handle_announce(tracker_socket_addr: &SocketAddr, info_hash: &TorrustIn .context("failed to handle announce") } -async fn handle_scrape(tracker_socket_addr: &SocketAddr, info_hashes: &[TorrustInfoHash]) -> anyhow::Result { +async fn handle_scrape(addr: &SocketAddr, timeout: &Duration, info_hashes: &[InfoHash]) -> anyhow::Result { let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); - let client = checker::Client::bind_and_connect(tracker_socket_addr).await?; + let client = checker::Client::bind_and_connect(addr, timeout).await?; let ctx = client.send_connection_request(transaction_id).await?; client - .send_scrape_request(&ctx, info_hashes.to_vec()) + .send_scrape_request(&ctx, info_hashes) .await .context("failed to handle scrape") } @@ -190,59 +168,3 @@ fn print_response(response: Response) -> anyhow::Result<()> { Ok(()) } - -fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result { - debug!("Tracker socket address: {tracker_socket_addr_str:#?}"); - - // Check if the address is a valid URL. If so, extract the host and port. - let resolved_addr = if let Ok(url) = Url::parse(tracker_socket_addr_str) { - debug!("Tracker socket address URL: {url:?}"); - - let host = url - .host_str() - .with_context(|| format!("invalid host in URL: `{tracker_socket_addr_str}`"))? - .to_owned(); - - let port = url - .port() - .with_context(|| format!("port not found in URL: `{tracker_socket_addr_str}`"))? - .to_owned(); - - (host, port) - } else { - // If not a URL, assume it's a host:port pair. - - let parts: Vec<&str> = tracker_socket_addr_str.split(':').collect(); - - if parts.len() != 2 { - return Err(anyhow::anyhow!( - "invalid address format: `{}`. Expected format is host:port", - tracker_socket_addr_str - )); - } - - let host = parts[0].to_owned(); - - let port = parts[1] - .parse::() - .with_context(|| format!("invalid port: `{}`", parts[1]))? - .to_owned(); - - (host, port) - }; - - debug!("Resolved address: {resolved_addr:#?}"); - - // Perform DNS resolution. - let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect(); - if socket_addrs.is_empty() { - Err(anyhow::anyhow!("DNS resolution failed for `{}`", tracker_socket_addr_str)) - } else { - Ok(socket_addrs[0]) - } -} - -fn parse_info_hash(info_hash_str: &str) -> anyhow::Result { - TorrustInfoHash::from_str(info_hash_str) - .map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash_str}`: {e:?}"))) -} diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index d280d1ea7..054bca609 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -1,12 +1,12 @@ use std::net::{Ipv4Addr, SocketAddr}; +use std::time::Duration; -use aquatic_udp_protocol::common::InfoHash; use aquatic_udp_protocol::{ AnnounceEvent, AnnounceRequest, ConnectResponse, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, ScrapeRequest, TransactionId, }; -use log::debug; -use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; +use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use super::Error; use crate::shared::bit_torrent::tracker::udp; @@ -14,7 +14,7 @@ use crate::shared::bit_torrent::tracker::udp; /// A UDP Tracker client to make test requests (checks). #[derive(Debug, Clone)] pub struct Client { - pub client: udp::client::Client, + pub client: udp::Client, } impl Client { @@ -26,10 +26,10 @@ impl Client { /// /// - It can't bound to the local socket address. /// - It can't make a connection request successfully to the remote UDP server. - pub async fn bind_and_connect(addr: &SocketAddr) -> Result { - let client = udp::client::Client::connect(*addr) + pub async fn bind_and_connect(&addr: &SocketAddr, &timeout: &Duration) -> Result { + let client = udp::Client::connect(addr, timeout) .await - .map_err(|err| Error::UnableToBindAndConnect { addr: *addr, err })?; + .map_err(|err| Error::UnableToBindAndConnect { addr, err })?; Ok(Self { client }) } @@ -66,7 +66,7 @@ impl Client { pub async fn send_announce_request( &self, ctx: &ConnectResponse, - info_hash: TorrustInfoHash, + info_hash: InfoHash, client_port: Port, ) -> Result { debug!("Sending announce request with transaction id: {:#?}", ctx.transaction_id); @@ -74,7 +74,7 @@ impl Client { let announce_request = AnnounceRequest { connection_id: ctx.connection_id, transaction_id: ctx.transaction_id, - info_hash: InfoHash(info_hash.bytes()), + info_hash: aquatic_udp_protocol::InfoHash(info_hash.bytes()), peer_id: PeerId(*b"-qB00000000000000001"), bytes_downloaded: NumberOfBytes(0i64), bytes_uploaded: NumberOfBytes(0i64), @@ -109,7 +109,7 @@ impl Client { /// /// Will return and error if the client is not connected. You have to connect /// before calling this function. - pub async fn send_scrape_request(&self, ctx: &ConnectResponse, info_hashes: Vec) -> Result { + pub async fn send_scrape_request(&self, ctx: &ConnectResponse, info_hashes: &[InfoHash]) -> Result { debug!("Sending scrape request with transaction id: {:#?}", ctx.transaction_id); let scrape_request = ScrapeRequest { @@ -117,7 +117,7 @@ impl Client { transaction_id: ctx.transaction_id, info_hashes: info_hashes .iter() - .map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes())) + .map(|torrust_info_hash| aquatic_udp_protocol::InfoHash(torrust_info_hash.bytes())) .collect(), }; diff --git a/src/console/profiling.rs b/src/console/profiling.rs index e0867159f..9bf085950 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -159,12 +159,15 @@ use std::env; use std::time::Duration; -use log::info; use tokio::time::sleep; +use tracing::info; use crate::{app, bootstrap}; pub async fn run() { + let (config, level) = bootstrap::app::config(); + let () = tracing_subscriber::fmt().compact().with_max_level(level).init(); + // Parse command line arguments let args: Vec = env::args().collect(); @@ -180,7 +183,7 @@ pub async fn run() { return; }; - let (config, tracker) = bootstrap::app::setup(); + let tracker = bootstrap::app::tracker(&config); let jobs = app::start(&config, tracker).await; diff --git a/src/core/auth.rs b/src/core/auth.rs index b5326a373..94d455d7e 100644 --- a/src/core/auth.rs +++ b/src/core/auth.rs @@ -42,7 +42,6 @@ use std::sync::Arc; use std::time::Duration; use derive_more::Display; -use log::debug; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; @@ -51,6 +50,7 @@ use torrust_tracker_clock::clock::Time; use torrust_tracker_clock::conv::convert_from_timestamp_to_datetime_utc; use torrust_tracker_located_error::{DynError, LocatedError}; use torrust_tracker_primitives::DurationSinceUnixEpoch; +use tracing::debug; use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH; use crate::CurrentClock; diff --git a/src/core/databases/mod.rs b/src/core/databases/mod.rs index c08aed76a..60c92b0e3 100644 --- a/src/core/databases/mod.rs +++ b/src/core/databases/mod.rs @@ -48,6 +48,7 @@ pub mod error; pub mod mysql; pub mod sqlite; +use std::fmt::Debug; use std::marker::PhantomData; use async_trait::async_trait; @@ -80,7 +81,7 @@ where /// The persistence trait. It contains all the methods to interact with the database. #[async_trait] -pub trait Database: Sync + Send { +pub trait Database: Debug + Sync + Send { /// It instantiates a new database driver. /// /// # Errors diff --git a/src/core/databases/mysql.rs b/src/core/databases/mysql.rs index ca95fa0b9..bff66b710 100644 --- a/src/core/databases/mysql.rs +++ b/src/core/databases/mysql.rs @@ -3,13 +3,13 @@ use std::str::FromStr; use std::time::Duration; use async_trait::async_trait; -use log::debug; use r2d2::Pool; use r2d2_mysql::mysql::prelude::Queryable; use r2d2_mysql::mysql::{params, Opts, OptsBuilder}; use r2d2_mysql::MySqlConnectionManager; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::{DatabaseDriver, PersistentTorrents}; +use tracing::debug; use super::{Database, Error}; use crate::core::auth::{self, Key}; @@ -17,6 +17,7 @@ use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH; const DRIVER: DatabaseDriver = DatabaseDriver::MySQL; +#[derive(Debug)] pub struct Mysql { pool: Pool, } diff --git a/src/core/databases/sqlite.rs b/src/core/databases/sqlite.rs index 53a01f80c..cbddb538e 100644 --- a/src/core/databases/sqlite.rs +++ b/src/core/databases/sqlite.rs @@ -13,6 +13,7 @@ use crate::core::auth::{self, Key}; const DRIVER: DatabaseDriver = DatabaseDriver::Sqlite3; +#[derive(Debug)] pub struct Sqlite { pool: Pool, } diff --git a/src/core/mod.rs b/src/core/mod.rs index 6628426c1..81b3c9572 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -442,7 +442,6 @@ use std::sync::Arc; use std::time::Duration; use derive_more::Constructor; -use log::debug; use tokio::sync::mpsc::error::SendError; use torrust_tracker_clock::clock::Time; use torrust_tracker_configuration::{AnnouncePolicy, Configuration, TrackerPolicy, TORRENT_PEERS_LIMIT}; @@ -452,6 +451,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, TrackerMode}; use torrust_tracker_torrent_repository::entry::EntrySync; use torrust_tracker_torrent_repository::repository::Repository; +use tracing::debug; use self::auth::Key; use self::error::Error; @@ -468,6 +468,8 @@ use crate::CurrentClock; /// > **NOTICE**: the `Tracker` is not responsible for handling the network layer. /// Typically, the `Tracker` is used by a higher application service that handles /// the network layer. +/// +#[derive(Debug)] pub struct Tracker { announce_policy: AnnouncePolicy, /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite) @@ -512,7 +514,7 @@ impl ScrapeData { /// Creates a new `ScrapeData` with zeroed metadata for each torrent. #[must_use] - pub fn zeroed(info_hashes: &Vec) -> Self { + pub fn zeroed(info_hashes: &[InfoHash]) -> Self { let mut scrape_data = Self::empty(); for info_hash in info_hashes { @@ -643,7 +645,7 @@ impl Tracker { /// # Context: Tracker /// /// BEP 48: [Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html). - pub async fn scrape(&self, info_hashes: &Vec) -> ScrapeData { + pub async fn scrape(&self, info_hashes: &[InfoHash]) -> ScrapeData { let mut scrape_data = ScrapeData::empty(); for info_hash in info_hashes { @@ -1422,7 +1424,7 @@ mod tests { .await; // Scrape - let scrape_data = tracker.scrape(&vec![info_hash]).await; + let scrape_data = tracker.scrape(&[info_hash]).await; // The expected swarm metadata for the file let mut expected_scrape_data = ScrapeData::empty(); @@ -1557,7 +1559,7 @@ mod tests { let mut expected_scrape_data = ScrapeData::empty(); expected_scrape_data.add_file_with_zeroed_metadata(&sample_info_hash); - assert_eq!(ScrapeData::zeroed(&vec![sample_info_hash]), expected_scrape_data); + assert_eq!(ScrapeData::zeroed(&[sample_info_hash]), expected_scrape_data); } #[tokio::test] @@ -1573,7 +1575,7 @@ mod tests { let mut peer = complete_peer(); tracker.announce(&info_hash, &mut peer, &peer_ip()).await; - let scrape_data = tracker.scrape(&vec![info_hash]).await; + let scrape_data = tracker.scrape(&[info_hash]).await; // The expected zeroed swarm metadata for the file let mut expected_scrape_data = ScrapeData::empty(); diff --git a/src/core/statistics.rs b/src/core/statistics.rs index f38662cdd..b690fbb4f 100644 --- a/src/core/statistics.rs +++ b/src/core/statistics.rs @@ -17,14 +17,15 @@ //! The [`statistics::Keeper`](crate::core::statistics::Keeper) listens to new events and uses the [`statistics::Repo`](crate::core::statistics::Repo) to upgrade and store metrics. //! //! See the [`statistics::Event`](crate::core::statistics::Event) enum to check which events are available. +use std::fmt::Debug; use std::sync::Arc; use async_trait::async_trait; -use log::debug; #[cfg(test)] use mockall::{automock, predicate::str}; use tokio::sync::mpsc::error::SendError; use tokio::sync::{mpsc, RwLock, RwLockReadGuard}; +use tracing::debug; const CHANNEL_BUFFER_SIZE: usize = 65_535; @@ -187,7 +188,7 @@ async fn event_handler(event: Event, stats_repository: &Repo) { /// A trait to allow sending statistics events #[async_trait] #[cfg_attr(test, automock)] -pub trait EventSender: Sync + Send { +pub trait EventSender: Debug + Sync + Send { async fn send_event(&self, event: Event) -> Option>>; } @@ -195,6 +196,8 @@ pub trait EventSender: Sync + Send { /// /// It uses a channel sender to send the statistic events. The channel is created by a /// [`statistics::Keeper`](crate::core::statistics::Keeper) +/// +#[derive(Debug)] pub struct Sender { sender: mpsc::Sender, } @@ -207,7 +210,7 @@ impl EventSender for Sender { } /// A repository for the tracker metrics. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Repo { pub stats: Arc>, } diff --git a/src/main.rs b/src/main.rs index bd07f4a58..284c3892e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ -use log::info; use torrust_tracker::{app, bootstrap}; +use tracing::info; #[tokio::main] async fn main() { - let (config, tracker) = bootstrap::app::setup(); + let (config, level) = bootstrap::app::config(); + + let () = tracing_subscriber::fmt().compact().with_max_level(level).init(); + + let tracker = bootstrap::app::tracker(&config); let jobs = app::start(&config, tracker).await; diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs index e3d1ef446..8d5f4e013 100644 --- a/src/servers/apis/routes.rs +++ b/src/servers/apis/routes.rs @@ -5,6 +5,7 @@ //! //! All the API routes have the `/api` prefix and the version number as the //! first path segment. For example: `/api/v1/torrents`. +use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -27,7 +28,7 @@ use crate::core::Tracker; /// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] -pub fn router(tracker: Arc, access_tokens: Arc) -> Router { +pub fn router(tracker: Arc, access_tokens: Arc, &_: &SocketAddr) -> Router { let router = Router::new(); let api_url_prefix = "/api"; diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index e72890557..b37c38a5a 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -27,233 +27,140 @@ use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use axum_server::Handle; -use derive_more::Constructor; -use futures::future::BoxFuture; -use log::{debug, error, info}; -use tokio::sync::oneshot::{Receiver, Sender}; +use derive_more::{Constructor, Display}; +use futures::{FutureExt as _, TryFutureExt as _}; use torrust_tracker_configuration::AccessTokens; use super::routes::router; -use crate::bootstrap::jobs::Started; use crate::core::Tracker; -use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; -use crate::servers::signals::{graceful_shutdown, Halted}; +use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; +use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; +use crate::servers::signals::Halted; +use crate::servers::tcp::graceful_axum_shutdown; -/// Errors that can occur when starting or stopping the API server. -#[derive(Debug)] -pub enum Error { - Error(String), -} - -/// An alias for the `ApiServer` struct with the `Stopped` state. -#[allow(clippy::module_name_repetitions)] -pub type StoppedApiServer = ApiServer; +/// Checks the Health by connecting to the API service endpoint. +/// +/// # Errors +/// +/// This function will return an error if unable to connect. +/// Or if there request returns an error code. +#[must_use] +fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { + let url = format!("http://{binding}/api/health_check"); -/// An alias for the `ApiServer` struct with the `Running` state. -#[allow(clippy::module_name_repetitions)] -pub type RunningApiServer = ApiServer; + let info = format!("checking api health check at: {url}"); -/// A struct responsible for starting and stopping an API server with a -/// specific configuration and keeping track of the started server. -/// -/// It's a state machine that can be in one of two -/// states: `Stopped` or `Running`. -#[allow(clippy::module_name_repetitions)] -pub struct ApiServer { - pub state: S, + let job = tokio::spawn(async move { + match reqwest::get(url).await { + Ok(response) => Ok(response.status().to_string()), + Err(err) => Err(err.to_string()), + } + }); + ServiceHealthCheckJob::new(*binding, info, job) } -/// The `Stopped` state of the `ApiServer` struct. -pub struct Stopped { - launcher: Launcher, +#[derive(Debug)] +pub struct ApiHandle { + pub axum_handle: axum_server::Handle, + tx_shutdown: Option>, } -/// The `Running` state of the `ApiServer` struct. -pub struct Running { - pub binding: SocketAddr, - pub halt_task: tokio::sync::oneshot::Sender, - pub task: tokio::task::JoinHandle, -} +impl ApiHandle { + fn shutdown(&mut self) -> Result<(), Error> { + if let Some(tx) = self.tx_shutdown.take() { + tx.send(Halted::Normal) + .map_err(|err| Error::UnableToSendHaltingMessage { err })?; + } else { + panic!("it has already taken the channel?"); + }; -impl Running { - #[must_use] - pub fn new( - binding: SocketAddr, - halt_task: tokio::sync::oneshot::Sender, - task: tokio::task::JoinHandle, - ) -> Self { - Self { - binding, - halt_task, - task, - } + Ok(()) } } -impl ApiServer { - #[must_use] - pub fn new(launcher: Launcher) -> Self { +impl Default for ApiHandle { + fn default() -> Self { + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + let axum_handle = axum_server::Handle::default(); + + let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "Api service".to_owned()); + Self { - state: Stopped { launcher }, + axum_handle: axum_server::Handle::new(), + tx_shutdown: Some(tx_shutdown), } } +} - /// Starts the API server with the given configuration. - /// - /// # Errors - /// - /// It would return an error if no `SocketAddr` is returned after launching the server. - /// - /// # Panics - /// - /// It would panic if the bound socket address cannot be sent back to this starter. - pub async fn start( - self, - tracker: Arc, - form: ServiceRegistrationForm, - access_tokens: Arc, - ) -> Result, Error> { - let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); - let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); - - let launcher = self.state.launcher; - - let task = tokio::spawn(async move { - debug!(target: "API", "Starting with launcher in spawned task ..."); - - let _task = launcher.start(tracker, access_tokens, tx_start, rx_halt).await; - - debug!(target: "API", "Started with launcher in spawned task"); - - launcher - }); - - let api_server = match rx_start.await { - Ok(started) => { - form.send(ServiceRegistration::new(started.address, check_fn)) - .expect("it should be able to send service registration"); - - ApiServer { - state: Running::new(started.address, tx_halt, task), - } - } - Err(err) => { - let msg = format!("Unable to start API server: {err}"); - error!("{}", msg); - panic!("{}", msg); - } - }; +impl Handle for ApiHandle { + fn stop(mut self) -> Result<(), Error> { + self.shutdown() + } - Ok(api_server) + fn listening(&self) -> AddrFuture<'_> { + self.axum_handle.listening().boxed() } } -impl ApiServer { - /// Stops the API server. - /// - /// # Errors - /// - /// It would return an error if the channel for the task killer signal was closed. - pub async fn stop(self) -> Result, Error> { - self.state - .halt_task - .send(Halted::Normal) - .map_err(|_| Error::Error("Task killer channel was closed.".to_string()))?; - - let launcher = self.state.task.await.map_err(|e| Error::Error(e.to_string()))?; - - Ok(ApiServer { - state: Stopped { launcher }, - }) +impl Drop for ApiHandle { + fn drop(&mut self) { + self.shutdown().expect("it should shutdown when dropped"); } } -/// Checks the Health by connecting to the API service endpoint. -/// -/// # Errors -/// -/// This function will return an error if unable to connect. -/// Or if there request returns an error code. -#[must_use] -pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/api/health_check"); - - let info = format!("checking api health check at: {url}"); - - let job = tokio::spawn(async move { - match reqwest::get(url).await { - Ok(response) => Ok(response.status().to_string()), - Err(err) => Err(err.to_string()), - } - }); - ServiceHealthCheckJob::new(*binding, info, job) +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, with tracker, {access_tokens}, and {}", "self.have_tls()")] +pub struct ApiLauncher { + pub tracker: Arc, + pub access_tokens: Arc, + pub addr: SocketAddr, + pub tls: Option, } -/// A struct responsible for starting the API server. -#[derive(Constructor, Debug)] -pub struct Launcher { - bind_to: SocketAddr, - tls: Option, +impl ApiLauncher { + fn have_tls(&self) -> String { + match self.tls { + Some(_) => "some", + None => "none", + } + .to_string() + } } -impl Launcher { - /// Starts the API server with graceful shutdown. - /// - /// If TLS is enabled in the configuration, it will start the server with - /// TLS. See [`torrust-tracker-configuration`](torrust_tracker_configuration) - /// for more information about configuration. - /// - /// # Panics - /// - /// Will panic if unable to bind to the socket, or unable to get the address of the bound socket. - /// Will also panic if unable to send message regarding the bound socket address. - pub fn start( - &self, - tracker: Arc, - access_tokens: Arc, - tx_start: Sender, - rx_halt: Receiver, - ) -> BoxFuture<'static, ()> { - let router = router(tracker, access_tokens); - let socket = std::net::TcpListener::bind(self.bind_to).expect("Could not bind tcp_listener to address."); - let address = socket.local_addr().expect("Could not get local_addr from tcp_listener."); - - let handle = Handle::new(); - - tokio::task::spawn(graceful_shutdown( - handle.clone(), - rx_halt, - format!("Shutting down tracker API server on socket address: {address}"), - )); - - let tls = self.tls.clone(); - let protocol = if tls.is_some() { "https" } else { "http" }; - - info!(target: "API", "Starting on {protocol}://{}", address); - - let running = Box::pin(async { - match tls { - Some(tls) => axum_server::from_tcp_rustls(socket, tls) - .handle(handle) - .serve(router.into_make_service_with_connect_info::()) - .await - .expect("Axum server for tracker API crashed."), - None => axum_server::from_tcp(socket) - .handle(handle) - .serve(router.into_make_service_with_connect_info::()) - .await - .expect("Axum server for tracker API crashed."), +impl Launcher for ApiLauncher { + fn start(self) -> Result<(TaskFuture<'static, (), Error>, ApiHandle, FnSpawnServiceHeathCheck), Error> { + let handle = ApiHandle::default(); + + let running: TaskFuture<'_, (), Error> = { + let listener = std::net::TcpListener::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let addr = listener + .local_addr() + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; + + let make_service = + router(self.tracker, self.access_tokens, &addr).into_make_service_with_connect_info::(); + + match self.tls.clone() { + Some(tls) => axum_server::from_tcp_rustls(listener, tls) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| Error::UnableToServe { err: e.into() }) + .boxed(), + + None => axum_server::from_tcp(listener) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| Error::UnableToServe { err: e.into() }) + .boxed(), } - }); - - info!(target: "API", "Started on {protocol}://{}", address); - - tx_start - .send(Started { address }) - .expect("the HTTP(s) Tracker API service should not be dropped"); + }; - running + Ok((running, handle, check_fn)) } } @@ -263,19 +170,20 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - use crate::bootstrap::app::initialize_with_configuration; + use crate::bootstrap::app::tracker; use crate::bootstrap::jobs::make_rust_tls; - use crate::servers::apis::server::{ApiServer, Launcher}; + use crate::servers::apis::server::ApiLauncher; use crate::servers::registar::Registar; + use crate::servers::service::Service; #[tokio::test] async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); let config = &cfg.http_api; - let tracker = initialize_with_configuration(&cfg); + let tracker = tracker(&cfg); - let bind_to = config + let addr = config .bind_address .parse::() .expect("Tracker API bind_address invalid."); @@ -286,16 +194,15 @@ mod tests { let access_tokens = Arc::new(config.access_tokens.clone()); - let stopped = ApiServer::new(Launcher::new(bind_to, tls)); - let register = &Registar::default(); - let started = stopped - .start(tracker, register.give_form(), access_tokens) - .await - .expect("it should start the server"); + let stopped = Service::new(ApiLauncher::new(tracker, access_tokens, addr, tls)); + + let started = stopped.start().expect("it should start the server"); + let () = started.reg_form(register.give_form()).await.expect("it should register"); + let stopped = started.stop().await.expect("it should stop the server"); - assert_eq!(stopped.state.launcher.bind_to, bind_to); + drop(stopped); } } diff --git a/src/servers/apis/v1/context/torrent/handlers.rs b/src/servers/apis/v1/context/torrent/handlers.rs index 15f70c8b6..b2418c689 100644 --- a/src/servers/apis/v1/context/torrent/handlers.rs +++ b/src/servers/apis/v1/context/torrent/handlers.rs @@ -7,11 +7,11 @@ use std::sync::Arc; use axum::extract::{Path, State}; use axum::response::{IntoResponse, Response}; use axum_extra::extract::Query; -use log::debug; use serde::{de, Deserialize, Deserializer}; use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; +use tracing::debug; use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response}; use crate::core::services::torrent::{get_torrent_info, get_torrents, get_torrents_page}; diff --git a/src/servers/apis/v1/middlewares/auth.rs b/src/servers/apis/v1/middlewares/auth.rs index 58219c7ca..f2ed7f0f8 100644 --- a/src/servers/apis/v1/middlewares/auth.rs +++ b/src/servers/apis/v1/middlewares/auth.rs @@ -79,7 +79,7 @@ impl IntoResponse for AuthError { } fn authenticate(token: &str, tokens: &AccessTokens) -> bool { - tokens.values().any(|t| t == token) + tokens.as_ref().values().any(|t| t == token) } /// `500` error response returned when the token is missing. diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs index 944e84a1d..f63f98c12 100644 --- a/src/servers/health_check_api/handlers.rs +++ b/src/servers/health_check_api/handlers.rs @@ -16,9 +16,9 @@ pub(crate) async fn health_check_handler(State(register): State let mut checks: VecDeque = VecDeque::new(); { - let mutex = register.lock(); + let mutex = register.as_ref().lock().expect("it should get a lock"); - checks = mutex.await.values().map(ServiceRegistration::spawn_check).collect(); + checks = mutex.values().map(ServiceRegistration::spawn_check).collect(); } // if we do not have any checks, lets return a `none` result. @@ -29,7 +29,7 @@ pub(crate) async fn health_check_handler(State(register): State let jobs = checks.drain(..).map(|c| { tokio::spawn(async move { CheckReport { - binding: c.binding, + binding: c.addr, info: c.info.clone(), result: c.job.await.expect("it should be able to join into the checking function"), } diff --git a/src/servers/health_check_api/mod.rs b/src/servers/health_check_api/mod.rs index ec608387d..060fb44a1 100644 --- a/src/servers/health_check_api/mod.rs +++ b/src/servers/health_check_api/mod.rs @@ -2,3 +2,9 @@ pub mod handlers; pub mod resources; pub mod responses; pub mod server; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Version { + /// The `v0` i.e un-versioned version of the Health Check. + V0, +} diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs index 05ed605f4..1146c42e6 100644 --- a/src/servers/health_check_api/server.rs +++ b/src/servers/health_check_api/server.rs @@ -8,46 +8,170 @@ use std::time::Duration; use axum::http::HeaderName; use axum::response::Response; use axum::routing::get; -use axum::{Json, Router}; -use axum_server::Handle; -use futures::Future; +use axum::Json; +use derive_more::{Constructor, Display}; +use futures::{FutureExt as _, TryFutureExt as _}; use hyper::Request; -use log::debug; use serde_json::json; -use tokio::sync::oneshot::{Receiver, Sender}; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; use tower_http::trace::{DefaultMakeSpan, TraceLayer}; -use tracing::{Level, Span}; +use tracing::{error, info, instrument, trace, warn, Level, Span}; -use crate::bootstrap::jobs::Started; use crate::servers::health_check_api::handlers::health_check_handler; -use crate::servers::registar::ServiceRegistry; -use crate::servers::signals::{graceful_shutdown, Halted}; +use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob, ServiceRegistry}; +use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; +use crate::servers::signals::Halted; +use crate::servers::tcp::graceful_axum_shutdown; -/// Starts Health Check API server. +/// Placeholder Check Function for the Health Check Itself /// -/// # Panics +/// # Errors /// -/// Will panic if binding to the socket address fails. -pub fn start( - bind_to: SocketAddr, - tx: Sender, - rx_halt: Receiver, - register: ServiceRegistry, -) -> impl Future> { - let router = Router::new() +/// This function will return an error if the check would fail. +/// +#[must_use] +#[instrument(ret)] +fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { + let url = format!("http://{binding}/health_check"); + + let info = format!("todo self-check health check at: {url}"); + + let job = tokio::spawn(async move { Ok("Todo: Not Implemented".to_string()) }); + ServiceHealthCheckJob::new(*binding, info, job) +} + +pub struct HealthCheckHandle { + pub axum_handle: axum_server::Handle, + tx_shutdown: Option>, +} + +impl std::fmt::Debug for HealthCheckHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HealthCheckHandle") + .field("axum_handle_conn:", &self.axum_handle.connection_count()) + .finish_non_exhaustive() + } +} + +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, registry: {registry}")] +pub struct HealthCheckLauncher { + pub addr: SocketAddr, + pub registry: ServiceRegistry, +} + +impl HealthCheckHandle { + #[instrument(err, ret)] + fn shutdown(&mut self) -> Result<(), Error> { + trace!("the internal shut down was called"); + if let Some(tx) = self.tx_shutdown.take() { + trace!("sending a normal halt on the shutdown channel"); + tx.send(Halted::Normal) + .map_err(|err| Error::UnableToSendHaltingMessage { err })?; + } else { + error!("shutdown was called, but the channel was missing!"); + panic!(); + }; + Ok(()) + } +} + +impl Default for HealthCheckHandle { + #[instrument(ret)] + fn default() -> Self { + trace!("setup the shutdown channel"); + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + trace!("setup the axum handle"); + let axum_handle = axum_server::Handle::default(); + + trace!("setup the graceful axum meta-handler"); + let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "Health Check Server".to_string()); + + trace!("returning the new default handler"); + Self { + axum_handle: axum_server::Handle::new(), + tx_shutdown: Some(tx_shutdown), + } + } +} + +impl Handle for HealthCheckHandle { + #[instrument(ret)] + fn stop(mut self) -> Result<(), Error> { + info!("shutdown function was called"); + self.shutdown() + } + + #[instrument] + fn listening(&self) -> AddrFuture<'_> { + info!("return the listening future form the axum handler"); + self.axum_handle.listening().boxed() + } +} + +impl Drop for HealthCheckHandle { + #[instrument] + fn drop(&mut self) { + warn!("the health check handle was dropped, now shutting down"); + self.shutdown().expect("it should shutdown when dropped"); + } +} + +impl Launcher for HealthCheckLauncher { + #[instrument(err, fields(self = %self))] + fn start(self) -> Result<(TaskFuture<'static, (), Error>, HealthCheckHandle, FnSpawnServiceHeathCheck), Error> { + trace!("setup the health check handler"); + let handle = HealthCheckHandle::default(); + + trace!("make service task"); + let task: TaskFuture<'_, (), Error> = { + trace!(address = ?self.addr, "try to bind on socket"); + let listener = std::net::TcpListener::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + trace!("try to get local address"); + let addr = listener + .local_addr() + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; + info!(address = ?addr, "health tracker bound to tcp socket: {addr}"); + + trace!("setup router"); + let router = router(self.registry, addr); + + trace!("make router into service"); + let make_service = router.into_make_service_with_connect_info::(); + + info!("start and return axum service"); + axum_server::from_tcp(listener) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| Error::UnableToServe { err: e.into() }) + .boxed() + }; + + trace!("returning the axum task, handle, and check function closure"); + Ok((task, handle, check_fn)) + } +} + +#[instrument(fields(registry = %registry))] +fn router(registry: ServiceRegistry, addr: SocketAddr) -> axum::Router { + axum::Router::new() .route("/", get(|| async { Json(json!({})) })) .route("/health_check", get(health_check_handler)) - .with_state(register) + .with_state(registry) .layer(CompressionLayer::new()) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id"))) .layer( TraceLayer::new_for_http() .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) - .on_request(|request: &Request, _span: &Span| { + .on_request( move|request: &Request, _span: &Span| { let method = request.method().to_string(); let uri = request.uri().to_string(); let request_id = request @@ -58,9 +182,9 @@ pub fn start( tracing::span!( target: "HEALTH CHECK API", - tracing::Level::INFO, "request", method = %method, uri = %uri, request_id = %request_id); + tracing::Level::INFO, "request", socket_addr= %addr, method = %method, uri = %uri, request_id = %request_id); }) - .on_response(|response: &Response, latency: Duration, _span: &Span| { + .on_response(move|response: &Response, latency: Duration, _span: &Span| { let status_code = response.status(); let request_id = response .headers() @@ -71,30 +195,8 @@ pub fn start( tracing::span!( target: "HEALTH CHECK API", - tracing::Level::INFO, "response", latency = %latency_ms, status = %status_code, request_id = %request_id); + tracing::Level::INFO, "response", socket_addr= %addr, latency = %latency_ms, status = %status_code, request_id = %request_id); }), ) - .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)); - - let socket = std::net::TcpListener::bind(bind_to).expect("Could not bind tcp_listener to address."); - let address = socket.local_addr().expect("Could not get local_addr from tcp_listener."); - - let handle = Handle::new(); - - debug!(target: "HEALTH CHECK API", "Starting service with graceful shutdown in a spawned task ..."); - - tokio::task::spawn(graceful_shutdown( - handle.clone(), - rx_halt, - format!("Shutting down http server on socket address: {address}"), - )); - - let running = axum_server::from_tcp(socket) - .handle(handle) - .serve(router.into_make_service_with_connect_info::()); - - tx.send(Started { address }) - .expect("the Health Check API server should not be dropped"); - - running + .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) } diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index 3ef85e600..e0b15f492 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -306,7 +306,7 @@ use serde::{Deserialize, Serialize}; pub mod percent_encoding; -pub mod server; +pub mod service; pub mod v1; /// The version of the HTTP tracker. diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs deleted file mode 100644 index decc734c5..000000000 --- a/src/servers/http/server.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Module to handle the HTTP server instances. -use std::net::SocketAddr; -use std::sync::Arc; - -use axum_server::tls_rustls::RustlsConfig; -use axum_server::Handle; -use derive_more::Constructor; -use futures::future::BoxFuture; -use log::info; -use tokio::sync::oneshot::{Receiver, Sender}; - -use super::v1::routes::router; -use crate::bootstrap::jobs::Started; -use crate::core::Tracker; -use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; -use crate::servers::signals::{graceful_shutdown, Halted}; - -/// Error that can occur when starting or stopping the HTTP server. -/// -/// Some errors triggered while starting the server are: -/// -/// - The spawned server cannot send its `SocketAddr` back to the main thread. -/// - The launcher cannot receive the `SocketAddr` from the spawned server. -/// -/// Some errors triggered while stopping the server are: -/// -/// - The channel to send the shutdown signal to the server is closed. -/// - The task to shutdown the server on the spawned server failed to execute to -/// completion. -#[derive(Debug)] -pub enum Error { - Error(String), -} - -#[derive(Constructor, Debug)] -pub struct Launcher { - pub bind_to: SocketAddr, - pub tls: Option, -} - -impl Launcher { - fn start(&self, tracker: Arc, tx_start: Sender, rx_halt: Receiver) -> BoxFuture<'static, ()> { - let socket = std::net::TcpListener::bind(self.bind_to).expect("Could not bind tcp_listener to address."); - let address = socket.local_addr().expect("Could not get local_addr from tcp_listener."); - - let handle = Handle::new(); - - tokio::task::spawn(graceful_shutdown( - handle.clone(), - rx_halt, - format!("Shutting down HTTP server on socket address: {address}"), - )); - - let tls = self.tls.clone(); - let protocol = if tls.is_some() { "https" } else { "http" }; - - info!(target: "HTTP TRACKER", "Starting on: {protocol}://{}", address); - - let app = router(tracker, address); - - let running = Box::pin(async { - match tls { - Some(tls) => axum_server::from_tcp_rustls(socket, tls) - .handle(handle) - .serve(app.into_make_service_with_connect_info::()) - .await - .expect("Axum server crashed."), - None => axum_server::from_tcp(socket) - .handle(handle) - .serve(app.into_make_service_with_connect_info::()) - .await - .expect("Axum server crashed."), - } - }); - - info!(target: "HTTP TRACKER", "Started on: {protocol}://{}", address); - - tx_start - .send(Started { address }) - .expect("the HTTP(s) Tracker service should not be dropped"); - - running - } -} - -/// A HTTP server instance controller with no HTTP instance running. -#[allow(clippy::module_name_repetitions)] -pub type StoppedHttpServer = HttpServer; - -/// A HTTP server instance controller with a running HTTP instance. -#[allow(clippy::module_name_repetitions)] -pub type RunningHttpServer = HttpServer; - -/// A HTTP server instance controller. -/// -/// It's responsible for: -/// -/// - Keeping the initial configuration of the server. -/// - Starting and stopping the server. -/// - Keeping the state of the server: `running` or `stopped`. -/// -/// It's an state machine. Configurations cannot be changed. This struct -/// represents concrete configuration and state. It allows to start and stop the -/// server but always keeping the same configuration. -/// -/// > **NOTICE**: if the configurations changes after running the server it will -/// reset to the initial value after stopping the server. This struct is not -/// intended to persist configurations between runs. -#[allow(clippy::module_name_repetitions)] -pub struct HttpServer { - /// The state of the server: `running` or `stopped`. - pub state: S, -} - -/// A stopped HTTP server state. -pub struct Stopped { - launcher: Launcher, -} - -/// A running HTTP server state. -pub struct Running { - /// The address where the server is bound. - pub binding: SocketAddr, - pub halt_task: tokio::sync::oneshot::Sender, - pub task: tokio::task::JoinHandle, -} - -impl HttpServer { - /// It creates a new `HttpServer` controller in `stopped` state. - #[must_use] - pub fn new(launcher: Launcher) -> Self { - Self { - state: Stopped { launcher }, - } - } - - /// It starts the server and returns a `HttpServer` controller in `running` - /// state. - /// - /// # Errors - /// - /// It would return an error if no `SocketAddr` is returned after launching the server. - /// - /// # Panics - /// - /// It would panic spawned HTTP server launcher cannot send the bound `SocketAddr` - /// back to the main thread. - pub async fn start(self, tracker: Arc, form: ServiceRegistrationForm) -> Result, Error> { - let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); - let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); - - let launcher = self.state.launcher; - - let task = tokio::spawn(async move { - let server = launcher.start(tracker, tx_start, rx_halt); - - server.await; - - launcher - }); - - let binding = rx_start.await.expect("it should be able to start the service").address; - - form.send(ServiceRegistration::new(binding, check_fn)) - .expect("it should be able to send service registration"); - - Ok(HttpServer { - state: Running { - binding, - halt_task: tx_halt, - task, - }, - }) - } -} - -impl HttpServer { - /// It stops the server and returns a `HttpServer` controller in `stopped` - /// state. - /// - /// # Errors - /// - /// It would return an error if the channel for the task killer signal was closed. - pub async fn stop(self) -> Result, Error> { - self.state - .halt_task - .send(Halted::Normal) - .map_err(|_| Error::Error("Task killer channel was closed.".to_string()))?; - - let launcher = self.state.task.await.map_err(|e| Error::Error(e.to_string()))?; - - Ok(HttpServer { - state: Stopped { launcher }, - }) - } -} - -/// Checks the Health by connecting to the HTTP tracker endpoint. -/// -/// # Errors -/// -/// This function will return an error if unable to connect. -/// Or if the request returns an error. -#[must_use] -pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/health_check"); - - let info = format!("checking http tracker health check at: {url}"); - - let job = tokio::spawn(async move { - match reqwest::get(url).await { - Ok(response) => Ok(response.status().to_string()), - Err(err) => Err(err.to_string()), - } - }); - - ServiceHealthCheckJob::new(*binding, info, job) -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - - use crate::bootstrap::app::initialize_with_configuration; - use crate::bootstrap::jobs::make_rust_tls; - use crate::servers::http::server::{HttpServer, Launcher}; - use crate::servers::registar::Registar; - - #[tokio::test] - async fn it_should_be_able_to_start_and_stop() { - let cfg = Arc::new(ephemeral_mode_public()); - let tracker = initialize_with_configuration(&cfg); - let config = &cfg.http_trackers[0]; - - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("tls config failed")); - - let register = &Registar::default(); - - let stopped = HttpServer::new(Launcher::new(bind_to, tls)); - let started = stopped - .start(tracker, register.give_form()) - .await - .expect("it should start the server"); - let stopped = started.stop().await.expect("it should stop the server"); - - assert_eq!(stopped.state.launcher.bind_to, bind_to); - } -} diff --git a/src/servers/http/service.rs b/src/servers/http/service.rs new file mode 100644 index 000000000..fbd1cdade --- /dev/null +++ b/src/servers/http/service.rs @@ -0,0 +1,178 @@ +//! Module to handle the HTTP server instances. +use std::net::SocketAddr; +use std::sync::Arc; + +use axum_server::tls_rustls::RustlsConfig; +use derive_more::{Constructor, Display}; +use futures::{FutureExt, TryFutureExt as _}; + +use super::v1::routes::router; +use crate::core::Tracker; +use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; +use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; +use crate::servers::signals::Halted; +use crate::servers::tcp::graceful_axum_shutdown; + +/// Checks the Health by connecting to the HTTP tracker endpoint. +/// +/// # Errors +/// +/// This function will return an error if unable to connect. +/// Or if the request returns an error. +#[must_use] +fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { + let url = format!("http://{binding}/health_check"); + + let info = format!("checking http tracker health check at: {url}"); + + let job = tokio::spawn(async move { + match reqwest::get(url).await { + Ok(response) => Ok(response.status().to_string()), + Err(err) => Err(err.to_string()), + } + }); + + ServiceHealthCheckJob::new(*binding, info, job) +} + +#[derive(Debug)] +pub struct HttpHandle { + pub axum_handle: axum_server::Handle, + tx_shutdown: Option>, +} + +impl HttpHandle { + fn shutdown(&mut self) -> Result<(), Error> { + let () = if let Some(tx) = self.tx_shutdown.take() { + tx.send(Halted::Normal) + .map_err(|err| Error::UnableToSendHaltingMessage { err })?; + } else { + panic!("it has already taken the channel?"); + }; + Ok(()) + } +} + +impl Default for HttpHandle { + fn default() -> Self { + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + let axum_handle = axum_server::Handle::default(); + + let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "HTTP service".to_string()); + + Self { + axum_handle: axum_server::Handle::new(), + tx_shutdown: Some(tx_shutdown), + } + } +} + +impl Handle for HttpHandle { + fn stop(mut self) -> Result<(), Error> { + self.shutdown() + } + + fn listening(&self) -> AddrFuture<'_> { + self.axum_handle.listening().boxed() + } +} + +impl Drop for HttpHandle { + fn drop(&mut self) { + self.shutdown().expect("it should shutdown when dropped"); + } +} + +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, with tracker, and {}", "self.have_tls()")] +pub struct HttpLauncher { + pub tracker: Arc, + pub addr: SocketAddr, + pub tls: Option, +} + +impl HttpLauncher { + fn have_tls(&self) -> String { + match self.tls { + Some(_) => "some", + None => "none", + } + .to_string() + } +} + +impl Launcher for HttpLauncher { + fn start(self) -> Result<(TaskFuture<'static, (), Error>, HttpHandle, FnSpawnServiceHeathCheck), Error> { + let handle = HttpHandle::default(); + + let running: TaskFuture<'_, (), Error> = { + let listener = std::net::TcpListener::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let addr = listener + .local_addr() + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; + + let make_service = router(self.tracker, &addr).into_make_service_with_connect_info::(); + + match self.tls.clone() { + Some(tls) => axum_server::from_tcp_rustls(listener, tls) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| Error::UnableToServe { err: e.into() }) + .boxed(), + + None => axum_server::from_tcp(listener) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| Error::UnableToServe { err: e.into() }) + .boxed(), + } + }; + + Ok((running, handle, check_fn)) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + + use crate::bootstrap::app::tracker; + use crate::bootstrap::jobs::make_rust_tls; + use crate::servers::http::service::HttpLauncher; + use crate::servers::registar::Registar; + use crate::servers::service::Service; + + #[tokio::test] + async fn it_should_be_able_to_start_and_stop() { + let cfg = Arc::new(ephemeral_mode_public()); + let tracker = tracker(&cfg); + let config = &cfg.http_trackers[0]; + + let bind_to = config + .bind_address + .parse::() + .expect("Tracker API bind_address invalid."); + + let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + .await + .map(|tls| tls.expect("tls config failed")); + + let form = &Registar::default(); + + let stopped = Service::new(HttpLauncher::new(tracker, bind_to, tls)); + + let started = stopped.start().expect("it should start the server"); + let () = started.reg_form(form.give_form()).await.expect("it should register"); + + let stopped = started.stop().await.expect("it should stop the server"); + + drop(stopped); + } +} diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs index e9198f20c..0b009f700 100644 --- a/src/servers/http/v1/handlers/announce.rs +++ b/src/servers/http/v1/handlers/announce.rs @@ -11,10 +11,10 @@ use std::sync::Arc; use axum::extract::State; use axum::response::{IntoResponse, Response}; -use log::debug; use torrust_tracker_clock::clock::Time; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::{peer, NumberOfBytes}; +use tracing::debug; use crate::core::auth::Key; use crate::core::{AnnounceData, Tracker}; diff --git a/src/servers/http/v1/handlers/scrape.rs b/src/servers/http/v1/handlers/scrape.rs index d6b39cc53..172607637 100644 --- a/src/servers/http/v1/handlers/scrape.rs +++ b/src/servers/http/v1/handlers/scrape.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use axum::extract::State; use axum::response::{IntoResponse, Response}; -use log::debug; +use tracing::debug; use crate::core::auth::Key; use crate::core::{ScrapeData, Tracker}; diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index 05cd38713..a04fb280f 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -23,7 +23,7 @@ use crate::core::Tracker; /// > **NOTICE**: it's added a layer to get the client IP from the connection /// info. The tracker could use the connection info to get the client IP. #[allow(clippy::needless_pass_by_value)] -pub fn router(tracker: Arc, server_socket_addr: SocketAddr) -> Router { +pub fn router(tracker: Arc, &addr: &SocketAddr) -> Router { Router::new() // Health check .route("/health_check", get(health_check::handler)) @@ -52,7 +52,7 @@ pub fn router(tracker: Arc, server_socket_addr: SocketAddr) -> Router { tracing::span!( target:"HTTP TRACKER", - tracing::Level::INFO, "request", server_socket_addr= %server_socket_addr, method = %method, uri = %uri, request_id = %request_id); + tracing::Level::INFO, "request", server_socket_addr= %addr, method = %method, uri = %uri, request_id = %request_id); }) .on_response(move |response: &Response, latency: Duration, _span: &Span| { let status_code = response.status(); @@ -65,7 +65,7 @@ pub fn router(tracker: Arc, server_socket_addr: SocketAddr) -> Router { tracing::span!( target: "HTTP TRACKER", - tracing::Level::INFO, "response", server_socket_addr= %server_socket_addr, latency = %latency_ms, status = %status_code, request_id = %request_id); + tracing::Level::INFO, "response", server_socket_addr= %addr, latency = %latency_ms, status = %status_code, request_id = %request_id); }), ) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index 18b57f479..990d0f93d 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -25,7 +25,7 @@ use crate::core::{statistics, ScrapeData, Tracker}; /// > **NOTICE**: as the HTTP tracker does not requires a connection request /// like the UDP tracker, the number of TCP connections is incremented for /// each `scrape` request. -pub async fn invoke(tracker: &Arc, info_hashes: &Vec, original_peer_ip: &IpAddr) -> ScrapeData { +pub async fn invoke(tracker: &Arc, info_hashes: &[InfoHash], original_peer_ip: &IpAddr) -> ScrapeData { let scrape_data = tracker.scrape(info_hashes).await; send_scrape_event(original_peer_ip, tracker).await; @@ -39,7 +39,7 @@ pub async fn invoke(tracker: &Arc, info_hashes: &Vec, origina /// the tracker returns empty stats for all the torrents. /// /// > **NOTICE**: tracker statistics are not updated in this case. -pub async fn fake(tracker: &Arc, info_hashes: &Vec, original_peer_ip: &IpAddr) -> ScrapeData { +pub async fn fake(tracker: &Arc, info_hashes: &[InfoHash], original_peer_ip: &IpAddr) -> ScrapeData { send_scrape_event(original_peer_ip, tracker).await; ScrapeData::zeroed(info_hashes) diff --git a/src/servers/mod.rs b/src/servers/mod.rs index b0e222d2a..bd2e90ba6 100644 --- a/src/servers/mod.rs +++ b/src/servers/mod.rs @@ -3,5 +3,41 @@ pub mod apis; pub mod health_check_api; pub mod http; pub mod registar; +pub mod service; pub mod signals; pub mod udp; + +pub mod tcp { + use std::time::Duration; + + use tracing::info; + + use super::signals::Halted; + use crate::servers::signals::shutdown_signal_with_message; + + pub fn graceful_axum_shutdown( + handle: axum_server::Handle, + rx_shutdown: tokio::sync::oneshot::Receiver, + message: String, + ) { + tokio::task::spawn(async move { + match handle.listening().await { + Some(addr) => { + shutdown_signal_with_message(rx_shutdown, format!("{message}, on socket address: {addr}")).await; + + info!("Sending graceful shutdown signal"); + handle.graceful_shutdown(Some(Duration::from_secs(90))); + + println!("!! shuting down in 90 seconds !!"); + + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + + info!("remaining alive connections: {}", handle.connection_count()); + } + } + None => handle.shutdown(), + } + }); + } +} diff --git a/src/servers/registar.rs b/src/servers/registar.rs index 9c23573c4..4237f630a 100644 --- a/src/servers/registar.rs +++ b/src/servers/registar.rs @@ -2,12 +2,11 @@ use std::collections::HashMap; use std::net::SocketAddr; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; -use derive_more::Constructor; -use log::debug; -use tokio::sync::Mutex; +use derive_more::{AsRef, Constructor, Display}; use tokio::task::JoinHandle; +use tracing::debug; /// A [`ServiceHeathCheckResult`] is returned by a completed health check. pub type ServiceHeathCheckResult = Result; @@ -17,7 +16,7 @@ pub type ServiceHeathCheckResult = Result; /// The `job` awaits a [`ServiceHeathCheckResult`]. #[derive(Debug, Constructor)] pub struct ServiceHealthCheckJob { - pub binding: SocketAddr, + pub addr: SocketAddr, pub info: String, pub job: JoinHandle, } @@ -32,14 +31,14 @@ pub type FnSpawnServiceHeathCheck = fn(&SocketAddr) -> ServiceHealthCheckJob; /// Each registration includes a function that fulfils the [`FnSpawnServiceHeathCheck`] specification. #[derive(Clone, Debug, Constructor)] pub struct ServiceRegistration { - binding: SocketAddr, + addr: SocketAddr, check_fn: FnSpawnServiceHeathCheck, } impl ServiceRegistration { #[must_use] pub fn spawn_check(&self) -> ServiceHealthCheckJob { - (self.check_fn)(&self.binding) + (self.check_fn)(&self.addr) } } @@ -47,7 +46,17 @@ impl ServiceRegistration { pub type ServiceRegistrationForm = tokio::sync::oneshot::Sender; /// The [`ServiceRegistry`] contains each unique [`ServiceRegistration`] by it's [`SocketAddr`]. -pub type ServiceRegistry = Arc>>; +#[derive(AsRef, Clone, Debug, Default, Display)] +#[display(fmt = "targets: {:?}", "self.targets()")] +pub struct ServiceRegistry { + registry: Arc>>, +} + +impl ServiceRegistry { + fn targets(&self) -> Vec { + self.registry.lock().expect("it should get a lock").keys().copied().collect() + } +} /// The [`Registar`] manages the [`ServiceRegistry`]. #[derive(Clone, Debug)] @@ -55,6 +64,14 @@ pub struct Registar { registry: ServiceRegistry, } +impl Registar { + /// Please try to only use for testing... + #[must_use] + pub fn get_registry(&self) -> ServiceRegistry { + self.registry.clone() + } +} + #[allow(clippy::derivable_impls)] impl Default for Registar { fn default() -> Self { @@ -65,6 +82,7 @@ impl Default for Registar { } impl Registar { + #[must_use] pub fn new(register: ServiceRegistry) -> Self { Self { registry: register } } @@ -88,9 +106,9 @@ impl Registar { .await .expect("it should receive the service registration from the started service"); - let mut mutex = self.registry.lock().await; + let mut mutex = self.registry.as_ref().lock().expect("it should get a lock"); - mutex.insert(service_registration.binding, service_registration); + mutex.insert(service_registration.addr, service_registration); } /// Returns the [`ServiceRegistry`] of services diff --git a/src/servers/service.rs b/src/servers/service.rs new file mode 100644 index 000000000..9899dc670 --- /dev/null +++ b/src/servers/service.rs @@ -0,0 +1,256 @@ +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; +use std::net::SocketAddr; +use std::sync::Arc; + +use futures::future::BoxFuture; +use thiserror::Error; +use tokio::task::{JoinError, JoinHandle}; +use tracing::{debug, instrument, trace}; + +use super::registar::{FnSpawnServiceHeathCheck, ServiceRegistration, ServiceRegistrationForm}; +use super::signals::Halted; +use super::udp::server::UdpError; + +#[derive(Error, Debug, Clone)] +pub enum Error { + #[error("Failed to bind to socket: {addr}, with error: {err}")] + UnableToBindToSocket { addr: SocketAddr, err: Arc }, + #[error("Failed to get Local Address from Socket: {err:?}")] + UnableToGetLocalAddress { err: Arc }, + #[error("Failed to get listening address.")] + UnableToGetListeningAddress {}, + #[error("Failed to serve service: {err:?}")] + UnableToServe { err: Arc }, + #[error("Failed to receive started message: {err:?}")] + UnableToReceiveStartedMessage { + err: Arc, + }, + #[error("Failed to send ServiceRegistration: {err:?}")] + UnableToSendRegistrationMessage { err: ServiceRegistration }, + #[error("Failed to send Halted: {err:?}")] + UnableToSendHaltingMessage { err: Halted }, + #[error("Failed to join task when stopping: {err:?}")] + UnableToJoinStoppingService { err: Arc }, + #[error("Failed to start udp service: {err:?}")] + UnableToStartUdpService { err: UdpError }, + + #[error("Failed to join the tokio task: {err:?}")] + UnableJoinTokioTask { err: Arc }, +} + +pub type AddrFuture<'a> = BoxFuture<'a, Option>; + +pub type TaskFuture<'a, T, E> = BoxFuture<'a, Result>; +pub type TaskHandle = JoinHandle>; + +pub trait Handle: Debug + Default + Send + 'static { + /// Stops the Service + /// + /// # Errors + /// + /// This function will return an error if the service fails to stop cleanly. + /// + fn stop(self) -> Result<(), Error>; + + fn listening(&self) -> AddrFuture<'_>; +} + +pub trait Launcher: Clone + Debug + Display + Send + 'static +where + H: Handle, +{ + /// Starts the service + /// + /// # Errors + /// + /// This function will return an error if the launching fails, + /// or when the future returns with an error. + /// + fn start(self) -> Result<(TaskFuture<'static, (), Error>, H, FnSpawnServiceHeathCheck), Error>; +} + +/// A service instance controller. +/// +/// It's responsible for: +/// +/// - Starting and stopping a service. +/// - Keeping the state of the server: `running` or `stopped`. +/// +#[allow(clippy::module_name_repetitions)] +pub struct Service +where + L: Launcher + Send + 'static, + H: Handle, + S: Debug, +{ + phantom: PhantomData, + /// The state of the server: `running` or `stopped`. + pub state: S, + launcher: L, +} + +impl Debug for Service +where + L: Launcher + Send + 'static, + H: Handle, + S: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Service").field("state", &self.state).finish_non_exhaustive() + } +} + +/// A stopped service state. +#[derive(Debug)] +pub struct Stopped {} + +/// A running service state. +pub struct Started +where + H: Handle, +{ + task: TaskHandle<(), Error>, + pub handle: H, + check_fn: FnSpawnServiceHeathCheck, +} + +impl Started { + #[instrument(skip(task))] + pub fn new(task: TaskFuture<'static, (), Error>, handle: H, check_fn: FnSpawnServiceHeathCheck) -> Self { + debug!("spawning the task in tokio"); + let task: TaskHandle<(), Error> = tokio::task::spawn(task); + + Self { task, handle, check_fn } + } +} + +impl Debug for Started +where + H: Handle, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Started") + .field("handle", &self.handle) + .finish_non_exhaustive() + } +} + +impl + Send + 'static> Service { + #[must_use] + #[instrument(ret, fields(launcher = %launcher))] + pub fn new(launcher: L) -> Self { + Self { + phantom: PhantomData, + state: Stopped {}, + launcher, + } + } + + /// It starts the server and returns a controller in `running` state. + /// + /// # Errors + /// + /// It would return an error if the underling launcher returns an error. + /// + #[instrument(err, ret)] + pub fn start(self) -> Result, L, H>, Error> { + trace!("starting the task"); + let (task, handle, check_fn) = self.launcher.clone().start()?; + + trace!("building the service in the started state"); + Ok(Service { + phantom: PhantomData, + state: Started::new(task, handle, check_fn), + launcher: self.launcher, + }) + } +} + +impl<'a, H: Handle, L: Launcher + Send + 'a> Service, L, H> { + /// Returns the listening Address of this Service. + /// + /// # Errors + /// + /// This function will return an error if unable to get address. + /// + #[instrument(err, ret)] + pub async fn listening(&self) -> Result { + trace!("awaiting the service to inform it's ready and listening"); + self.state + .handle + .listening() + .await + .ok_or_else(|| Error::UnableToGetListeningAddress {}) + } + + /// It registers the service on a form. + /// + /// # Errors + /// + /// It would return an error unable to get the current address, + /// or unable to complete the registration. + /// + #[instrument(err, ret, skip(form))] + pub async fn reg_form(&self, form: ServiceRegistrationForm) -> Result<(), Error> { + trace!("awaiting for the service to be ready and return it's local address"); + let addr = self.listening().await?; + + trace!( + "sends the service registration on the supplied form, + with the local address and self-check closure" + ); + form.send(ServiceRegistration::new(addr, self.state.check_fn)) + .map_err(|err| Error::UnableToSendRegistrationMessage { err }) + } + + /// It returns the active task with it's handler. + /// + /// When the task completes, it will return a controller in the stopped state. + /// + #[instrument()] + pub fn run(self) -> (TaskHandle, Error>, H) { + trace!( + "gets the service task future and changes it's future + return type to include the service in the stopped state" + ); + + let task = tokio::task::spawn(async move { + let () = self + .state + .task + .await + .map_err(|e| Error::UnableJoinTokioTask { err: e.into() })??; + + Ok(Service { + phantom: PhantomData, + state: Stopped {}, + launcher: self.launcher, + }) + }); + + (task, self.state.handle) + } + + /// It stops the server and returns a controller in `stopped` + /// state. + /// + /// # Errors + /// + /// It would return an error if unable to stop, of the task finished with an error. + #[instrument(err, ret)] + pub async fn stop(self) -> Result, Error> { + let () = self.state.handle.stop()?; + let () = self + .state + .task + .await + .map_err(|e| Error::UnableJoinTokioTask { err: e.into() })??; + + Ok(Service { + phantom: PhantomData, + state: Stopped {}, + launcher: self.launcher, + }) + } +} diff --git a/src/servers/signals.rs b/src/servers/signals.rs index 42fd868e8..956fe3079 100644 --- a/src/servers/signals.rs +++ b/src/servers/signals.rs @@ -1,16 +1,15 @@ //! This module contains functions to handle signals. -use std::time::Duration; use derive_more::Display; -use log::info; -use tokio::time::sleep; +use tracing::info; /// This is the message that the "launcher" spawned task receives from the main /// application process to notify the service to shutdown. /// -#[derive(Copy, Clone, Debug, Display)] +#[derive(Copy, Clone, Debug, Display, PartialEq, Eq)] pub enum Halted { Normal, + Dropped, } /// Resolves on `ctrl_c` or the `terminate` signal. @@ -65,18 +64,3 @@ pub async fn shutdown_signal_with_message(rx_halt: tokio::sync::oneshot::Receive info!("{message}"); } - -pub async fn graceful_shutdown(handle: axum_server::Handle, rx_halt: tokio::sync::oneshot::Receiver, message: String) { - shutdown_signal_with_message(rx_halt, message).await; - - info!("Sending graceful shutdown signal"); - handle.graceful_shutdown(Some(Duration::from_secs(90))); - - println!("!! shuting down in 90 seconds !!"); - - loop { - sleep(Duration::from_secs(1)).await; - - info!("remaining alive connections: {}", handle.connection_count()); - } -} diff --git a/src/servers/udp/handle.rs b/src/servers/udp/handle.rs new file mode 100644 index 000000000..42bae2e1a --- /dev/null +++ b/src/servers/udp/handle.rs @@ -0,0 +1,167 @@ +//! Copy-Paste from axum. Thanks. + +use std::net::SocketAddr; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tokio::sync::Notify; +use tokio::time::sleep; + +/// A handle for [`Server`](crate::server::Server). +#[derive(Clone, Debug, Default)] +pub struct Handle { + inner: Arc, +} + +#[derive(Debug, Default)] +struct HandleInner { + addr: Mutex>, + addr_notify: Notify, + conn_count: AtomicUsize, + shutdown: NotifyOnce, + graceful: NotifyOnce, + graceful_dur: Mutex>, + conn_end: NotifyOnce, +} + +impl Handle { + /// Create a new handle. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Get the number of connections. + #[must_use] + pub fn connection_count(&self) -> usize { + self.inner.conn_count.load(Ordering::SeqCst) + } + + /// Shutdown the server. + pub fn shutdown(&self) { + self.inner.shutdown.notify_waiters(); + } + + /// Gracefully shutdown the server. + /// + /// `None` means indefinite grace period. + /// + #[allow(clippy::missing_panics_doc)] + pub fn graceful_shutdown(&self, duration: Option) { + *self.inner.graceful_dur.lock().unwrap() = duration; + + self.inner.graceful.notify_waiters(); + } + + /// Returns local address and port when server starts listening. + /// + /// Returns `None` if server fails to bind. + /// + #[allow(clippy::missing_panics_doc)] + pub async fn listening(&self) -> Option { + let notified = self.inner.addr_notify.notified(); + + if let Some(addr) = *self.inner.addr.lock().unwrap() { + return Some(addr); + } + + notified.await; + + *self.inner.addr.lock().unwrap() + } + + pub(crate) fn notify_listening(&self, addr: Option) { + *self.inner.addr.lock().unwrap() = addr; + + self.inner.addr_notify.notify_waiters(); + } + + pub(crate) fn watcher(&self) -> Watcher { + Watcher::new(self.clone()) + } + + pub(crate) async fn wait_shutdown(&self) { + self.inner.shutdown.notified().await; + } + + pub(crate) async fn wait_graceful_shutdown(&self) { + self.inner.graceful.notified().await; + } + + #[allow(dead_code)] + pub(crate) async fn wait_connections_end(&self) { + if self.inner.conn_count.load(Ordering::SeqCst) == 0 { + return; + } + + let deadline = *self.inner.graceful_dur.lock().unwrap(); + + match deadline { + Some(duration) => tokio::select! { + biased; + () = sleep(duration) => self.shutdown(), + () = self.inner.conn_end.notified() => (), + }, + None => self.inner.conn_end.notified().await, + } + } +} + +#[derive(Debug)] +pub(crate) struct Watcher { + handle: Handle, +} + +impl Watcher { + fn new(handle: Handle) -> Self { + handle.inner.conn_count.fetch_add(1, Ordering::SeqCst); + + Self { handle } + } + + #[allow(dead_code)] + pub(crate) async fn wait_graceful_shutdown(&self) { + self.handle.wait_graceful_shutdown().await; + } + + pub(crate) async fn wait_shutdown(&self) { + self.handle.wait_shutdown().await; + } +} + +impl Drop for Watcher { + fn drop(&mut self) { + let count = self.handle.inner.conn_count.fetch_sub(1, Ordering::SeqCst) - 1; + + if count == 0 && self.handle.inner.graceful.is_notified() { + self.handle.inner.conn_end.notify_waiters(); + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct NotifyOnce { + notified: AtomicBool, + notify: Notify, +} + +impl NotifyOnce { + pub(crate) fn notify_waiters(&self) { + self.notified.store(true, Ordering::SeqCst); + + self.notify.notify_waiters(); + } + + pub(crate) fn is_notified(&self) -> bool { + self.notified.load(Ordering::SeqCst) + } + + pub(crate) async fn notified(&self) { + let future = self.notify.notified(); + + if !self.notified.load(Ordering::SeqCst) { + future.await; + } + } +} diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 2d5038ec3..4215b231a 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -9,10 +9,10 @@ use aquatic_udp_protocol::{ AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; -use log::debug; use tokio::net::UdpSocket; use torrust_tracker_located_error::DynError; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use uuid::Uuid; use super::connection_cookie::{check, from_connection_id, into_connection_id, make}; @@ -33,12 +33,12 @@ use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; /// type. /// /// It will return an `Error` response if the request is invalid. -pub(crate) async fn handle_packet(udp_request: UdpRequest, tracker: &Arc, socket: Arc) -> Response { +pub(crate) async fn handle_packet(udp_request: &UdpRequest, tracker: &Arc, socket: Arc) -> Response { debug!("Handling Packets: {udp_request:?}"); let start_time = Instant::now(); - let request_id = RequestId::make(&udp_request); + let request_id = RequestId::make(udp_request); let server_socket_addr = socket.local_addr().expect("Could not get local_addr for socket."); match Request::from_bytes(&udp_request.payload[..udp_request.payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| { diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index fa4e8e926..9c5dac58e 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -643,12 +643,17 @@ use std::net::SocketAddr; pub mod connection_cookie; pub mod error; +pub mod handle; pub mod handlers; pub mod logging; pub mod peer_builder; pub mod request; pub mod server; +pub use handle::Handle; + +use self::handle::Watcher; + /// Number of bytes. pub type Bytes = u64; /// The port the peer is listening on. @@ -657,8 +662,23 @@ pub type Port = u16; /// match requests and responses. pub type TransactionId = i64; -#[derive(Clone, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Version { + /// The `v0` i.e un-versioned version of the Udp API. + V0, +} + pub(crate) struct UdpRequest { payload: Vec, from: SocketAddr, + watcher: Watcher, +} + +impl std::fmt::Debug for UdpRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UdpRequest") + .field("payload", &self.payload) + .field("from", &self.from) + .finish_non_exhaustive() + } } diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 77432a957..cb6036d60 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -17,188 +17,142 @@ //! because we want to be able to start and stop the server multiple times, and //! we want to know the bound address and the current state of the server. //! In production, the `Udp` launcher is used directly. -use std::io::Cursor; use std::net::SocketAddr; use std::sync::Arc; +use std::time::Duration; -use aquatic_udp_protocol::Response; -use derive_more::Constructor; -use futures::TryFutureExt; -use log::{debug, error, info, trace}; +use derive_more::{Constructor, Display}; +use futures::future::BoxFuture; +use futures::{FutureExt, TryFutureExt as _}; use ringbuf::{Rb, StaticRb}; +use thiserror::Error; use tokio::net::UdpSocket; -use tokio::sync::oneshot; +use tokio::select; use tokio::task::{AbortHandle, JoinHandle}; -use tokio::{select, task}; -use torrust_tracker_configuration::MAX_PACKET_SIZE; +use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE}; +use tracing::{debug, error, info, instrument, trace, Level}; +use super::handle::Watcher; use super::UdpRequest; -use crate::bootstrap::jobs::Started; use crate::core::Tracker; -use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; +use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; +use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; use crate::servers::signals::{shutdown_signal_with_message, Halted}; use crate::servers::udp::handlers; -use crate::shared::bit_torrent::tracker::udp::client::Client; - -/// Error that can occur when starting or stopping the UDP server. -/// -/// Some errors triggered while starting the server are: -/// -/// - The server cannot bind to the given address. -/// - It cannot get the bound address. -/// -/// Some errors triggered while stopping the server are: -/// -/// - The [`UdpServer`] cannot send the -/// shutdown signal to the spawned UDP service thread. -#[derive(Debug)] -pub enum Error { - /// Any kind of error starting or stopping the server. - Error(String), // todo: refactor to use thiserror and add more variants for specific errors. +use crate::shared::bit_torrent::tracker::udp::Client; + +#[derive(Error, Debug, Clone)] +pub enum UdpError { + #[error("Gracefully Canceled Processing Requests For: {addr}")] + GracefullyCanceled { addr: SocketAddr }, + #[error("Canceled Sending Response to Target: {target}")] + CanceledSending { target: SocketAddr }, + + #[error("Failed to get Local Address from Socket: {err:?}")] + UnableToGetLocalAddress { err: Arc }, + + #[error("Failed to get Listening Address from Socket")] + UnableToGetListeningAddress {}, + #[error("Socket Errored when waiting to read from: {addr}, with error: {err:?}")] + UnableToGetReadableSocket { addr: SocketAddr, err: Arc }, + #[error("Socket Errored when reading from: {addr}, with error: {err:?}")] + UnableToReadFromSocket { addr: SocketAddr, err: Arc }, + #[error("Socket Errored when sending to: {target}, with error: {err:?}")] + UnableToSendToSocket { target: SocketAddr, err: Arc }, + #[error("Socket Errored when writing to: {target}, with error: {err:?}")] + UnableToWriteToSocket { target: SocketAddr, err: Arc }, } -/// A UDP server instance controller with no UDP instance running. -#[allow(clippy::module_name_repetitions)] -pub type StoppedUdpServer = UdpServer; - -/// A UDP server instance controller with a running UDP instance. -#[allow(clippy::module_name_repetitions)] -pub type RunningUdpServer = UdpServer; - -/// A UDP server instance controller. -/// -/// It's responsible for: -/// -/// - Keeping the initial configuration of the server. -/// - Starting and stopping the server. -/// - Keeping the state of the server: `running` or `stopped`. -/// -/// It's an state machine. Configurations cannot be changed. This struct -/// represents concrete configuration and state. It allows to start and stop the -/// server but always keeping the same configuration. -/// -/// > **NOTICE**: if the configurations changes after running the server it will -/// reset to the initial value after stopping the server. This struct is not -/// intended to persist configurations between runs. -#[allow(clippy::module_name_repetitions)] -pub struct UdpServer { - /// The state of the server: `running` or `stopped`. - pub state: S, -} +fn check_fn(&addr: &SocketAddr) -> ServiceHealthCheckJob { + let info = format!("checking the udp tracker health check at: {addr}"); -/// A stopped UDP server state. + let job = tokio::spawn( + Client::connect(addr, CLIENT_TIMEOUT_DEFAULT) + .and_then(Client::check) + .map_err(|e| e.to_string()), + ); -pub struct Stopped { - launcher: Launcher, + ServiceHealthCheckJob::new(addr, info, job) } -/// A running UDP server state. -#[derive(Debug, Constructor)] -pub struct Running { - /// The address where the server is bound. - pub binding: SocketAddr, - pub halt_task: tokio::sync::oneshot::Sender, - pub task: JoinHandle, +#[derive(Debug)] +pub struct UdpHandle { + tx_shutdown: Option>, + pub udp_handle: super::handle::Handle, } -impl UdpServer { - /// Creates a new `UdpServer` instance in `stopped`state. - #[must_use] - pub fn new(launcher: Launcher) -> Self { - Self { - state: Stopped { launcher }, - } +impl UdpHandle { + #[instrument(err, ret)] + fn shutdown(&mut self) -> Result<(), Error> { + if let Some(tx) = self.tx_shutdown.take() { + tx.send(Halted::Normal) + .map_err(|err| Error::UnableToSendHaltingMessage { err })?; + } else { + panic!("it has already taken the channel?"); + }; + Ok(()) } +} - /// It starts the server and returns a `UdpServer` controller in `running` - /// state. - /// - /// # Errors - /// - /// Will return `Err` if UDP can't bind to given bind address. - /// - /// # Panics - /// - /// It panics if unable to receive the bound socket address from service. - /// - pub async fn start(self, tracker: Arc, form: ServiceRegistrationForm) -> Result, Error> { - let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); - let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); - - assert!(!tx_halt.is_closed(), "Halt channel for UDP tracker should be open"); - - // May need to wrap in a task to about a tokio bug. - let task = self.state.launcher.start(tracker, tx_start, rx_halt); - - let binding = rx_start.await.expect("it should be able to start the service").address; - - form.send(ServiceRegistration::new(binding, Udp::check)) - .expect("it should be able to send service registration"); - - let running_udp_server: UdpServer = UdpServer { - state: Running { - binding, - halt_task: tx_halt, - task, - }, - }; +impl Default for UdpHandle { + fn default() -> Self { + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + let udp_handle = super::handle::Handle::default(); - trace!("Running UDP Tracker on Socket: {}", running_udp_server.state.binding); + let () = Udp::graceful_udp_shutdown(udp_handle.clone(), rx_shutdown, "UDP service".to_string()); - Ok(running_udp_server) + Self { + udp_handle: super::handle::Handle::new(), + tx_shutdown: Some(tx_shutdown), + } } } -impl UdpServer { - /// It stops the server and returns a `UdpServer` controller in `stopped` - /// state. - /// - /// # Errors - /// - /// Will return `Err` if the oneshot channel to send the stop signal - /// has already been called once. - /// - /// # Panics - /// - /// It panics if unable to shutdown service. - pub async fn stop(self) -> Result, Error> { - self.state - .halt_task - .send(Halted::Normal) - .map_err(|e| Error::Error(e.to_string()))?; - - let launcher = self.state.task.await.expect("unable to shutdown service"); - - let stopped_api_server: UdpServer = UdpServer { - state: Stopped { launcher }, - }; +impl Handle for UdpHandle { + fn stop(mut self) -> Result<(), Error> { + self.shutdown() + } - Ok(stopped_api_server) + fn listening(&self) -> AddrFuture<'_> { + self.udp_handle.listening().boxed() } } -#[derive(Constructor, Copy, Clone, Debug)] -pub struct Launcher { - bind_to: SocketAddr, +impl Drop for UdpHandle { + fn drop(&mut self) { + self.shutdown().expect("it should shutdown when dropped"); + } } -impl Launcher { - /// It starts the UDP server instance. - /// - /// # Panics - /// - /// It would panic if unable to resolve the `local_addr` from the supplied ´socket´. - pub fn start( - &self, - tracker: Arc, - tx_start: oneshot::Sender, - rx_halt: oneshot::Receiver, - ) -> JoinHandle { - let launcher = Launcher::new(self.bind_to); - tokio::spawn(async move { - Udp::run_with_graceful_shutdown(tracker, launcher.bind_to, tx_start, rx_halt).await; - launcher - }) +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, with tracker")] +pub struct UdpLauncher { + pub tracker: Arc, + pub addr: SocketAddr, +} + +impl Launcher for UdpLauncher { + #[instrument(err)] + fn start(self) -> Result<(TaskFuture<'static, (), Error>, UdpHandle, FnSpawnServiceHeathCheck), Error> { + let handle = UdpHandle::default(); + + let std_socket = std::net::UdpSocket::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let socket = UdpSocket::from_std(std_socket).map_err(|e| Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let task = Udp::run_udp_server(self.tracker, socket, handle.udp_handle.clone()) + .map_err(|err| Error::UnableToStartUdpService { err })? + .map_err(|err| Error::UnableToStartUdpService { err }) + .boxed(); + + Ok((task, handle, check_fn)) } } @@ -227,196 +181,170 @@ impl Drop for ActiveRequests { /// A UDP server instance launcher. #[derive(Constructor)] -pub struct Udp; +pub(super) struct Udp {} impl Udp { - /// It starts the UDP server instance with graceful shutdown. - /// - /// # Panics - /// - /// It panics if unable to bind to udp socket, and get the address from the udp socket. - /// It also panics if unable to send address of socket. - async fn run_with_graceful_shutdown( + #[instrument(err)] + pub fn run_udp_server( tracker: Arc, - bind_to: SocketAddr, - tx_start: oneshot::Sender, - rx_halt: oneshot::Receiver, - ) { - let socket = Arc::new( - UdpSocket::bind(bind_to) - .await - .unwrap_or_else(|_| panic!("Could not bind to {bind_to}.")), - ); - let address = socket - .local_addr() - .unwrap_or_else(|_| panic!("Could not get local_addr from {bind_to}.")); - let halt = shutdown_signal_with_message(rx_halt, format!("Halting Http Service Bound to Socket: {address}")); - - info!(target: "UDP TRACKER", "Starting on: udp://{}", address); + socket: UdpSocket, + handle: super::Handle, + ) -> Result>, UdpError> { + let socket = Arc::new(socket); - let running = tokio::task::spawn(async move { - debug!(target: "UDP TRACKER", "Started: Waiting for packets on socket address: udp://{address} ..."); - Self::run_udp_server(tracker, socket).await; - }); - - tx_start - .send(Started { address }) - .expect("the UDP Tracker service should not be dropped"); + let addr = socket + .local_addr() + .map_err(|e| UdpError::UnableToGetLocalAddress { err: e.into() })?; + + let () = handle.notify_listening(Some(addr)); + + Ok(async move { + let reqs = &mut ActiveRequests::default(); + + // Main Waiting Loop, awaits on async [`receive_request`]. + loop { + let Ok(request) = Self::receive_request(socket.clone(), handle.clone()).await else { + break; + }; + + if let Some(h) = reqs + .rb + .push_overwrite(Self::spawn_request_processor(request, tracker.clone(), socket.clone()).abort_handle()) + { + if !h.is_finished() { + // the task is still running, lets yield and give it a chance to flush. + let () = tokio::task::yield_now().await; + h.abort(); + } + } + } - debug!(target: "UDP TRACKER", "Started on: udp://{}", address); + let () = tokio::task::yield_now().await; - let stop = running.abort_handle(); + let () = handle.wait_connections_end().await; - select! { - _ = running => { debug!(target: "UDP TRACKER", "Socket listener stopped on address: udp://{address}"); }, - () = halt => { debug!(target: "UDP TRACKER", "Halt signal spawned task stopped on address: udp://{address}"); } + Ok(()) } - stop.abort(); - - task::yield_now().await; // lets allow the other threads to complete. + .boxed()) } - async fn run_udp_server(tracker: Arc, socket: Arc) { - let tracker = tracker.clone(); - let socket = socket.clone(); - - let reqs = &mut ActiveRequests::default(); - - // Main Waiting Loop, awaits on async [`receive_request`]. - loop { - if let Some(h) = reqs.rb.push_overwrite( - Self::spawn_request_processor(Self::receive_request(socket.clone()).await, tracker.clone(), socket.clone()) - .abort_handle(), - ) { - if !h.is_finished() { - // the task is still running, lets yield and give it a chance to flush. - tokio::task::yield_now().await; - h.abort(); - } - } - } - } + #[instrument(err, ret)] + async fn receive_request(socket: Arc, handle: super::Handle) -> Result { + let addr = handle + .listening() + .await + .ok_or_else(|| UdpError::UnableToGetListeningAddress {})?; - async fn receive_request(socket: Arc) -> Result> { - // Wait for the socket to be readable - socket.readable().await?; + let () = select! { + () = handle.wait_graceful_shutdown() => Err(UdpError::GracefullyCanceled { addr }), + r = socket.readable() => r.map_err(|e| UdpError::UnableToGetReadableSocket { addr ,err: e.into() }), + }?; let mut buf = Vec::with_capacity(MAX_PACKET_SIZE); - match socket.recv_buf_from(&mut buf).await { - Ok((n, from)) => { - Vec::truncate(&mut buf, n); - trace!("GOT {buf:?}"); - Ok(UdpRequest { payload: buf, from }) - } - - Err(e) => Err(Box::new(e)), - } + let (n, from) = socket + .recv_buf_from(&mut buf) + .await + .map_err(|e| UdpError::UnableToReadFromSocket { addr, err: e.into() })?; + + Vec::truncate(&mut buf, n); + trace!("GOT {buf:?}"); + Ok(UdpRequest { + payload: buf, + from, + watcher: handle.watcher(), + }) } - fn spawn_request_processor( - result: Result>, - tracker: Arc, - socket: Arc, - ) -> JoinHandle<()> { - tokio::task::spawn(Self::process_request(result, tracker, socket)) + #[instrument(level = Level::TRACE,)] + fn spawn_request_processor(request: UdpRequest, tracker: Arc, socket: Arc) -> JoinHandle<()> { + tokio::task::spawn(Self::process_request(request, tracker, socket)) } - async fn process_request(result: Result>, tracker: Arc, socket: Arc) { - match result { - Ok(udp_request) => { - trace!("Received Request from: {}", udp_request.from); - Self::process_valid_request(tracker.clone(), socket.clone(), udp_request).await; - } - Err(error) => { - debug!("error: {error}"); - } - } + #[instrument(level = Level::TRACE,)] + async fn process_request<'a>(request: UdpRequest, tracker: Arc, socket: Arc) { + select! { + _ = Self::process_valid_request(tracker.clone(), socket.clone(), &request) => (), + () = request.watcher.wait_shutdown() => () + }; } - async fn process_valid_request(tracker: Arc, socket: Arc, udp_request: UdpRequest) { - trace!("Making Response to {udp_request:?}"); - let from = udp_request.from; - let response = handlers::handle_packet(udp_request, &tracker.clone(), socket.clone()).await; - Self::send_response(&socket.clone(), from, response).await; + #[instrument(err, ret)] + async fn process_valid_request(tracker: Arc, socket: Arc, request: &UdpRequest) -> Result<(), UdpError> { + trace!("Making Response to {request:?}"); + let target = request.from; + let response = handlers::handle_packet(request, &tracker.clone(), socket.clone()).await; + let _ = Self::send_response(&socket.clone(), target, response, &request.watcher).await?; + + Ok(()) } - async fn send_response(socket: &Arc, to: SocketAddr, response: Response) { - trace!("Sending Response: {response:?} to: {to:?}"); + #[instrument(err, ret)] + async fn send_response( + socket: &Arc, + target: SocketAddr, + response: aquatic_udp_protocol::Response, + watcher: &Watcher, + ) -> Result { + trace!("Sending Response: {response:?} to: {target:?}"); let buffer = vec![0u8; MAX_PACKET_SIZE]; - let mut cursor = Cursor::new(buffer); + let mut cursor = std::io::Cursor::new(buffer); - match response.write(&mut cursor) { - Ok(()) => { - #[allow(clippy::cast_possible_truncation)] - let position = cursor.position() as usize; - let inner = cursor.get_ref(); + let () = response + .write(&mut cursor) + .map_err(|e| UdpError::UnableToWriteToSocket { target, err: e.into() })?; - debug!("Sending {} bytes ...", &inner[..position].len()); - debug!("To: {:?}", &to); - debug!("Payload: {:?}", &inner[..position]); + #[allow(clippy::cast_possible_truncation)] + let position = cursor.position() as usize; + let inner = cursor.get_ref(); - Self::send_packet(socket, &to, &inner[..position]).await; + debug!("Sending {} bytes ...", &inner[..position].len()); + debug!("To: {:?}", &target); + debug!("Payload: {:?}", &inner[..position]); - debug!("{} bytes sent", &inner[..position].len()); - } - Err(_) => { - error!("could not write response to bytes."); - } - } - } + let result = Self::send_packet(socket, &target, &inner[..position], watcher).await; - async fn send_packet(socket: &Arc, remote_addr: &SocketAddr, payload: &[u8]) { - trace!("Sending Packets: {payload:?} to: {remote_addr:?}"); + debug!("{} bytes sent", &inner[..position].len()); - // doesn't matter if it reaches or not - drop(socket.send_to(payload, remote_addr).await); + result } - fn check(&addr: &SocketAddr) -> ServiceHealthCheckJob { - let info = format!("checking the udp tracker health check at: {addr}"); + #[instrument(err, ret)] + async fn send_packet( + socket: &Arc, + &target: &SocketAddr, + payload: &[u8], + watcher: &Watcher, + ) -> Result { + trace!("Sending Packets: {payload:?} to: {target:?}"); - let job = tokio::spawn(Client::connect(addr).and_then(Client::check).map_err(|e| e.to_string())); - - ServiceHealthCheckJob::new(addr, info, job) + select! { + r = socket.send_to(payload, target) => r.map_err(|e| UdpError::UnableToSendToSocket{ target, err: e.into()}), + () = watcher.wait_shutdown() => Err(UdpError::CanceledSending{target}) + } } -} -#[cfg(test)] -mod tests { - use std::sync::Arc; - use std::time::Duration; + #[instrument(ret)] + pub fn graceful_udp_shutdown(handle: super::Handle, rx_shutdown: tokio::sync::oneshot::Receiver, message: String) { + tokio::task::spawn(async move { + match handle.listening().await { + Some(addr) => { + shutdown_signal_with_message(rx_shutdown, format!("{message}, on socket address: {addr}")).await; - use tokio::time::sleep; - use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + info!("Sending graceful shutdown signal"); + handle.graceful_shutdown(Some(Duration::from_secs(90))); - use crate::bootstrap::app::initialize_with_configuration; - use crate::servers::registar::Registar; - use crate::servers::udp::server::{Launcher, UdpServer}; + println!("!! shuting down in 90 seconds !!"); - #[tokio::test] - async fn it_should_be_able_to_start_and_stop() { - let cfg = Arc::new(ephemeral_mode_public()); - let tracker = initialize_with_configuration(&cfg); - let config = &cfg.udp_trackers[0]; + loop { + tokio::time::sleep(Duration::from_secs(1)).await; - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let register = &Registar::default(); - - let stopped = UdpServer::new(Launcher::new(bind_to)); - let started = stopped - .start(tracker, register.give_form()) - .await - .expect("it should start the server"); - let stopped = started.stop().await.expect("it should stop the server"); - - sleep(Duration::from_secs(1)).await; - - assert_eq!(stopped.state.launcher.bind_to, bind_to); + info!("remaining alive connections: {}", handle.connection_count()); + } + } + None => handle.shutdown(), + } + }); } } diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index ac79a59d2..574fea0d6 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -5,10 +5,10 @@ use std::time::Duration; use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Request, Response, TransactionId}; use derive_more::{AsRef, Constructor, From, Into}; -use log::debug; use tokio::net::UdpSocket; use tokio::time; use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE, PORT_ASSIGNED_BY_OS}; +use tracing::debug; use super::{source_address, Error}; @@ -70,6 +70,7 @@ impl Bound { /// # Errors /// /// This function errors if underlying function fails. + #[allow(dead_code)] pub fn local_addr(&self) -> Result { self.sock .local_addr() @@ -96,8 +97,11 @@ impl Client { /// # Errors /// /// This function returns and error if the the binding fails. - pub async fn connect(addr: SocketAddr) -> Result { - let launcher = Launcher::default(); + pub async fn connect(addr: SocketAddr, timeout: Duration) -> Result { + let launcher = Launcher { + timeout, + ..Default::default() + }; let bound = launcher.bind().await?; bound.connect(addr).await @@ -261,7 +265,7 @@ impl Client { /// # Panics pub async fn check(self) -> Result { let connect_request = ConnectRequest { - transaction_id: TransactionId(123), + transaction_id: TransactionId(rand::Rng::gen(&mut rand::thread_rng())), }; let _ = self.send_request(connect_request.clone().into()).await?; diff --git a/src/shared/bit_torrent/tracker/udp/mod.rs b/src/shared/bit_torrent/tracker/udp/mod.rs index 41ebeafc4..e384b4524 100644 --- a/src/shared/bit_torrent/tracker/udp/mod.rs +++ b/src/shared/bit_torrent/tracker/udp/mod.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use thiserror::Error; -pub mod client; +mod client; + +pub use client::Client; #[derive(Debug, Clone, Error)] pub enum Error { diff --git a/tests/clients/mod.rs b/tests/clients/mod.rs new file mode 100644 index 000000000..952bc833f --- /dev/null +++ b/tests/clients/mod.rs @@ -0,0 +1 @@ +mod udp; diff --git a/tests/clients/udp/mod.rs b/tests/clients/udp/mod.rs new file mode 100644 index 000000000..2f6786377 --- /dev/null +++ b/tests/clients/udp/mod.rs @@ -0,0 +1,40 @@ +// use futures::future::BoxFuture; +// use futures::FutureExt; +// use rstest::{fixture, rstest}; +// use torrust_tracker::shared::bit_torrent::tracker; +// use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; +// use torrust_tracker_test_helpers::configuration; + +// use crate::common::udp::Started; + +// type Server<'b, 'a> = BoxFuture<'a, Started<'b>>; +// type ClientFuture<'a> = BoxFuture<'a, tracker::udp::Client>; +// type Client<'a> = BoxFuture<'a, dyn (Fn(&Started<'a>) -> ClientFuture<'a>)>; + +// #[fixture] +// fn server<'a>() -> Server<'a> { +// let a = Started::new(&configuration::ephemeral().into()).boxed(); + +// a +// } + +// #[fixture] +// fn client() -> Client<'static> { +// |server| async move { +// tracker::udp::Client::connect(server.bind_address(), CLIENT_TIMEOUT_DEFAULT) +// .await +// .unwrap() +// } +// } + +// #[rstest] +// #[tokio::test] +// async fn it_should_send_and_receive(server: Server<'_>, client: Client<'_>) { +// //let server = + +// let client = client(&server.await).await; + +// client.check().await.expect("it should run check successfully"); + +// //drop(server.tracker.get_stats().await); +// } diff --git a/tests/common/udp.rs b/tests/common/udp.rs deleted file mode 100644 index 3d84e2b97..000000000 --- a/tests/common/udp.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; - -use tokio::net::UdpSocket; - -/// A generic UDP client -pub struct Client { - pub socket: Arc, -} - -impl Client { - #[allow(dead_code)] - pub async fn connected(remote_socket_addr: &SocketAddr, local_socket_addr: &SocketAddr) -> Client { - let client = Client::bind(local_socket_addr).await; - client.connect(remote_socket_addr).await; - client - } - - pub async fn bind(local_socket_addr: &SocketAddr) -> Self { - let socket = UdpSocket::bind(local_socket_addr).await.unwrap(); - Self { - socket: Arc::new(socket), - } - } - - pub async fn connect(&self, remote_address: &SocketAddr) { - self.socket.connect(remote_address).await.unwrap(); - } - - #[allow(dead_code)] - pub async fn send(&self, bytes: &[u8]) -> usize { - self.socket.writable().await.unwrap(); - self.socket.send(bytes).await.unwrap() - } - - #[allow(dead_code)] - pub async fn receive(&self, bytes: &mut [u8]) -> usize { - self.socket.readable().await.unwrap(); - self.socket.recv(bytes).await.unwrap() - } -} diff --git a/tests/servers/udp/environment.rs b/tests/common/udp/environment.rs similarity index 53% rename from tests/servers/udp/environment.rs rename to tests/common/udp/environment.rs index 12f4aeb9e..50c11d543 100644 --- a/tests/servers/udp/environment.rs +++ b/tests/common/udp/environment.rs @@ -1,25 +1,28 @@ +use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; -use torrust_tracker::bootstrap::app::initialize_with_configuration; +use torrust_tracker::bootstrap::app::tracker; use torrust_tracker::core::Tracker; use torrust_tracker::servers::registar::Registar; -use torrust_tracker::servers::udp::server::{Launcher, Running, Stopped, UdpServer}; +use torrust_tracker::servers::service::{Service, Started, Stopped}; +use torrust_tracker::servers::udp::server::{UdpHandle, UdpLauncher}; use torrust_tracker_configuration::{Configuration, UdpTracker}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; -pub struct Environment { +pub struct Environment { pub config: Arc, pub tracker: Arc, pub registar: Registar, - pub server: UdpServer, + pub server: Service, + pub addr: Option, } -impl Environment { +impl Environment { /// Add a torrent to the tracker #[allow(dead_code)] - pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &peer::Peer) { + pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; } } @@ -27,70 +30,64 @@ impl Environment { impl Environment { #[allow(dead_code)] pub fn new(configuration: &Arc) -> Self { - let tracker = initialize_with_configuration(configuration); + let tracker = tracker(configuration); let config = Arc::new(configuration.udp_trackers[0].clone()); - let bind_to = config + let addr = config .bind_address .parse::() .expect("Tracker API bind_address invalid."); - let server = UdpServer::new(Launcher::new(bind_to)); + let server = Service::new(UdpLauncher::new(tracker.clone(), addr)); Self { config, tracker, - registar: Registar::default(), server, + registar: Registar::default(), + addr: None, } } #[allow(dead_code)] - pub async fn start(self) -> Environment { + pub async fn start(self) -> Environment> { + let server = self.server.start().unwrap(); + + // reg_form wait for the service to be ready before proceeding + let () = server + .reg_form(self.registar.give_form()) + .await + .expect("it should register a form"); + + let addr = server.listening().await.expect("it should get address"); + Environment { config: self.config, tracker: self.tracker.clone(), registar: self.registar.clone(), - server: self.server.start(self.tracker, self.registar.give_form()).await.unwrap(), + server, + addr: Some(addr), } } } -impl Environment { +impl Environment> { pub async fn new(configuration: &Arc) -> Self { Environment::::new(configuration).start().await } - #[allow(dead_code)] pub async fn stop(self) -> Environment { Environment { config: self.config, tracker: self.tracker, registar: Registar::default(), - server: self.server.stop().await.expect("it stop the udp tracker service"), + server: self.server.stop().await.unwrap(), + addr: None, } } - pub fn bind_address(&self) -> SocketAddr { - self.server.state.binding - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use tokio::time::sleep; - use torrust_tracker_test_helpers::configuration; - - use crate::servers::udp::Started; - - #[tokio::test] - async fn it_should_make_and_stop_udp_server() { - let env = Started::new(&configuration::ephemeral().into()).await; - sleep(Duration::from_secs(1)).await; - env.stop().await; - sleep(Duration::from_secs(1)).await; + pub fn bind_address(&self) -> std::net::SocketAddr { + self.addr.expect("it should get the listening address") } } diff --git a/tests/common/udp/mod.rs b/tests/common/udp/mod.rs new file mode 100644 index 000000000..2ff0347bc --- /dev/null +++ b/tests/common/udp/mod.rs @@ -0,0 +1,25 @@ +use torrust_tracker::servers::service; +use torrust_tracker::servers::udp::server::UdpHandle; + +pub mod environment; + +pub type Started<'a> = environment::Environment>; +//pub type Stopped<'a> = environment::Environment; + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use tokio::time::sleep; + use torrust_tracker_test_helpers::configuration; + + use crate::common::udp::Started; + + #[tokio::test] + async fn it_should_make_and_stop_udp_server() { + let env = Started::new(&configuration::ephemeral().into()).await; + sleep(Duration::from_secs(1)).await; + env.stop().await; + sleep(Duration::from_secs(1)).await; + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 8e3d46826..b4eb4804a 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -5,6 +5,7 @@ //! ``` use torrust_tracker_clock::clock; +mod clients; mod common; mod servers; diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 8d91f3ae8..147a65e44 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -1,26 +1,29 @@ +use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; use futures::executor::block_on; -use torrust_tracker::bootstrap::app::initialize_with_configuration; +use torrust_tracker::bootstrap::app::tracker; use torrust_tracker::bootstrap::jobs::make_rust_tls; use torrust_tracker::core::Tracker; -use torrust_tracker::servers::apis::server::{ApiServer, Launcher, Running, Stopped}; +use torrust_tracker::servers::apis::server::{ApiHandle, ApiLauncher}; use torrust_tracker::servers::registar::Registar; +use torrust_tracker::servers::service::{Service, Started, Stopped}; use torrust_tracker_configuration::{Configuration, HttpApi}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; use super::connection_info::ConnectionInfo; -pub struct Environment { +pub struct Environment { pub config: Arc, pub tracker: Arc, pub registar: Registar, - pub server: ApiServer, + pub server: Service, + pub addr: Option, } -impl Environment { +impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; @@ -29,11 +32,12 @@ impl Environment { impl Environment { pub fn new(configuration: &Arc) -> Self { - let tracker = initialize_with_configuration(configuration); + let tracker = tracker(configuration); let config = Arc::new(configuration.http_api.clone()); + let access_tokens = Arc::new(config.access_tokens.clone()); - let bind_to = config + let addr = config .bind_address .parse::() .expect("Tracker API bind_address invalid."); @@ -41,33 +45,39 @@ impl Environment { let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) .map(|tls| tls.expect("tls config failed")); - let server = ApiServer::new(Launcher::new(bind_to, tls)); + let stopped = Service::new(ApiLauncher::new(tracker.clone(), access_tokens, addr, tls)); Self { config, tracker, registar: Registar::default(), - server, + server: stopped, + addr: None, } } - pub async fn start(self) -> Environment { - let access_tokens = Arc::new(self.config.access_tokens.clone()); + pub async fn start(self) -> Environment> { + let server = self.server.start().unwrap(); + + // reg_form wait for the service to be ready before proceeding + let () = server + .reg_form(self.registar.give_form()) + .await + .expect("it should register a form"); + + let addr = server.listening().await.expect("it should get address"); Environment { config: self.config, tracker: self.tracker.clone(), registar: self.registar.clone(), - server: self - .server - .start(self.tracker, self.registar.give_form(), access_tokens) - .await - .unwrap(), + server, + addr: Some(addr), } } } -impl Environment { +impl Environment> { pub async fn new(configuration: &Arc) -> Self { Environment::::new(configuration).start().await } @@ -78,17 +88,18 @@ impl Environment { tracker: self.tracker, registar: Registar::default(), server: self.server.stop().await.unwrap(), + addr: None, } } + pub fn bind_address(&self) -> std::net::SocketAddr { + self.addr.expect("it should get the listening address") + } + pub fn get_connection_info(&self) -> ConnectionInfo { ConnectionInfo { - bind_address: self.server.state.binding.to_string(), - api_token: self.config.access_tokens.get("admin").cloned(), + bind_address: self.bind_address().to_string(), + api_token: self.config.access_tokens.as_ref().get("admin").cloned(), } } - - pub fn bind_address(&self) -> SocketAddr { - self.server.state.binding - } } diff --git a/tests/servers/api/mod.rs b/tests/servers/api/mod.rs index 9c30e316a..b35eb01a4 100644 --- a/tests/servers/api/mod.rs +++ b/tests/servers/api/mod.rs @@ -1,13 +1,15 @@ use std::sync::Arc; use torrust_tracker::core::Tracker; -use torrust_tracker::servers::apis::server; +use torrust_tracker::servers::apis::server::ApiHandle; +use torrust_tracker::servers::service; pub mod connection_info; pub mod environment; pub mod v1; -pub type Started = environment::Environment; +pub type Started = environment::Environment>; +//pub type Stopped<'a> = environment::Environment; /// It forces a database error by dropping all tables. /// That makes any query fail. diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs index a9ed7f31b..19844b5da 100644 --- a/tests/servers/health_check_api/contract.rs +++ b/tests/servers/health_check_api/contract.rs @@ -8,10 +8,12 @@ use crate::servers::health_check_api::Started; #[tokio::test] async fn health_check_endpoint_should_return_status_ok_when_there_is_no_services_registered() { let configuration = configuration::ephemeral_with_no_services(); + let registar = Registar::default(); - let env = Started::new(&configuration.health_check_api.into(), Registar::default()).await; + let config = configuration.health_check_api.clone(); + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -46,9 +48,9 @@ mod api { { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -95,9 +97,9 @@ mod api { { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -151,9 +153,9 @@ mod http { { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -168,7 +170,7 @@ mod http { let details = report.details.first().expect("it should have some details"); - assert_eq!(details.binding, *service.bind_address()); + assert_eq!(details.binding, service.bind_address()); assert_eq!(details.result, Ok("200 OK".to_string())); assert_eq!( @@ -191,7 +193,7 @@ mod http { let service = http::Started::new(&configuration).await; - let binding = *service.bind_address(); + let binding = service.bind_address(); let registar = service.registar.clone(); @@ -199,9 +201,9 @@ mod http { { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -241,23 +243,23 @@ mod udp { use torrust_tracker::servers::health_check_api::resources::{Report, Status}; use torrust_tracker_test_helpers::configuration; + use crate::common::udp; use crate::servers::health_check_api::client::get; use crate::servers::health_check_api::Started; - use crate::servers::udp; #[tokio::test] pub(crate) async fn it_should_return_good_health_for_udp_service() { let configuration = Arc::new(configuration::ephemeral()); - let service = udp::Started::new(&configuration).await; + let service = udp::Started::new(&configuration.clone()).await; let registar = service.registar.clone(); { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); @@ -295,7 +297,7 @@ mod udp { pub(crate) async fn it_should_return_error_when_udp_service_was_stopped_after_registration() { let configuration = Arc::new(configuration::ephemeral()); - let service = udp::Started::new(&configuration).await; + let service = udp::Started::new(&configuration.clone()).await; let binding = service.bind_address(); @@ -305,9 +307,9 @@ mod udp { { let config = configuration.health_check_api.clone(); - let env = Started::new(&config.into(), registar).await; + let env = Started::new(&config.into(), ®istar).await; - let response = get(&format!("http://{}/health_check", env.state.binding)).await; + let response = get(&format!("http://{}/health_check", env.addr)).await; assert_eq!(response.status(), 200); assert_eq!(response.headers().get("content-type").unwrap(), "application/json"); diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs index 0856985d5..02c13e48a 100644 --- a/tests/servers/health_check_api/environment.rs +++ b/tests/servers/health_check_api/environment.rs @@ -1,102 +1,69 @@ use std::net::SocketAddr; use std::sync::Arc; -use log::debug; -use tokio::sync::oneshot::{self, Sender}; -use tokio::task::JoinHandle; -use torrust_tracker::bootstrap::jobs::Started; -use torrust_tracker::servers::health_check_api::server; -use torrust_tracker::servers::registar::Registar; -use torrust_tracker::servers::signals::{self, Halted}; +use torrust_tracker::servers::health_check_api::server::{HealthCheckHandle, HealthCheckLauncher}; +use torrust_tracker::servers::registar::{Registar, ServiceRegistrationForm, ServiceRegistry}; +use torrust_tracker::servers::service; use torrust_tracker_configuration::HealthCheckApi; -#[derive(Debug)] -pub enum Error { - #[allow(dead_code)] - Error(String), -} - -pub struct Running { - pub binding: SocketAddr, - pub halt_task: Sender, - pub task: JoinHandle, -} +type Started = service::Service, HealthCheckLauncher, HealthCheckHandle>; -pub struct Stopped { - pub bind_to: SocketAddr, +pub struct Environment { + service: Started, + pub addr: SocketAddr, } -pub struct Environment { - pub registar: Registar, - pub state: S, -} +impl Environment { + /// Starts the health check environment. + /// + /// # Panics + /// + /// Panics if something goes wrong... + pub async fn new(config: &Arc, registar: &Registar) -> Self { + let registry = registar.clone().get_registry(); -impl Environment { - pub fn new(config: &Arc, registar: Registar) -> Self { - let bind_to = config + let addr = config .bind_address .parse::() - .expect("Tracker API bind_address invalid."); + .expect("it should have a valid http tracker bind address"); + + let form = registar.give_form(); + + let (started, listening) = Self::start_v0(addr, ®istry, form).await; Self { - registar, - state: Stopped { bind_to }, + service: started, + addr: listening, } } - /// Start the test environment for the Health Check API. - /// It runs the API server. - pub async fn start(self) -> Environment { - let (tx_start, rx_start) = oneshot::channel::(); - let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); - - let register = self.registar.entries(); - - debug!(target: "HEALTH CHECK API", "Spawning task to launch the service ..."); + /// Starts the first (un-versioned) tracker service health check. + /// + /// # Panics + /// + /// Panics if something goes wrong... + async fn start_v0(addr: SocketAddr, registry: &ServiceRegistry, form: ServiceRegistrationForm) -> (Started, SocketAddr) { + let service = service::Service::new(HealthCheckLauncher::new(addr, registry.clone())); - let server = tokio::spawn(async move { - debug!(target: "HEALTH CHECK API", "Starting the server in a spawned task ..."); + let started: Started = service.start().expect("it should start"); - server::start(self.state.bind_to, tx_start, rx_halt, register) - .await - .expect("it should start the health check service"); + let () = started.reg_form(form).await.expect("it should register"); - debug!(target: "HEALTH CHECK API", "Server started. Sending the binding {} ...", self.state.bind_to); - - self.state.bind_to - }); - - debug!(target: "HEALTH CHECK API", "Waiting for spawning task to send the binding ..."); - - let binding = rx_start.await.expect("it should send service binding").address; - - Environment { - registar: self.registar.clone(), - state: Running { - task: server, - halt_task: tx_halt, - binding, - }, - } - } -} + let listening = started.listening().await.expect("it should start listening"); -impl Environment { - pub async fn new(config: &Arc, registar: Registar) -> Self { - Environment::::new(config, registar).start().await + (started, listening) } - pub async fn stop(self) -> Result, Error> { - self.state - .halt_task - .send(Halted::Normal) - .map_err(|e| Error::Error(e.to_string()))?; + /// Starts the health check environment. + /// + /// # Panics + /// + /// Panics if something goes wrong... + pub async fn stop(self) -> Result<(), service::Error> { + let stopped = self.service.stop().await?; - let bind_to = self.state.task.await.expect("it should shutdown the service"); + drop(stopped); - Ok(Environment { - registar: self.registar.clone(), - state: Stopped { bind_to }, - }) + Ok(()) } } diff --git a/tests/servers/health_check_api/mod.rs b/tests/servers/health_check_api/mod.rs index 9e15c5f62..e31551470 100644 --- a/tests/servers/health_check_api/mod.rs +++ b/tests/servers/health_check_api/mod.rs @@ -2,4 +2,4 @@ pub mod client; pub mod contract; pub mod environment; -pub type Started = environment::Environment; +pub type Started = environment::Environment; diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 5638713aa..71e2bc248 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -1,23 +1,27 @@ +use std::fmt::Debug; +use std::net::SocketAddr; use std::sync::Arc; use futures::executor::block_on; -use torrust_tracker::bootstrap::app::initialize_with_configuration; +use torrust_tracker::bootstrap::app::tracker; use torrust_tracker::bootstrap::jobs::make_rust_tls; use torrust_tracker::core::Tracker; -use torrust_tracker::servers::http::server::{HttpServer, Launcher, Running, Stopped}; +use torrust_tracker::servers::http::service::{HttpHandle, HttpLauncher}; use torrust_tracker::servers::registar::Registar; +use torrust_tracker::servers::service::{Service, Started, Stopped}; use torrust_tracker_configuration::{Configuration, HttpTracker}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; -pub struct Environment { +pub struct Environment { pub config: Arc, pub tracker: Arc, pub registar: Registar, - pub server: HttpServer, + pub server: Service, + pub addr: Option, } -impl Environment { +impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; @@ -27,7 +31,7 @@ impl Environment { impl Environment { #[allow(dead_code)] pub fn new(configuration: &Arc) -> Self { - let tracker = initialize_with_configuration(configuration); + let tracker = tracker(configuration); let config = Arc::new(configuration.http_trackers[0].clone()); @@ -39,28 +43,40 @@ impl Environment { let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) .map(|tls| tls.expect("tls config failed")); - let server = HttpServer::new(Launcher::new(bind_to, tls)); + let server = Service::new(HttpLauncher::new(tracker.clone(), bind_to, tls)); Self { config, tracker, - registar: Registar::default(), server, + registar: Registar::default(), + addr: None, } } #[allow(dead_code)] - pub async fn start(self) -> Environment { + pub async fn start(self) -> Environment> { + let server = self.server.start().unwrap(); + + // reg_form wait for the service to be ready before proceeding + let () = server + .reg_form(self.registar.give_form()) + .await + .expect("it should register a form"); + + let addr = server.listening().await.expect("it should get address"); + Environment { config: self.config, tracker: self.tracker.clone(), registar: self.registar.clone(), - server: self.server.start(self.tracker, self.registar.give_form()).await.unwrap(), + server, + addr: Some(addr), } } } -impl Environment { +impl Environment> { pub async fn new(configuration: &Arc) -> Self { Environment::::new(configuration).start().await } @@ -70,12 +86,12 @@ impl Environment { config: self.config, tracker: self.tracker, registar: Registar::default(), - server: self.server.stop().await.unwrap(), + addr: None, } } - pub fn bind_address(&self) -> &std::net::SocketAddr { - &self.server.state.binding + pub fn bind_address(&self) -> std::net::SocketAddr { + self.addr.expect("it should get the listening address") } } diff --git a/tests/servers/http/mod.rs b/tests/servers/http/mod.rs index 5a40cfcdb..26f26da20 100644 --- a/tests/servers/http/mod.rs +++ b/tests/servers/http/mod.rs @@ -1,11 +1,10 @@ +use torrust_tracker::servers::http::service::HttpHandle; +use torrust_tracker::servers::service; + pub mod asserts; pub mod environment; pub mod v1; -use std::time::Duration; - -use torrust_tracker::servers::http::server; - -pub type Started = environment::Environment; +pub type Started<'a> = environment::Environment>; -pub(crate) const TIMEOUT: Duration = Duration::from_secs(5); +//pub type Stopped<'a> = environment::Environment; diff --git a/tests/servers/http/v1/mod.rs b/tests/servers/http/v1/mod.rs index f70f72f5d..fc7047339 100644 --- a/tests/servers/http/v1/mod.rs +++ b/tests/servers/http/v1/mod.rs @@ -2,51 +2,50 @@ use std::net::IpAddr; use reqwest::Response; use torrust_tracker::core::auth::Key; -use torrust_tracker::servers::http::server::Running; use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, Client}; +use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; -use super::environment::Environment; -use super::TIMEOUT; +use super::Started; pub mod contract; pub(crate) const PORT: u16 = 17548; -pub(crate) fn create_default_client(env: &Environment) -> Client { - let url: url::Url = format!("http://{}/", &env.bind_address()) +pub(crate) fn create_default_client(env: &Started<'static>) -> Client { + let url: url::Url = format!("http://{}/", env.bind_address()) .parse() .expect("it should make a valid url"); - Client::new(url, TIMEOUT).expect("it should make a client") + Client::new(url, CLIENT_TIMEOUT_DEFAULT).expect("it should make a client") } -pub(crate) fn create_bonded_client(env: &Environment, local_address: IpAddr) -> Client { - let url: url::Url = format!("http://{}/", &env.bind_address()) +pub(crate) fn create_bonded_client(env: &Started<'static>, local_address: IpAddr) -> Client { + let url: url::Url = format!("http://{}/", env.bind_address()) .parse() .expect("it should make a valid url"); - Client::bind(url, TIMEOUT, local_address).expect("it should make a client") + Client::bind(url, CLIENT_TIMEOUT_DEFAULT, local_address).expect("it should make a client") } -pub(crate) fn create_authenticated_client(env: &Environment, key: Key) -> Client { - let url: url::Url = format!("http://{}/", &env.bind_address()) +pub(crate) fn create_authenticated_client(env: &Started<'static>, key: Key) -> Client { + let url: url::Url = format!("http://{}/", env.bind_address()) .parse() .expect("it should make a valid url"); - Client::authenticated(url, TIMEOUT, key).expect("it should make a client") + Client::authenticated(url, CLIENT_TIMEOUT_DEFAULT, key).expect("it should make a client") } -pub(crate) async fn create_client_response(env: &Environment, path: &str) -> Response { +pub(crate) async fn create_client_response(env: &Started<'static>, path: &str) -> Response { create_default_client(env).get(path).await.expect("it should get a response") } -pub(crate) async fn create_client_announce_response(env: &Environment, query: &requests::Announce) -> Response { +pub(crate) async fn create_client_announce_response(env: &Started<'static>, query: &requests::Announce) -> Response { create_default_client(env) .announce(query) .await .expect("it should get a response") } -pub(crate) async fn create_client_scrape_response(env: &Environment, query: &requests::Scrape) -> Response { +pub(crate) async fn create_client_scrape_response(env: &Started<'static>, query: &requests::Scrape) -> Response { create_default_client(env) .scrape(query) .await @@ -54,7 +53,7 @@ pub(crate) async fn create_client_scrape_response(env: &Environment, qu } pub(crate) async fn create_bonded_client_announce_response( - env: &Environment, + env: &Started<'static>, local_address: IpAddr, query: &requests::Announce, ) -> Response { @@ -65,7 +64,7 @@ pub(crate) async fn create_bonded_client_announce_response( } pub(crate) async fn create_bonded_client_scrape_response( - env: &Environment, + env: &Started<'static>, local_address: IpAddr, query: &requests::Scrape, ) -> Response { diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index c706cf00f..c9e1827bf 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -4,12 +4,12 @@ // https://www.bittorrent.org/beps/bep_0015.html use aquatic_udp_protocol::Response; -use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; -use torrust_tracker_configuration::MAX_PACKET_SIZE; +use torrust_tracker::shared::bit_torrent::tracker::udp::Client; +use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE}; use torrust_tracker_test_helpers::configuration; +use crate::common::udp::Started; use crate::servers::udp::asserts::is_error_response; -use crate::servers::udp::Started; fn empty_udp_request() -> [u8; MAX_PACKET_SIZE] { [0; MAX_PACKET_SIZE] @@ -23,7 +23,9 @@ fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = Client::connect(env.bind_address()).await.expect("it should connect"); + let client = Client::connect(env.bind_address(), CLIENT_TIMEOUT_DEFAULT) + .await + .expect("it should connect"); client.send(&empty_udp_request()).await.expect("it should send request"); @@ -39,17 +41,20 @@ async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_req mod receiving_a_connection_request { use aquatic_udp_protocol::{ConnectRequest, TransactionId}; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; + use torrust_tracker::shared::bit_torrent::tracker::udp::Client; + use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; use torrust_tracker_test_helpers::configuration; + use crate::common::udp::Started; use crate::servers::udp::asserts::is_connect_response; - use crate::servers::udp::Started; #[tokio::test] async fn should_return_a_connect_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = Client::connect(env.bind_address()).await.expect("it should connect"); + let client = Client::connect(env.bind_address(), CLIENT_TIMEOUT_DEFAULT) + .await + .expect("it should connect"); let connect_request = ConnectRequest { transaction_id: TransactionId(123), @@ -75,17 +80,20 @@ mod receiving_an_announce_request { AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, TransactionId, }; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; + use torrust_tracker::shared::bit_torrent::tracker::udp::Client; + use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; use torrust_tracker_test_helpers::configuration; + use crate::common::udp::Started; use crate::servers::udp::asserts::is_ipv4_announce_response; - use crate::servers::udp::Started; #[tokio::test] async fn should_return_an_announce_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = Client::connect(env.bind_address()).await.expect("it should connect"); + let client = Client::connect(env.bind_address(), CLIENT_TIMEOUT_DEFAULT) + .await + .expect("it should connect"); let ctx = client .do_connection_request(TransactionId(123)) @@ -126,17 +134,20 @@ mod receiving_an_announce_request { mod receiving_an_scrape_request { use aquatic_udp_protocol::{ConnectionId, InfoHash, ScrapeRequest, TransactionId}; - use torrust_tracker::shared::bit_torrent::tracker::udp::client::Client; + use torrust_tracker::shared::bit_torrent::tracker::udp::Client; + use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; use torrust_tracker_test_helpers::configuration; + use crate::common::udp::Started; use crate::servers::udp::asserts::is_scrape_response; - use crate::servers::udp::Started; #[tokio::test] async fn should_return_a_scrape_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = Client::connect(env.bind_address()).await.expect("it should connect"); + let client = Client::connect(env.bind_address(), CLIENT_TIMEOUT_DEFAULT) + .await + .expect("it should connect"); let ctx = client .do_connection_request(TransactionId(123)) diff --git a/tests/servers/udp/mod.rs b/tests/servers/udp/mod.rs index b13b82240..e2db6b4ce 100644 --- a/tests/servers/udp/mod.rs +++ b/tests/servers/udp/mod.rs @@ -1,7 +1,2 @@ -use torrust_tracker::servers::udp::server; - pub mod asserts; pub mod contract; -pub mod environment; - -pub type Started = environment::Environment; From 608585eaf65a08df28fbeb82400647fd15f78a25 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 4 Apr 2024 17:12:43 +0100 Subject: [PATCH 008/217] chore: add new cargo dependency: crossbeam-skiplist It will be used to create a new torrent repository implementation that allos adding torrents in parallel. That could be potencially faster than BTreeMap in a write intensive context. --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e77b5de6d..74d9aaafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,6 +1027,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -3908,6 +3918,7 @@ dependencies = [ "clap", "colored", "config", + "crossbeam-skiplist", "derive_more", "fern", "futures", @@ -4013,6 +4024,7 @@ version = "3.0.0-alpha.12-develop" dependencies = [ "async-std", "criterion", + "crossbeam-skiplist", "futures", "rstest", "tokio", diff --git a/Cargo.toml b/Cargo.toml index d045b945a..8c1df0685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } colored = "2" config = "0" +crossbeam-skiplist = "0.1" derive_more = "0" fern = "0" futures = "0" @@ -63,8 +64,8 @@ serde_json = "1" serde_repr = "0" thiserror = "1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } -torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "packages/clock" } +torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = "contrib/bencode" } torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "packages/located-error" } torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" } @@ -105,4 +106,4 @@ opt-level = 3 [profile.release-debug] inherits = "release" -debug = true \ No newline at end of file +debug = true From 642d6be04ae968a2b170ede2c379183a792bd782 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 4 Apr 2024 17:14:43 +0100 Subject: [PATCH 009/217] feat: new torrent repository using crossbeam_skiplist::SkipMap SkipMap is an ordered map based on a lock-free skip list. It's al alternative to BTreeMap which supports concurrent access across multiple threads. One of the performance problems with the current solution is we can only add one torrent at the time because threads need to lock the whole BTreeMap. The SkipMap should avoid that problem. More info about SkiMap: https://docs.rs/crossbeam-skiplist/latest/crossbeam_skiplist/struct.SkipMap.html#method.remove The aquatic UDP load test was executed with the current implementation and the new one: Current Implementation: Requests out: 397287.37/second Responses in: 357549.15/second - Connect responses: 177073.94 - Announce responses: 176905.36 - Scrape responses: 3569.85 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 287 - p100: 371 New Implementation: Requests out: 396788.68/second Responses in: 357105.27/second - Connect responses: 176662.91 - Announce responses: 176863.44 - Scrape responses: 3578.91 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 105 - p99.9: 287 - p100: 351 The result is pretty similar but the benchmarking for the repository using criterios shows that this implementations is a litle bit better than the current one. --- cSpell.json | 1 + packages/torrent-repository/Cargo.toml | 5 +- .../benches/repository_benchmark.rs | 21 +++- packages/torrent-repository/src/lib.rs | 3 + .../torrent-repository/src/repository/mod.rs | 1 + .../src/repository/skip_map_mutex_std.rs | 106 ++++++++++++++++++ src/core/torrent/mod.rs | 5 +- 7 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 packages/torrent-repository/src/repository/skip_map_mutex_std.rs diff --git a/cSpell.json b/cSpell.json index 0ee2f8306..0480590af 100644 --- a/cSpell.json +++ b/cSpell.json @@ -135,6 +135,7 @@ "Shareaza", "sharktorrent", "SHLVL", + "skiplist", "socketaddr", "sqllite", "subsec", diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 4cea8767f..5f1a20d32 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -16,11 +16,12 @@ rust-version.workspace = true version.workspace = true [dependencies] +crossbeam-skiplist = "0.1" futures = "0.3.29" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } -torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } -torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } +torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } +torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } [dev-dependencies] criterion = { version = "0", features = ["async_tokio"] } diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index a3684c8e2..65608c86c 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,7 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, - TorrentsRwLockTokioMutexTokio, + TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; use crate::helpers::{asyn, sync}; @@ -45,6 +45,10 @@ fn add_one_torrent(c: &mut Criterion) { .iter_custom(asyn::add_one_torrent::); }); + group.bench_function("SkipMapMutexStd", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.finish(); } @@ -89,6 +93,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| asyn::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -133,6 +142,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| asyn::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -178,6 +192,11 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { }); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 8bb1b6def..f7c19624c 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_clock::clock; pub mod entry; @@ -16,6 +17,8 @@ pub type TorrentsRwLockTokio = repository::RwLockTokio; pub type TorrentsRwLockTokioMutexStd = repository::RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = repository::RwLockTokio; +pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; + /// This code needs to be copied into each crate. /// Working version, for production. #[cfg(not(test))] diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 494040c9d..7ede1f87a 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -11,6 +11,7 @@ pub mod rw_lock_std_mutex_tokio; pub mod rw_lock_tokio; pub mod rw_lock_tokio_mutex_std; pub mod rw_lock_tokio_mutex_tokio; +pub mod skip_map_mutex_std; use std::fmt::Debug; diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs new file mode 100644 index 000000000..aa1f43826 --- /dev/null +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use crossbeam_skiplist::SkipMap; +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; + +use super::Repository; +use crate::entry::{Entry, EntrySync}; +use crate::{EntryMutexStd, EntrySingle}; + +#[derive(Default, Debug)] +pub struct CrossbeamSkipList { + torrents: SkipMap, +} + +impl Repository for CrossbeamSkipList +where + EntryMutexStd: EntrySync, + EntrySingle: Entry, +{ + fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().insert_or_update_peer_and_get_stats(peer) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().expect("it should get a lock").get_stats(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexStd::new( + EntrySingle { + peers: BTreeMap::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index ab78de683..5d42e8b4d 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -26,6 +26,7 @@ //! Peer that don not have a full copy of the torrent data are called "leechers". //! -use torrust_tracker_torrent_repository::TorrentsRwLockStdMutexStd; +use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; -pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used +//pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used +pub type Torrents = TorrentsSkipMapMutexStd; // Currently Used From eec20247e146b0b822e86061943f4c899c93658f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 5 Apr 2024 18:27:17 +0100 Subject: [PATCH 010/217] chore: ignore crossbeam-skiplist crate in cargo-machete It's been used in the src/packages/torrent-repository package. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c1df0685..f440799cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ url = "2" uuid = { version = "1", features = ["v4"] } [package.metadata.cargo-machete] -ignored = ["serde_bytes"] +ignored = ["serde_bytes", "crossbeam-skiplist"] [dev-dependencies] local-ip-address = "0" From 098928592f821cb2aa779746d6e40b71703c529b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 5 Apr 2024 18:29:24 +0100 Subject: [PATCH 011/217] refactor: separate torrent repository trait from implementations There are now more implementations. --- packages/torrent-repository/src/lib.rs | 14 ++++---- .../torrent-repository/src/repository/mod.rs | 34 ------------------- .../src/repository/rw_lock_std.rs | 16 +++++++++ .../src/repository/rw_lock_tokio.rs | 18 ++++++++++ .../tests/repository/mod.rs | 3 +- src/core/torrent/mod.rs | 2 -- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index f7c19624c..ccaf579e3 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use repository::rw_lock_std::RwLockStd; +use repository::rw_lock_tokio::RwLockTokio; use repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_clock::clock; @@ -10,12 +12,12 @@ pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; -pub type TorrentsRwLockStd = repository::RwLockStd; -pub type TorrentsRwLockStdMutexStd = repository::RwLockStd; -pub type TorrentsRwLockStdMutexTokio = repository::RwLockStd; -pub type TorrentsRwLockTokio = repository::RwLockTokio; -pub type TorrentsRwLockTokioMutexStd = repository::RwLockTokio; -pub type TorrentsRwLockTokioMutexTokio = repository::RwLockTokio; +pub type TorrentsRwLockStd = RwLockStd; +pub type TorrentsRwLockStdMutexStd = RwLockStd; +pub type TorrentsRwLockStdMutexTokio = RwLockStd; +pub type TorrentsRwLockTokio = RwLockTokio; +pub type TorrentsRwLockTokioMutexStd = RwLockTokio; +pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 7ede1f87a..975a876d8 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -41,37 +41,3 @@ pub trait RepositoryAsync: Debug + Default + Sized + 'static { peer: &peer::Peer, ) -> impl std::future::Future + Send; } - -#[derive(Default, Debug)] -pub struct RwLockStd { - torrents: std::sync::RwLock>, -} - -#[derive(Default, Debug)] -pub struct RwLockTokio { - torrents: tokio::sync::RwLock>, -} - -impl RwLockStd { - /// # Panics - /// - /// Panics if unable to get a lock. - pub fn write( - &self, - ) -> std::sync::RwLockWriteGuard<'_, std::collections::BTreeMap> { - self.torrents.write().expect("it should get lock") - } -} - -impl RwLockTokio { - pub fn write( - &self, - ) -> impl std::future::Future< - Output = tokio::sync::RwLockWriteGuard< - '_, - std::collections::BTreeMap, - >, - > { - self.torrents.write() - } -} diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 9d7f29416..e9074a271 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -11,6 +11,22 @@ use super::Repository; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockStd}; +#[derive(Default, Debug)] +pub struct RwLockStd { + pub(crate) torrents: std::sync::RwLock>, +} + +impl RwLockStd { + /// # Panics + /// + /// Panics if unable to get a lock. + pub fn write( + &self, + ) -> std::sync::RwLockWriteGuard<'_, std::collections::BTreeMap> { + self.torrents.write().expect("it should get lock") + } +} + impl TorrentsRwLockStd { fn get_torrents<'a>(&'a self) -> std::sync::RwLockReadGuard<'a, std::collections::BTreeMap> where diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index fa84e2451..d84074eaf 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -11,6 +11,24 @@ use super::RepositoryAsync; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockTokio}; +#[derive(Default, Debug)] +pub struct RwLockTokio { + pub(crate) torrents: tokio::sync::RwLock>, +} + +impl RwLockTokio { + pub fn write( + &self, + ) -> impl std::future::Future< + Output = tokio::sync::RwLockWriteGuard< + '_, + std::collections::BTreeMap, + >, + > { + self.torrents.write() + } +} + impl TorrentsRwLockTokio { async fn get_torrents<'a>(&'a self) -> tokio::sync::RwLockReadGuard<'a, std::collections::BTreeMap> where diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 7ffe17dd7..117f3c0a6 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; -use torrust_tracker_torrent_repository::repository::{RwLockStd, RwLockTokio}; +use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; +use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; use torrust_tracker_torrent_repository::EntrySingle; use crate::common::repo::Repo; diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index 5d42e8b4d..286a7e047 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -25,8 +25,6 @@ //! - The number of peers that have NOT completed downloading the torrent and are still active, that means they are actively participating in the network. //! Peer that don not have a full copy of the torrent data are called "leechers". //! - use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; -//pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used pub type Torrents = TorrentsSkipMapMutexStd; // Currently Used From ee10d712f8a432d17cb67b73acec56ce1d89adad Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Mon, 8 Apr 2024 17:11:29 +0800 Subject: [PATCH 012/217] udp server graceful shutdown --- Cargo.lock | 58 +- Cargo.toml | 2 +- packages/located-error/src/lib.rs | 1 + packages/primitives/src/peer.rs | 1 + src/app.rs | 20 +- src/bootstrap/jobs/health_check_api.rs | 37 +- src/bootstrap/jobs/http_tracker.rs | 17 +- src/bootstrap/jobs/tracker_apis.rs | 13 +- src/bootstrap/jobs/udp_tracker.rs | 15 +- src/servers/apis/server.rs | 72 ++- src/servers/health_check_api/check.rs | 26 + src/servers/health_check_api/handle.rs | 88 +++ src/servers/health_check_api/handlers.rs | 50 -- src/servers/health_check_api/launcher.rs | 61 ++ src/servers/health_check_api/mod.rs | 8 +- src/servers/health_check_api/server.rs | 202 ------ src/servers/health_check_api/v0/handlers.rs | 45 ++ src/servers/health_check_api/v0/mod.rs | 4 + .../health_check_api/{ => v0}/resources.rs | 33 + .../health_check_api/{ => v0}/responses.rs | 0 src/servers/health_check_api/v0/routes.rs | 58 ++ src/servers/http/check.rs | 45 ++ src/servers/http/handle.rs | 60 ++ src/servers/http/launcher.rs | 114 ++++ src/servers/http/mod.rs | 5 +- src/servers/http/service.rs | 178 ------ src/servers/registar.rs | 185 +++--- src/servers/service.rs | 47 +- src/servers/signals.rs | 51 +- src/servers/udp/check.rs | 35 ++ src/servers/udp/handle.rs | 348 +++++++---- src/servers/udp/launcher.rs | 52 ++ src/servers/udp/logging.rs | 2 +- src/servers/udp/mod.rs | 54 +- src/servers/udp/server.rs | 591 ++++++++++-------- .../{connection_cookie.rs => v0/cookie.rs} | 2 +- src/servers/udp/{ => v0}/error.rs | 0 src/servers/udp/{ => v0}/handlers.rs | 276 ++++---- src/servers/udp/v0/mod.rs | 14 + src/shared/bit_torrent/tracker/udp/mod.rs | 7 + src/shared/handle.rs | 289 +++++++++ src/shared/mod.rs | 1 + tests/common/udp/environment.rs | 13 +- tests/common/udp/mod.rs | 4 +- tests/servers/api/environment.rs | 2 +- tests/servers/health_check_api/contract.rs | 8 +- tests/servers/health_check_api/environment.rs | 17 +- tests/servers/http/environment.rs | 13 +- tests/servers/http/mod.rs | 4 +- 49 files changed, 1990 insertions(+), 1238 deletions(-) create mode 100644 src/servers/health_check_api/check.rs create mode 100644 src/servers/health_check_api/handle.rs delete mode 100644 src/servers/health_check_api/handlers.rs create mode 100644 src/servers/health_check_api/launcher.rs delete mode 100644 src/servers/health_check_api/server.rs create mode 100644 src/servers/health_check_api/v0/handlers.rs create mode 100644 src/servers/health_check_api/v0/mod.rs rename src/servers/health_check_api/{ => v0}/resources.rs (53%) rename src/servers/health_check_api/{ => v0}/responses.rs (100%) create mode 100644 src/servers/health_check_api/v0/routes.rs create mode 100644 src/servers/http/check.rs create mode 100644 src/servers/http/handle.rs create mode 100644 src/servers/http/launcher.rs delete mode 100644 src/servers/http/service.rs create mode 100644 src/servers/udp/check.rs create mode 100644 src/servers/udp/launcher.rs rename src/servers/udp/{connection_cookie.rs => v0/cookie.rs} (99%) rename src/servers/udp/{ => v0}/error.rs (100%) rename src/servers/udp/{ => v0}/handlers.rs (81%) create mode 100644 src/servers/udp/v0/mod.rs create mode 100644 src/shared/handle.rs diff --git a/Cargo.lock b/Cargo.lock index f4cedd763..6d27b019f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1769,15 +1769,6 @@ dependencies = [ "hashbrown 0.13.2", ] -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - [[package]] name = "matchit" version = "0.7.3" @@ -2562,17 +2553,8 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2583,15 +2565,9 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.2" @@ -3598,7 +3574,6 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", - "tracing-test", "url", "uuid", ] @@ -3777,41 +3752,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] -[[package]] -name = "tracing-test" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" -dependencies = [ - "lazy_static", - "tracing-core", - "tracing-subscriber", - "tracing-test-macro", -] - -[[package]] -name = "tracing-test-macro" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" -dependencies = [ - "lazy_static", - "quote", - "syn 1.0.109", -] - [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index a676e223d..86960d9e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } tracing = "0" tracing-subscriber = "0" -tracing-test = "0" +# tracing-test = "0" url = "2" uuid = { version = "1", features = ["v4"] } diff --git a/packages/located-error/src/lib.rs b/packages/located-error/src/lib.rs index bfd4d4a86..ff2ceebb4 100644 --- a/packages/located-error/src/lib.rs +++ b/packages/located-error/src/lib.rs @@ -137,4 +137,5 @@ mod tests { assert_eq!(b.location.file(), l.file()); } + } diff --git a/packages/primitives/src/peer.rs b/packages/primitives/src/peer.rs index 785818a0f..f71067065 100644 --- a/packages/primitives/src/peer.rs +++ b/packages/primitives/src/peer.rs @@ -427,6 +427,7 @@ pub mod fixture { #[cfg(test)] pub mod test { + mod torrent_peer_id { use crate::peer; diff --git a/src/app.rs b/src/app.rs index 0d0ddd8a7..2b13a57d8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -67,7 +67,7 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) config.bind_address, tracker_config.mode ); } else if let Some(job) = - udp_tracker::start_job(config, tracker.clone(), registar.give_form(), servers::udp::Version::V0).await + udp_tracker::start_job(config, tracker.clone(), registar.form(), servers::udp::Version::V0).await { jobs.push(job); }; @@ -75,9 +75,7 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) // Start the HTTP blocks for config in &tracker_config.http_trackers { - if let Some(job) = - http_tracker::start_job(config, tracker.clone(), registar.give_form(), servers::http::Version::V1).await - { + if let Some(job) = http_tracker::start_job(config, tracker.clone(), registar.form(), servers::http::Version::V1).await { jobs.push(job); }; } @@ -87,7 +85,7 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) if let Some(job) = tracker_apis::start_job( &tracker_config.http_api, tracker.clone(), - registar.give_form(), + registar.form(), servers::apis::Version::V1, ) .await @@ -101,16 +99,8 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) jobs.push(torrent_cleanup::start_job(tracker_config, &tracker)); } - // Start Health Check API - jobs.push( - health_check_api::start_job( - &tracker_config.health_check_api, - registar.entries(), - registar.give_form(), - Version::V0, - ) - .await, - ); + // Start Health Check API, consuming the registar. + jobs.push(health_check_api::start_job(&tracker_config.health_check_api, ®istar, Version::V0).await); jobs } diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index 5f87b02df..1e3fa6be3 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -15,6 +15,7 @@ //! for the API configuration options. use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; @@ -22,9 +23,9 @@ use torrust_tracker_configuration::HealthCheckApi; use tracing::instrument; use super::Error; -use crate::servers::health_check_api::server::HealthCheckLauncher; +use crate::servers::health_check_api::launcher::Launcher; use crate::servers::health_check_api::Version; -use crate::servers::registar::{ServiceRegistrationForm, ServiceRegistry}; +use crate::servers::registar::{Form, Registar, Registry}; use crate::servers::service::Service; /// This function starts a new Health Check API server with the provided @@ -39,15 +40,13 @@ use crate::servers::service::Service; /// It would panic if unable to send the `ApiServerJobStarted` notice. /// #[allow(clippy::async_yields_async)] -#[instrument(ret, skip(form), fields(registry = %registry))] -pub async fn start_job( - config: &HealthCheckApi, - registry: ServiceRegistry, - form: ServiceRegistrationForm, - version: Version, -) -> JoinHandle<()> { +#[instrument(ret, fields(registar = %registar))] +pub async fn start_job(config: &HealthCheckApi, registar: &Registar, version: Version) -> JoinHandle<()> { let addr = config.bind_address.parse().expect("it should parse the binding address"); + let form = registar.form(); + let registry = registar.as_ref().clone(); + match version { Version::V0 => start_v0(addr, registry, form).await.expect("it should start the service"), } @@ -60,8 +59,8 @@ pub async fn start_job( /// #[allow(clippy::async_yields_async)] #[instrument(err, ret, skip(form), fields(registry = %registry))] -async fn start_v0(addr: SocketAddr, registry: ServiceRegistry, form: ServiceRegistrationForm) -> Result, Error> { - let service = Service::new(HealthCheckLauncher::new(addr, registry)); +async fn start_v0(addr: SocketAddr, registry: Arc, form: Form) -> Result, Error> { + let service = Service::new(Launcher::new(addr, registry)); let started = service.start().map_err(Error::from)?; @@ -83,22 +82,22 @@ mod tests { use std::sync::Arc; use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - use tracing_test::traced_test; + //use tracing_test::traced_test; use crate::bootstrap::jobs::health_check_api::{start_job, start_v0}; use crate::servers::health_check_api::Version; use crate::servers::registar::Registar; #[tokio::test] - #[traced_test] + //#[traced_test] async fn it_should_start_and_stop_the_job_for_the_health_check_service() { let cfg = Arc::new(ephemeral_mode_public()); let config = &cfg.health_check_api; let version = Version::V0; - let registar = &Registar::default(); + let registar = Registar::default(); - let job = start_job(config, registar.get_registry(), registar.give_form(), version).await; + let job = start_job(config, ®istar, version).await; job.abort(); @@ -106,7 +105,7 @@ mod tests { } #[tokio::test] - #[traced_test] + //#[traced_test] async fn it_should_start_and_stop_the_v0_of_the_health_check_service() { let addr = ephemeral_mode_public() .health_check_api @@ -115,10 +114,10 @@ mod tests { .expect("it should parse the binding address"); let registar = Registar::default(); + let form = registar.form(); + let registry = registar.as_ref().clone(); - let job = start_v0(addr, registar.get_registry(), registar.give_form()) - .await - .expect("it should start"); + let job = start_v0(addr, registry, form).await.expect("it should start"); job.abort(); diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 34687afb6..fb129f000 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -20,9 +20,9 @@ use tracing::{info, instrument}; use super::make_rust_tls; use crate::core; -use crate::servers::http::service::HttpLauncher; +use crate::servers::http::launcher::Launcher; use crate::servers::http::Version; -use crate::servers::registar::ServiceRegistrationForm; +use crate::servers::registar::Form; use crate::servers::service::Service; /// It starts a new HTTP server with the provided configuration and version. @@ -39,7 +39,7 @@ use crate::servers::service::Service; pub async fn start_job( config: &HttpTracker, tracker: Arc, - form: ServiceRegistrationForm, + form: Form, version: Version, ) -> Option> { if config.enabled { @@ -63,13 +63,8 @@ pub async fn start_job( #[allow(clippy::async_yields_async)] #[instrument(ret)] -async fn start_v1( - socket: SocketAddr, - tls: Option, - tracker: Arc, - form: ServiceRegistrationForm, -) -> JoinHandle<()> { - let service = Service::new(HttpLauncher::new(tracker, socket, tls)); +async fn start_v1(socket: SocketAddr, tls: Option, tracker: Arc, form: Form) -> JoinHandle<()> { + let service = Service::new(Launcher::new(tracker, socket, tls)); let started = service.start().expect("it should start"); @@ -101,7 +96,7 @@ mod tests { let tracker = tracker(&cfg); let version = Version::V1; - let job = start_job(config, tracker, Registar::default().give_form(), version) + let job = start_job(config, tracker, Registar::default().form(), version) .await .expect("it should be able to join to the http tracker start-job"); diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index a6df7b125..f4865e191 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -32,7 +32,7 @@ use super::make_rust_tls; use crate::core; use crate::servers::apis::server::ApiLauncher; use crate::servers::apis::Version; -use crate::servers::registar::ServiceRegistrationForm; +use crate::servers::registar::Form; use crate::servers::service::Service; /// This is the message that the "launcher" spawned task sends to the main @@ -56,12 +56,7 @@ pub struct ApiServerJobStarted(); /// #[allow(clippy::async_yields_async)] #[instrument(ret)] -pub async fn start_job( - config: &HttpApi, - tracker: Arc, - form: ServiceRegistrationForm, - version: Version, -) -> Option> { +pub async fn start_job(config: &HttpApi, tracker: Arc, form: Form, version: Version) -> Option> { if config.enabled { let bind_to = config .bind_address @@ -89,7 +84,7 @@ async fn start_v1( socket: SocketAddr, tls: Option, tracker: Arc, - form: ServiceRegistrationForm, + form: Form, access_tokens: Arc, ) -> JoinHandle<()> { let service = Service::new(ApiLauncher::new(tracker, access_tokens, socket, tls)); @@ -124,7 +119,7 @@ mod tests { let tracker = tracker(&cfg); let version = Version::V1; - start_job(config, tracker, Registar::default().give_form(), version) + start_job(config, tracker, Registar::default().form(), version) .await .expect("it should be able to join to the tracker api start-job"); } diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index c0eea5237..abf952d4b 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -14,9 +14,9 @@ use torrust_tracker_configuration::UdpTracker; use tracing::{info, instrument}; use crate::core; -use crate::servers::registar::ServiceRegistrationForm; +use crate::servers::registar::Form; use crate::servers::service::Service; -use crate::servers::udp::server::UdpLauncher; +use crate::servers::udp::launcher::Launcher; use crate::servers::udp::Version; /// It starts a new UDP server with the provided configuration. @@ -32,12 +32,7 @@ use crate::servers::udp::Version; #[must_use] #[allow(clippy::async_yields_async)] #[instrument(ret)] -pub async fn start_job( - config: &UdpTracker, - tracker: Arc, - form: ServiceRegistrationForm, - version: Version, -) -> Option> { +pub async fn start_job(config: &UdpTracker, tracker: Arc, form: Form, version: Version) -> Option> { if config.enabled { let addr = config .bind_address @@ -55,8 +50,8 @@ pub async fn start_job( #[allow(clippy::async_yields_async)] #[instrument(ret)] -async fn start_v0(socket: SocketAddr, tracker: Arc, form: ServiceRegistrationForm) -> JoinHandle<()> { - let service = Service::new(UdpLauncher::new(tracker, socket)); +async fn start_v0(socket: SocketAddr, tracker: Arc, form: Form) -> JoinHandle<()> { + let service = Service::new(Launcher::new(tracker, socket)); let started = service.start().expect("it should start"); diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index b37c38a5a..bface9d13 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -30,33 +30,51 @@ use axum_server::tls_rustls::RustlsConfig; use derive_more::{Constructor, Display}; use futures::{FutureExt as _, TryFutureExt as _}; use torrust_tracker_configuration::AccessTokens; +use torrust_tracker_located_error::DynError; +use tracing::info; use super::routes::router; use crate::core::Tracker; -use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; -use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; +use crate::servers::registar::{self, HealthCheckFactory, HeathCheckFuture, HeathCheckResult}; +use crate::servers::service::{self, AddrFuture, Error, TaskFuture}; use crate::servers::signals::Halted; use crate::servers::tcp::graceful_axum_shutdown; -/// Checks the Health by connecting to the API service endpoint. +/// Build a health check future task for checking +/// the http api health check endpoint. /// -/// # Errors -/// -/// This function will return an error if unable to connect. -/// Or if there request returns an error code. #[must_use] -fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/api/health_check"); - - let info = format!("checking api health check at: {url}"); - - let job = tokio::spawn(async move { - match reqwest::get(url).await { - Ok(response) => Ok(response.status().to_string()), - Err(err) => Err(err.to_string()), - } - }); - ServiceHealthCheckJob::new(*binding, info, job) +fn check_builder(addr: SocketAddr) -> HeathCheckFuture<'static> { + let url = format!("http://{addr}/api/health_check"); + + info!("checking api health check at: {url}"); + + let response = reqwest::get(url) + .map_err(move |e| registar::Error::UnableToGetAnyResponse { + addr, + msg: "Udp Client".to_string(), + err: DynError::into(Arc::new(e)), + }) + .boxed(); + + let check = response + .and_then(move |r| async move { + r.error_for_status() + .map_err(move |e| registar::Error::UnableToObtainGoodResponse { + addr, + msg: "Udp Client".to_string(), + err: DynError::into(Arc::new(e)), + }) + }) + .boxed(); + + check + .map_ok(move |r| registar::Success::AllGood { + addr, + msg: r.status().to_string(), + }) + .map(HeathCheckResult::from) + .boxed() } #[derive(Debug)] @@ -93,7 +111,7 @@ impl Default for ApiHandle { } } -impl Handle for ApiHandle { +impl service::Handle for ApiHandle { fn stop(mut self) -> Result<(), Error> { self.shutdown() } @@ -101,6 +119,10 @@ impl Handle for ApiHandle { fn listening(&self) -> AddrFuture<'_> { self.axum_handle.listening().boxed() } + + fn into_graceful_shutdown_future<'a>(self) -> futures::prelude::future::BoxFuture<'a, Result<(), Error>> { + todo!() + } } impl Drop for ApiHandle { @@ -128,8 +150,8 @@ impl ApiLauncher { } } -impl Launcher for ApiLauncher { - fn start(self) -> Result<(TaskFuture<'static, (), Error>, ApiHandle, FnSpawnServiceHeathCheck), Error> { +impl service::Launcher for ApiLauncher { + fn start(self) -> Result<(TaskFuture<'static, (), Error>, ApiHandle, HealthCheckFactory), Error> { let handle = ApiHandle::default(); let running: TaskFuture<'_, (), Error> = { @@ -160,7 +182,9 @@ impl Launcher for ApiLauncher { } }; - Ok((running, handle, check_fn)) + let check_factory = HealthCheckFactory::new(check_builder); + + Ok((running, handle, check_factory)) } } @@ -199,7 +223,7 @@ mod tests { let stopped = Service::new(ApiLauncher::new(tracker, access_tokens, addr, tls)); let started = stopped.start().expect("it should start the server"); - let () = started.reg_form(register.give_form()).await.expect("it should register"); + let () = started.reg_form(register.form()).await.expect("it should register"); let stopped = started.stop().await.expect("it should stop the server"); diff --git a/src/servers/health_check_api/check.rs b/src/servers/health_check_api/check.rs new file mode 100644 index 000000000..cf6a883cd --- /dev/null +++ b/src/servers/health_check_api/check.rs @@ -0,0 +1,26 @@ +use std::net::SocketAddr; + +use futures::FutureExt as _; +use tracing::instrument; + +use crate::servers::registar; + +/// Placeholder Check Function for the Health Check Itself +/// +/// # Errors +/// +/// This function will return an error if the check would fail. +/// +#[must_use] +#[instrument] +pub fn check_builder(addr: SocketAddr) -> registar::HeathCheckFuture<'static> { + let url = format!("http://{addr}/health_check"); + + let msg = format!("todo self-check health check at: {url}"); + + let success = registar::Success::AllGood { addr, msg }; + + let result = registar::HeathCheckResult::from(Ok(success)); + + async move { result }.boxed() +} diff --git a/src/servers/health_check_api/handle.rs b/src/servers/health_check_api/handle.rs new file mode 100644 index 000000000..c530fe1c6 --- /dev/null +++ b/src/servers/health_check_api/handle.rs @@ -0,0 +1,88 @@ +use futures::FutureExt as _; +use tracing::{error, info, instrument, trace, warn}; + +use crate::servers::service; +use crate::servers::signals::Halted; +use crate::servers::tcp::graceful_axum_shutdown; + +pub struct Handle { + pub axum_handle: axum_server::Handle, + tx_shutdown: Option>, +} + +impl std::fmt::Debug for Handle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Handle") + .field("axum_handle_conn:", &self.axum_handle.connection_count()) + .finish_non_exhaustive() + } +} + +impl Handle { + #[instrument] + fn shutdown(&mut self) -> Result<(), service::Error> { + trace!("the internal shut down was called"); + if let Some(tx) = self.tx_shutdown.take() { + trace!("sending a normal halt on the shutdown channel"); + tx.send(Halted::Normal) + .map_err(|err| service::Error::UnableToSendHaltingMessage { err })?; + } else { + error!("shutdown was called, but the channel was missing!"); + panic!(); + }; + Ok(()) + } +} + +impl Default for Handle { + #[instrument(ret)] + fn default() -> Self { + trace!("setup the shutdown channel"); + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + trace!("setup the axum handle"); + let axum_handle = axum_server::Handle::default(); + + trace!("setup the graceful axum meta-handler"); + let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "Health Check Server".to_string()); + + trace!("returning the new default handler"); + Self { + axum_handle: axum_server::Handle::new(), + tx_shutdown: Some(tx_shutdown), + } + } +} + +impl service::Handle for Handle { + #[instrument(ret)] + fn stop(mut self) -> Result<(), service::Error> { + info!("shutdown function was called"); + self.shutdown() + } + + #[instrument] + fn listening(&self) -> service::AddrFuture<'_> { + info!("return the listening future form the axum handler"); + self.axum_handle.listening().boxed() + } + + fn into_graceful_shutdown_future<'a>(self) -> futures::prelude::future::BoxFuture<'a, Result<(), service::Error>> { + todo!() + } +} + +impl Drop for Handle { + #[instrument] + fn drop(&mut self) { + match self.tx_shutdown { + Some(_) => { + warn!("shutting down via drop"); + self.shutdown().expect("it should shutdown when dropped"); + } + None => { + trace!("shutdown has already been called, dropping"); + } + } + } +} diff --git a/src/servers/health_check_api/handlers.rs b/src/servers/health_check_api/handlers.rs deleted file mode 100644 index f63f98c12..000000000 --- a/src/servers/health_check_api/handlers.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::collections::VecDeque; - -use axum::extract::State; -use axum::Json; - -use super::resources::{CheckReport, Report}; -use super::responses; -use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistry}; - -/// Endpoint for container health check. -/// -/// Creates a vector [`CheckReport`] from the input set of [`CheckJob`], and then builds a report from the results. -/// -pub(crate) async fn health_check_handler(State(register): State) -> Json { - #[allow(unused_assignments)] - let mut checks: VecDeque = VecDeque::new(); - - { - let mutex = register.as_ref().lock().expect("it should get a lock"); - - checks = mutex.values().map(ServiceRegistration::spawn_check).collect(); - } - - // if we do not have any checks, lets return a `none` result. - if checks.is_empty() { - return responses::none(); - } - - let jobs = checks.drain(..).map(|c| { - tokio::spawn(async move { - CheckReport { - binding: c.addr, - info: c.info.clone(), - result: c.job.await.expect("it should be able to join into the checking function"), - } - }) - }); - - let results: Vec = futures::future::join_all(jobs) - .await - .drain(..) - .map(|r| r.expect("it should be able to connect to the job")) - .collect(); - - if results.iter().any(CheckReport::fail) { - responses::error("health check failed".to_string(), results) - } else { - responses::ok(results) - } -} diff --git a/src/servers/health_check_api/launcher.rs b/src/servers/health_check_api/launcher.rs new file mode 100644 index 000000000..40b9f0524 --- /dev/null +++ b/src/servers/health_check_api/launcher.rs @@ -0,0 +1,61 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use derive_more::{Constructor, Display}; +use futures::{FutureExt as _, TryFutureExt as _}; +use tracing::{info, instrument, trace}; + +use super::handle::Handle; +use crate::servers::health_check_api::check::check_builder; +use crate::servers::health_check_api::v0::routes::router; +use crate::servers::registar::{HealthCheckFactory, Registry}; +use crate::servers::service; + +#[derive(Clone, Constructor, Debug, Display)] +#[display(fmt = "intended_address: {addr}, registry: {registry}")] +pub struct Launcher { + pub addr: SocketAddr, + pub registry: Arc, +} + +impl service::Launcher for Launcher { + #[instrument(err, fields(self = %self))] + fn start(self) -> Result<(service::TaskFuture<'static, (), service::Error>, Handle, HealthCheckFactory), service::Error> { + trace!("setup the health check handler"); + let handle = Handle::default(); + + trace!("make service task"); + let task: service::TaskFuture<'_, (), service::Error> = { + trace!(address = ?self.addr, "try to bind on socket"); + let listener = std::net::TcpListener::bind(self.addr).map_err(|e| service::Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + trace!("try to get local address"); + let addr = listener + .local_addr() + .map_err(|e| service::Error::UnableToGetLocalAddress { err: e.into() })?; + info!(address = ?addr, "health tracker bound to tcp socket: {addr}"); + + trace!("setup router"); + + let router = router(self.registry, addr); + + trace!("make router into service"); + let make_service = router.into_make_service_with_connect_info::(); + + info!("start and return axum service"); + axum_server::from_tcp(listener) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| service::Error::UnableToServe { err: e.into() }) + .boxed() + }; + + let check_factory = HealthCheckFactory::new(check_builder); + + trace!("returning the axum task, handle, and check function closure"); + Ok((task, handle, check_factory)) + } +} diff --git a/src/servers/health_check_api/mod.rs b/src/servers/health_check_api/mod.rs index 060fb44a1..561e75efe 100644 --- a/src/servers/health_check_api/mod.rs +++ b/src/servers/health_check_api/mod.rs @@ -1,7 +1,7 @@ -pub mod handlers; -pub mod resources; -pub mod responses; -pub mod server; +pub mod check; +pub mod handle; +pub mod launcher; +pub mod v0; #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum Version { diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs deleted file mode 100644 index 1146c42e6..000000000 --- a/src/servers/health_check_api/server.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! Logic to run the Health Check HTTP API server. -//! -//! This API is intended to be used by the container infrastructure to check if -//! the whole application is healthy. -use std::net::SocketAddr; -use std::time::Duration; - -use axum::http::HeaderName; -use axum::response::Response; -use axum::routing::get; -use axum::Json; -use derive_more::{Constructor, Display}; -use futures::{FutureExt as _, TryFutureExt as _}; -use hyper::Request; -use serde_json::json; -use tower_http::compression::CompressionLayer; -use tower_http::propagate_header::PropagateHeaderLayer; -use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; -use tower_http::trace::{DefaultMakeSpan, TraceLayer}; -use tracing::{error, info, instrument, trace, warn, Level, Span}; - -use crate::servers::health_check_api::handlers::health_check_handler; -use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob, ServiceRegistry}; -use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; -use crate::servers::signals::Halted; -use crate::servers::tcp::graceful_axum_shutdown; - -/// Placeholder Check Function for the Health Check Itself -/// -/// # Errors -/// -/// This function will return an error if the check would fail. -/// -#[must_use] -#[instrument(ret)] -fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/health_check"); - - let info = format!("todo self-check health check at: {url}"); - - let job = tokio::spawn(async move { Ok("Todo: Not Implemented".to_string()) }); - ServiceHealthCheckJob::new(*binding, info, job) -} - -pub struct HealthCheckHandle { - pub axum_handle: axum_server::Handle, - tx_shutdown: Option>, -} - -impl std::fmt::Debug for HealthCheckHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("HealthCheckHandle") - .field("axum_handle_conn:", &self.axum_handle.connection_count()) - .finish_non_exhaustive() - } -} - -#[derive(Constructor, Clone, Debug, Display)] -#[display(fmt = "intended_address: {addr}, registry: {registry}")] -pub struct HealthCheckLauncher { - pub addr: SocketAddr, - pub registry: ServiceRegistry, -} - -impl HealthCheckHandle { - #[instrument(err, ret)] - fn shutdown(&mut self) -> Result<(), Error> { - trace!("the internal shut down was called"); - if let Some(tx) = self.tx_shutdown.take() { - trace!("sending a normal halt on the shutdown channel"); - tx.send(Halted::Normal) - .map_err(|err| Error::UnableToSendHaltingMessage { err })?; - } else { - error!("shutdown was called, but the channel was missing!"); - panic!(); - }; - Ok(()) - } -} - -impl Default for HealthCheckHandle { - #[instrument(ret)] - fn default() -> Self { - trace!("setup the shutdown channel"); - let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); - - trace!("setup the axum handle"); - let axum_handle = axum_server::Handle::default(); - - trace!("setup the graceful axum meta-handler"); - let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "Health Check Server".to_string()); - - trace!("returning the new default handler"); - Self { - axum_handle: axum_server::Handle::new(), - tx_shutdown: Some(tx_shutdown), - } - } -} - -impl Handle for HealthCheckHandle { - #[instrument(ret)] - fn stop(mut self) -> Result<(), Error> { - info!("shutdown function was called"); - self.shutdown() - } - - #[instrument] - fn listening(&self) -> AddrFuture<'_> { - info!("return the listening future form the axum handler"); - self.axum_handle.listening().boxed() - } -} - -impl Drop for HealthCheckHandle { - #[instrument] - fn drop(&mut self) { - warn!("the health check handle was dropped, now shutting down"); - self.shutdown().expect("it should shutdown when dropped"); - } -} - -impl Launcher for HealthCheckLauncher { - #[instrument(err, fields(self = %self))] - fn start(self) -> Result<(TaskFuture<'static, (), Error>, HealthCheckHandle, FnSpawnServiceHeathCheck), Error> { - trace!("setup the health check handler"); - let handle = HealthCheckHandle::default(); - - trace!("make service task"); - let task: TaskFuture<'_, (), Error> = { - trace!(address = ?self.addr, "try to bind on socket"); - let listener = std::net::TcpListener::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { - addr: self.addr, - err: e.into(), - })?; - - trace!("try to get local address"); - let addr = listener - .local_addr() - .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; - info!(address = ?addr, "health tracker bound to tcp socket: {addr}"); - - trace!("setup router"); - let router = router(self.registry, addr); - - trace!("make router into service"); - let make_service = router.into_make_service_with_connect_info::(); - - info!("start and return axum service"); - axum_server::from_tcp(listener) - .handle(handle.axum_handle.clone()) - .serve(make_service) - .map_err(|e| Error::UnableToServe { err: e.into() }) - .boxed() - }; - - trace!("returning the axum task, handle, and check function closure"); - Ok((task, handle, check_fn)) - } -} - -#[instrument(fields(registry = %registry))] -fn router(registry: ServiceRegistry, addr: SocketAddr) -> axum::Router { - axum::Router::new() - .route("/", get(|| async { Json(json!({})) })) - .route("/health_check", get(health_check_handler)) - .with_state(registry) - .layer(CompressionLayer::new()) - .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) - .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id"))) - .layer( - TraceLayer::new_for_http() - .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) - .on_request( move|request: &Request, _span: &Span| { - let method = request.method().to_string(); - let uri = request.uri().to_string(); - let request_id = request - .headers() - .get("x-request-id") - .map(|v| v.to_str().unwrap_or_default()) - .unwrap_or_default(); - - tracing::span!( - target: "HEALTH CHECK API", - tracing::Level::INFO, "request", socket_addr= %addr, method = %method, uri = %uri, request_id = %request_id); - }) - .on_response(move|response: &Response, latency: Duration, _span: &Span| { - let status_code = response.status(); - let request_id = response - .headers() - .get("x-request-id") - .map(|v| v.to_str().unwrap_or_default()) - .unwrap_or_default(); - let latency_ms = latency.as_millis(); - - tracing::span!( - target: "HEALTH CHECK API", - tracing::Level::INFO, "response", socket_addr= %addr, latency = %latency_ms, status = %status_code, request_id = %request_id); - }), - ) - .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) -} diff --git a/src/servers/health_check_api/v0/handlers.rs b/src/servers/health_check_api/v0/handlers.rs new file mode 100644 index 000000000..718dd1e50 --- /dev/null +++ b/src/servers/health_check_api/v0/handlers.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::Json; +use tokio::task::JoinSet; + +use super::resources::{CheckReport, Report}; +use super::responses; +use crate::servers::registar::{HeathCheckFuture, Registration, Registry}; + +/// Endpoint for container health check. +/// +/// Creates a vector [`CheckReport`] from the input set of [`CheckJob`], and then builds a report from the results. +/// +pub(crate) async fn handle_health_check(State(registry): State>) -> Json { + #[allow(unused_assignments)] + let mut checks = JoinSet::new(); + + { + let db = registry.lock().expect("it should get a lock"); + + let mut tasks: Vec> = db.values().map(Registration::check_task).collect(); + + let aborts = tasks.drain(..).map(|c| checks.spawn(c)).collect::>(); + + // if we do not have any checks, lets return a `none` result. + if aborts.is_empty() { + return responses::none(); + } + } + + let mut results = Vec::default(); + + while let Some(r) = checks.join_next().await { + let result = r.expect("it should join task"); + + results.push(CheckReport::from(result)); + } + + if results.iter().any(CheckReport::fail) { + responses::error("health check failed".to_string(), results) + } else { + responses::ok(results) + } +} diff --git a/src/servers/health_check_api/v0/mod.rs b/src/servers/health_check_api/v0/mod.rs new file mode 100644 index 000000000..746a2f064 --- /dev/null +++ b/src/servers/health_check_api/v0/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod resources; +pub mod responses; +pub mod routes; diff --git a/src/servers/health_check_api/resources.rs b/src/servers/health_check_api/v0/resources.rs similarity index 53% rename from src/servers/health_check_api/resources.rs rename to src/servers/health_check_api/v0/resources.rs index 3302fb966..393736201 100644 --- a/src/servers/health_check_api/resources.rs +++ b/src/servers/health_check_api/v0/resources.rs @@ -2,6 +2,8 @@ use std::net::SocketAddr; use serde::{Deserialize, Serialize}; +use crate::servers::registar::{Error, HeathCheckResult, Success}; + #[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum Status { Ok, @@ -27,6 +29,37 @@ impl CheckReport { } } +impl From for CheckReport { + fn from(result: HeathCheckResult) -> Self { + let (addr, msg, result) = match result.as_ref() { + Ok(success) => { + let (addr, msg) = match success { + Success::AllGood { addr, msg } => (addr, msg), + }; + + (addr, msg, Ok(success.to_string())) + } + Err(error) => { + let (addr, msg) = match error { + Error::UnableToPreformSuccessfulHealthCheck { addr, msg } + | Error::UnableToConnectToRemote { addr, msg, .. } + | Error::UnableToPreformCheck { addr, msg, .. } + | Error::UnableToObtainGoodResponse { addr, msg, .. } + | Error::UnableToGetAnyResponse { addr, msg, .. } => (addr, msg), + }; + + (addr, msg, Err(error.to_string())) + } + }; + + Self { + binding: *addr, + info: msg.to_string(), + result, + } + } +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Report { pub status: Status, diff --git a/src/servers/health_check_api/responses.rs b/src/servers/health_check_api/v0/responses.rs similarity index 100% rename from src/servers/health_check_api/responses.rs rename to src/servers/health_check_api/v0/responses.rs diff --git a/src/servers/health_check_api/v0/routes.rs b/src/servers/health_check_api/v0/routes.rs new file mode 100644 index 000000000..e82f16541 --- /dev/null +++ b/src/servers/health_check_api/v0/routes.rs @@ -0,0 +1,58 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use axum::http::HeaderName; +use axum::Json; +use hyper::Request; +use serde_json::json; +use tower_http::compression::CompressionLayer; +use tower_http::propagate_header::PropagateHeaderLayer; +use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; +use tower_http::trace::{DefaultMakeSpan, TraceLayer}; +use tracing::{instrument, Level, Span}; + +use super::handlers::handle_health_check; +use crate::servers::registar::Registry; + +#[instrument(fields(registry = %registry))] +pub fn router(registry: Arc, addr: SocketAddr) -> axum::Router { + axum::Router::new() + .route("/", axum::routing::get(|| async { Json(json!({})) })) + .route("/health_check", axum::routing::get(handle_health_check)) + .with_state(registry) + .layer(CompressionLayer::new()) + .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id"))) + .layer( + TraceLayer::new_for_http() + .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) + .on_request( move|request: &Request, _span: &Span| { + let method = request.method().to_string(); + let uri = request.uri().to_string(); + let request_id = request + .headers() + .get("x-request-id") + .map(|v| v.to_str().unwrap_or_default()) + .unwrap_or_default(); + + tracing::span!( + target: "HEALTH CHECK API", + tracing::Level::INFO, "request", socket_addr= %addr, method = %method, uri = %uri, request_id = %request_id); + }) + .on_response(move|response: &axum::response::Response, latency: Duration, _span: &Span| { + let status_code = response.status(); + let request_id = response + .headers() + .get("x-request-id") + .map(|v| v.to_str().unwrap_or_default()) + .unwrap_or_default(); + let latency_ms = latency.as_millis(); + + tracing::span!( + target: "HEALTH CHECK API", + tracing::Level::INFO, "response", socket_addr= %addr, latency = %latency_ms, status = %status_code, request_id = %request_id); + }), + ) + .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) +} diff --git a/src/servers/http/check.rs b/src/servers/http/check.rs new file mode 100644 index 000000000..411285ec3 --- /dev/null +++ b/src/servers/http/check.rs @@ -0,0 +1,45 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use futures::{FutureExt as _, TryFutureExt as _}; +use torrust_tracker_located_error::DynError; +use tracing::info; + +use crate::servers::registar; + +/// Build a health check future task for checking +/// the http tracker health check endpoint. +/// +#[must_use] +pub(super) fn check_builder(addr: SocketAddr) -> registar::HeathCheckFuture<'static> { + let url = format!("http://{addr}/health_check"); + + info!("checking http tracker health check at: {url}"); + + let response = reqwest::get(url) + .map_err(move |e| registar::Error::UnableToGetAnyResponse { + addr, + msg: "Udp Client".to_string(), + err: DynError::into(Arc::new(e)), + }) + .boxed(); + + let check = response + .and_then(move |r| async move { + r.error_for_status() + .map_err(move |e| registar::Error::UnableToObtainGoodResponse { + addr, + msg: "Udp Client".to_string(), + err: DynError::into(Arc::new(e)), + }) + }) + .boxed(); + + check + .map_ok(move |r| registar::Success::AllGood { + addr, + msg: r.status().to_string(), + }) + .map(registar::HeathCheckResult::from) + .boxed() +} diff --git a/src/servers/http/handle.rs b/src/servers/http/handle.rs new file mode 100644 index 000000000..30774e6df --- /dev/null +++ b/src/servers/http/handle.rs @@ -0,0 +1,60 @@ +//! Module to handle the HTTP server instances. + +use futures::FutureExt; + +use crate::servers::service::{self}; +use crate::servers::signals::Halted; +use crate::servers::tcp::graceful_axum_shutdown; + +#[derive(Debug)] +pub struct Handle { + pub axum_handle: axum_server::Handle, + tx_shutdown: Option>, +} + +impl Handle { + fn shutdown(&mut self) -> Result<(), service::Error> { + let () = if let Some(tx) = self.tx_shutdown.take() { + tx.send(Halted::Normal) + .map_err(|err| service::Error::UnableToSendHaltingMessage { err })?; + } else { + panic!("it has already taken the channel?"); + }; + Ok(()) + } +} + +impl Default for Handle { + fn default() -> Self { + let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); + + let axum_handle = axum_server::Handle::default(); + + let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "HTTP service".to_string()); + + Self { + axum_handle: axum_server::Handle::new(), + tx_shutdown: Some(tx_shutdown), + } + } +} + +impl service::Handle for Handle { + fn stop(mut self) -> Result<(), service::Error> { + self.shutdown() + } + + fn listening(&self) -> service::AddrFuture<'_> { + self.axum_handle.listening().boxed() + } + + fn into_graceful_shutdown_future<'a>(self) -> futures::prelude::future::BoxFuture<'a, Result<(), service::Error>> { + todo!() + } +} + +impl Drop for Handle { + fn drop(&mut self) { + self.shutdown().expect("it should shutdown when dropped"); + } +} diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs new file mode 100644 index 000000000..2946c6fea --- /dev/null +++ b/src/servers/http/launcher.rs @@ -0,0 +1,114 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use derive_more::{Constructor, Display}; +use futures::{FutureExt as _, TryFutureExt as _}; + +use super::check::check_builder; +use super::handle::Handle; +use super::v1::routes::router; +use crate::core::Tracker; +use crate::servers::{registar, service}; + +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, with tracker, and {}", "self.have_tls()")] +pub struct Launcher { + pub tracker: Arc, + pub addr: SocketAddr, + pub tls: Option, +} + +impl Launcher { + fn have_tls(&self) -> String { + match self.tls { + Some(_) => "some", + None => "none", + } + .to_string() + } +} + +impl service::Launcher for Launcher { + fn start( + self, + ) -> Result< + ( + service::TaskFuture<'static, (), service::Error>, + Handle, + registar::HealthCheckFactory, + ), + service::Error, + > { + let handle = Handle::default(); + + let running: service::TaskFuture<'_, (), service::Error> = { + let listener = std::net::TcpListener::bind(self.addr).map_err(|e| service::Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let addr = listener + .local_addr() + .map_err(|e| service::Error::UnableToGetLocalAddress { err: e.into() })?; + + let make_service = router(self.tracker, &addr).into_make_service_with_connect_info::(); + + match self.tls.clone() { + Some(tls) => axum_server::from_tcp_rustls(listener, tls) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| service::Error::UnableToServe { err: e.into() }) + .boxed(), + + None => axum_server::from_tcp(listener) + .handle(handle.axum_handle.clone()) + .serve(make_service) + .map_err(|e| service::Error::UnableToServe { err: e.into() }) + .boxed(), + } + }; + + let check_factory = registar::HealthCheckFactory::new(check_builder); + + Ok((running, handle, check_factory)) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + + use crate::bootstrap::app::tracker; + use crate::bootstrap::jobs::make_rust_tls; + use crate::servers::http::launcher::Launcher; + use crate::servers::{registar, service}; + + #[tokio::test] + async fn it_should_be_able_to_start_and_stop() { + let cfg = Arc::new(ephemeral_mode_public()); + let tracker = tracker(&cfg); + let config = &cfg.http_trackers[0]; + + let bind_to = config + .bind_address + .parse::() + .expect("Tracker API bind_address invalid."); + + let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + .await + .map(|tls| tls.expect("tls config failed")); + + let form = ®istar::Registar::default(); + + let stopped = service::Service::new(Launcher::new(tracker, bind_to, tls)); + + let started = stopped.start().expect("it should start the server"); + let () = started.reg_form(form.form()).await.expect("it should register"); + + let stopped = started.stop().await.expect("it should stop the server"); + + drop(stopped); + } +} diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index e0b15f492..4680e99ed 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -303,10 +303,13 @@ //! //! - [Bencode](https://en.wikipedia.org/wiki/Bencode). //! - [Bencode to Json Online converter](https://chocobo1.github.io/bencode_online). + use serde::{Deserialize, Serialize}; +pub mod check; +pub mod handle; +pub mod launcher; pub mod percent_encoding; -pub mod service; pub mod v1; /// The version of the HTTP tracker. diff --git a/src/servers/http/service.rs b/src/servers/http/service.rs deleted file mode 100644 index fbd1cdade..000000000 --- a/src/servers/http/service.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Module to handle the HTTP server instances. -use std::net::SocketAddr; -use std::sync::Arc; - -use axum_server::tls_rustls::RustlsConfig; -use derive_more::{Constructor, Display}; -use futures::{FutureExt, TryFutureExt as _}; - -use super::v1::routes::router; -use crate::core::Tracker; -use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; -use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; -use crate::servers::signals::Halted; -use crate::servers::tcp::graceful_axum_shutdown; - -/// Checks the Health by connecting to the HTTP tracker endpoint. -/// -/// # Errors -/// -/// This function will return an error if unable to connect. -/// Or if the request returns an error. -#[must_use] -fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/health_check"); - - let info = format!("checking http tracker health check at: {url}"); - - let job = tokio::spawn(async move { - match reqwest::get(url).await { - Ok(response) => Ok(response.status().to_string()), - Err(err) => Err(err.to_string()), - } - }); - - ServiceHealthCheckJob::new(*binding, info, job) -} - -#[derive(Debug)] -pub struct HttpHandle { - pub axum_handle: axum_server::Handle, - tx_shutdown: Option>, -} - -impl HttpHandle { - fn shutdown(&mut self) -> Result<(), Error> { - let () = if let Some(tx) = self.tx_shutdown.take() { - tx.send(Halted::Normal) - .map_err(|err| Error::UnableToSendHaltingMessage { err })?; - } else { - panic!("it has already taken the channel?"); - }; - Ok(()) - } -} - -impl Default for HttpHandle { - fn default() -> Self { - let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); - - let axum_handle = axum_server::Handle::default(); - - let () = graceful_axum_shutdown(axum_handle.clone(), rx_shutdown, "HTTP service".to_string()); - - Self { - axum_handle: axum_server::Handle::new(), - tx_shutdown: Some(tx_shutdown), - } - } -} - -impl Handle for HttpHandle { - fn stop(mut self) -> Result<(), Error> { - self.shutdown() - } - - fn listening(&self) -> AddrFuture<'_> { - self.axum_handle.listening().boxed() - } -} - -impl Drop for HttpHandle { - fn drop(&mut self) { - self.shutdown().expect("it should shutdown when dropped"); - } -} - -#[derive(Constructor, Clone, Debug, Display)] -#[display(fmt = "intended_address: {addr}, with tracker, and {}", "self.have_tls()")] -pub struct HttpLauncher { - pub tracker: Arc, - pub addr: SocketAddr, - pub tls: Option, -} - -impl HttpLauncher { - fn have_tls(&self) -> String { - match self.tls { - Some(_) => "some", - None => "none", - } - .to_string() - } -} - -impl Launcher for HttpLauncher { - fn start(self) -> Result<(TaskFuture<'static, (), Error>, HttpHandle, FnSpawnServiceHeathCheck), Error> { - let handle = HttpHandle::default(); - - let running: TaskFuture<'_, (), Error> = { - let listener = std::net::TcpListener::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { - addr: self.addr, - err: e.into(), - })?; - - let addr = listener - .local_addr() - .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; - - let make_service = router(self.tracker, &addr).into_make_service_with_connect_info::(); - - match self.tls.clone() { - Some(tls) => axum_server::from_tcp_rustls(listener, tls) - .handle(handle.axum_handle.clone()) - .serve(make_service) - .map_err(|e| Error::UnableToServe { err: e.into() }) - .boxed(), - - None => axum_server::from_tcp(listener) - .handle(handle.axum_handle.clone()) - .serve(make_service) - .map_err(|e| Error::UnableToServe { err: e.into() }) - .boxed(), - } - }; - - Ok((running, handle, check_fn)) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; - - use crate::bootstrap::app::tracker; - use crate::bootstrap::jobs::make_rust_tls; - use crate::servers::http::service::HttpLauncher; - use crate::servers::registar::Registar; - use crate::servers::service::Service; - - #[tokio::test] - async fn it_should_be_able_to_start_and_stop() { - let cfg = Arc::new(ephemeral_mode_public()); - let tracker = tracker(&cfg); - let config = &cfg.http_trackers[0]; - - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("tls config failed")); - - let form = &Registar::default(); - - let stopped = Service::new(HttpLauncher::new(tracker, bind_to, tls)); - - let started = stopped.start().expect("it should start the server"); - let () = started.reg_form(form.give_form()).await.expect("it should register"); - - let stopped = started.stop().await.expect("it should stop the server"); - - drop(stopped); - } -} diff --git a/src/servers/registar.rs b/src/servers/registar.rs index 4237f630a..679f0098f 100644 --- a/src/servers/registar.rs +++ b/src/servers/registar.rs @@ -2,118 +2,157 @@ use std::collections::HashMap; use std::net::SocketAddr; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LockResult, Mutex, MutexGuard}; -use derive_more::{AsRef, Constructor, Display}; -use tokio::task::JoinHandle; +use derive_more::{AsRef, Constructor, DebugCustom, Display, From}; +use futures::future::BoxFuture; +use thiserror::Error; +use torrust_tracker_located_error::DynError; use tracing::debug; -/// A [`ServiceHeathCheckResult`] is returned by a completed health check. -pub type ServiceHeathCheckResult = Result; +/// A [`HeathCheckFuture`] preforms a health check when spawned. +pub type HeathCheckFuture<'a> = BoxFuture<'a, HeathCheckResult>; -/// The [`ServiceHealthCheckJob`] has a health check job with it's metadata +pub type HealthCheckBuilder = fn(SocketAddr) -> HeathCheckFuture<'static>; + +#[derive(Error, Debug, Clone)] +pub enum Error { + #[error("Failed Check: for {addr}, {msg}")] + UnableToPreformSuccessfulHealthCheck { addr: SocketAddr, msg: String }, + + #[error("Failed To Connect to {addr}, {msg}, err: {err}")] + UnableToConnectToRemote { addr: SocketAddr, msg: String, err: DynError }, + + #[error("Failed To Preform Check {addr}, {msg}, err: {err}")] + UnableToPreformCheck { addr: SocketAddr, msg: String, err: DynError }, + + #[error("Failed To Get A Successful Response {addr}, {msg}, err: {err}")] + UnableToObtainGoodResponse { addr: SocketAddr, msg: String, err: DynError }, + + #[error("Failed To Get Any Response {addr}, {msg}, err: {err}")] + UnableToGetAnyResponse { addr: SocketAddr, msg: String, err: DynError }, +} + +#[derive(Error, Debug, Display, Clone)] +pub enum Success { + #[display(fmt = "Success: {addr}, {msg}")] + AllGood { addr: SocketAddr, msg: String }, +} + +#[derive(Clone, Debug, AsRef, Display, From)] +#[display(fmt = "Result: {}", "self.get_result_display()")] +pub struct HeathCheckResult(Result); + +impl HeathCheckResult { + fn get_result_display(&self) -> String { + let result = self.as_ref().clone(); + + result.map_or_else(|e| e.to_string(), |f| format!("Ok: {f}")) + } +} + +/// The [`ServiceHealthCheck`] provides a builder that generates check futures. /// -/// The `job` awaits a [`ServiceHeathCheckResult`]. -#[derive(Debug, Constructor)] -pub struct ServiceHealthCheckJob { - pub addr: SocketAddr, - pub info: String, - pub job: JoinHandle, +#[derive(AsRef, Constructor, Clone, DebugCustom)] +#[debug(fmt = "...")] +pub struct HealthCheckFactory { + pub builder: HealthCheckBuilder, } -/// The function specification [`FnSpawnServiceHeathCheck`]. +impl HealthCheckFactory { + fn make<'a>(&self, addr: SocketAddr) -> HeathCheckFuture<'a> { + (self.builder)(addr) + } +} + +/// The [`Registration`] [`Form`] is provided to the [`Registar`] for registration. /// -/// A function fulfilling this specification will spawn a new [`ServiceHealthCheckJob`]. -pub type FnSpawnServiceHeathCheck = fn(&SocketAddr) -> ServiceHealthCheckJob; +pub type Form = tokio::sync::oneshot::Sender; -/// A [`ServiceRegistration`] is provided to the [`Registar`] for registration. +/// A [`Registration`] is provided to the [`Registar`] for registration. /// -/// Each registration includes a function that fulfils the [`FnSpawnServiceHeathCheck`] specification. -#[derive(Clone, Debug, Constructor)] -pub struct ServiceRegistration { +#[derive(Debug, Constructor)] +pub struct Registration { addr: SocketAddr, - check_fn: FnSpawnServiceHeathCheck, + check_factory: HealthCheckFactory, } -impl ServiceRegistration { +impl Registration { + /// Creates the Check Task Future + /// + /// Note: This future is not spawned yet. #[must_use] - pub fn spawn_check(&self) -> ServiceHealthCheckJob { - (self.check_fn)(&self.addr) + pub fn check_task<'a>(&self) -> HeathCheckFuture<'a> { + self.check_factory.make(self.addr) } } -/// A [`ServiceRegistrationForm`] will return a completed [`ServiceRegistration`] to the [`Registar`]. -pub type ServiceRegistrationForm = tokio::sync::oneshot::Sender; +type Db = HashMap; -/// The [`ServiceRegistry`] contains each unique [`ServiceRegistration`] by it's [`SocketAddr`]. -#[derive(AsRef, Clone, Debug, Default, Display)] +/// The [`Registry`] contains each unique [`ServiceRegistration`] by it's [`SocketAddr`]. +/// +#[derive(Default, Display)] #[display(fmt = "targets: {:?}", "self.targets()")] -pub struct ServiceRegistry { - registry: Arc>>, +pub struct Registry { + db: Mutex, } -impl ServiceRegistry { - fn targets(&self) -> Vec { - self.registry.lock().expect("it should get a lock").keys().copied().collect() +impl std::fmt::Debug for Registry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.targets()).finish() } } -/// The [`Registar`] manages the [`ServiceRegistry`]. -#[derive(Clone, Debug)] -pub struct Registar { - registry: ServiceRegistry, -} +impl Registry { + /// Returns the locked db of this [`Registry`]. + /// + /// # Errors + /// + /// This function will error if the lock is not functioning. + pub fn lock(&self) -> LockResult> { + self.db.lock() + } -impl Registar { - /// Please try to only use for testing... - #[must_use] - pub fn get_registry(&self) -> ServiceRegistry { - self.registry.clone() + fn targets(&self) -> Vec { + self.lock().expect("it should get a lock").keys().copied().collect() } } -#[allow(clippy::derivable_impls)] -impl Default for Registar { - fn default() -> Self { - Self { - registry: ServiceRegistry::default(), - } - } +/// The [`Registar`] manages the [`Registry`]. +/// +#[derive(Clone, AsRef, Debug, Display, Default)] +pub struct Registar { + registry: Arc, } impl Registar { #[must_use] - pub fn new(register: ServiceRegistry) -> Self { - Self { registry: register } + pub fn new(register: Registry) -> Self { + Self { + registry: register.into(), + } } - /// Registers a Service + /// Gets the registration form and preforms the asynchronous registration. + /// + /// # Panics + /// + /// Inside the dropped future it can panic if the receiving channel is broken, + /// or if unable to get a lock for the registry. #[must_use] - pub fn give_form(&self) -> ServiceRegistrationForm { - let (tx, rx) = tokio::sync::oneshot::channel::(); - let register = self.clone(); - tokio::spawn(async move { - register.insert(rx).await; - }); - tx - } + pub fn form(&self) -> Form { + let (tx, rx) = tokio::sync::oneshot::channel::(); - /// Inserts a listing into the registry. - async fn insert(&self, rx: tokio::sync::oneshot::Receiver) { - debug!("Waiting for the started service to send registration data ..."); + let registry = self.registry.clone(); - let service_registration = rx - .await - .expect("it should receive the service registration from the started service"); + drop(tokio::spawn(async move { + let form = rx.await.expect("it should make a form"); - let mut mutex = self.registry.as_ref().lock().expect("it should get a lock"); + let mut db = registry.db.lock().expect("it should get a lock"); - mutex.insert(service_registration.addr, service_registration); - } + db.insert(form.addr, form) + })); - /// Returns the [`ServiceRegistry`] of services - #[must_use] - pub fn entries(&self) -> ServiceRegistry { - self.registry.clone() + tx } } diff --git a/src/servers/service.rs b/src/servers/service.rs index 9899dc670..c4ece14f4 100644 --- a/src/servers/service.rs +++ b/src/servers/service.rs @@ -5,12 +5,11 @@ use std::sync::Arc; use futures::future::BoxFuture; use thiserror::Error; -use tokio::task::{JoinError, JoinHandle}; use tracing::{debug, instrument, trace}; -use super::registar::{FnSpawnServiceHeathCheck, ServiceRegistration, ServiceRegistrationForm}; +use super::registar::{Form, HealthCheckFactory, Registration}; use super::signals::Halted; -use super::udp::server::UdpError; +use super::udp; #[derive(Error, Debug, Clone)] pub enum Error { @@ -27,22 +26,22 @@ pub enum Error { err: Arc, }, #[error("Failed to send ServiceRegistration: {err:?}")] - UnableToSendRegistrationMessage { err: ServiceRegistration }, + UnableToSendRegistrationMessage { err: Arc }, #[error("Failed to send Halted: {err:?}")] UnableToSendHaltingMessage { err: Halted }, #[error("Failed to join task when stopping: {err:?}")] UnableToJoinStoppingService { err: Arc }, #[error("Failed to start udp service: {err:?}")] - UnableToStartUdpService { err: UdpError }, + UnableToStartUdpService { err: udp::Error }, #[error("Failed to join the tokio task: {err:?}")] - UnableJoinTokioTask { err: Arc }, + UnableJoinTokioTask { err: Arc }, } pub type AddrFuture<'a> = BoxFuture<'a, Option>; pub type TaskFuture<'a, T, E> = BoxFuture<'a, Result>; -pub type TaskHandle = JoinHandle>; +pub type TaskHandle = tokio::task::JoinHandle>; pub trait Handle: Debug + Default + Send + 'static { /// Stops the Service @@ -51,8 +50,18 @@ pub trait Handle: Debug + Default + Send + 'static { /// /// This function will return an error if the service fails to stop cleanly. /// + /// Stops the service. + /// fn stop(self) -> Result<(), Error>; + /// Shutdown the service gracefully when the future is spawned. + /// + /// Note: the task returns when shutdown has completed. + /// + /// # Errors: if the shutdown fails, the future will return an error. + /// + fn into_graceful_shutdown_future<'a>(self) -> BoxFuture<'a, Result<(), Error>>; + fn listening(&self) -> AddrFuture<'_>; } @@ -67,7 +76,7 @@ where /// This function will return an error if the launching fails, /// or when the future returns with an error. /// - fn start(self) -> Result<(TaskFuture<'static, (), Error>, H, FnSpawnServiceHeathCheck), Error>; + fn start(self) -> Result<(TaskFuture<'static, (), Error>, H, HealthCheckFactory), Error>; } /// A service instance controller. @@ -112,16 +121,20 @@ where { task: TaskHandle<(), Error>, pub handle: H, - check_fn: FnSpawnServiceHeathCheck, + check_factory: HealthCheckFactory, } impl Started { #[instrument(skip(task))] - pub fn new(task: TaskFuture<'static, (), Error>, handle: H, check_fn: FnSpawnServiceHeathCheck) -> Self { + pub fn new(task: TaskFuture<'static, (), Error>, handle: H, check_factory: HealthCheckFactory) -> Self { debug!("spawning the task in tokio"); let task: TaskHandle<(), Error> = tokio::task::spawn(task); - Self { task, handle, check_fn } + Self { + task, + handle, + check_factory, + } } } @@ -153,7 +166,7 @@ impl + Send + 'static> Service { /// /// It would return an error if the underling launcher returns an error. /// - #[instrument(err, ret)] + #[instrument(err)] pub fn start(self) -> Result, L, H>, Error> { trace!("starting the task"); let (task, handle, check_fn) = self.launcher.clone().start()?; @@ -174,7 +187,7 @@ impl<'a, H: Handle, L: Launcher + Send + 'a> Service, L, H> { /// /// This function will return an error if unable to get address. /// - #[instrument(err, ret)] + #[instrument(err)] pub async fn listening(&self) -> Result { trace!("awaiting the service to inform it's ready and listening"); self.state @@ -192,7 +205,7 @@ impl<'a, H: Handle, L: Launcher + Send + 'a> Service, L, H> { /// or unable to complete the registration. /// #[instrument(err, ret, skip(form))] - pub async fn reg_form(&self, form: ServiceRegistrationForm) -> Result<(), Error> { + pub async fn reg_form(&self, form: Form) -> Result<(), Error> { trace!("awaiting for the service to be ready and return it's local address"); let addr = self.listening().await?; @@ -200,8 +213,8 @@ impl<'a, H: Handle, L: Launcher + Send + 'a> Service, L, H> { "sends the service registration on the supplied form, with the local address and self-check closure" ); - form.send(ServiceRegistration::new(addr, self.state.check_fn)) - .map_err(|err| Error::UnableToSendRegistrationMessage { err }) + form.send(Registration::new(addr, self.state.check_factory.clone())) + .map_err(|e| Error::UnableToSendRegistrationMessage { err: e.into() }) } /// It returns the active task with it's handler. @@ -238,7 +251,7 @@ impl<'a, H: Handle, L: Launcher + Send + 'a> Service, L, H> { /// # Errors /// /// It would return an error if unable to stop, of the task finished with an error. - #[instrument(err, ret)] + #[instrument(err)] pub async fn stop(self) -> Result, Error> { let () = self.state.handle.stop()?; let () = self diff --git a/src/servers/signals.rs b/src/servers/signals.rs index 956fe3079..86ee70eb9 100644 --- a/src/servers/signals.rs +++ b/src/servers/signals.rs @@ -1,6 +1,8 @@ //! This module contains functions to handle signals. use derive_more::Display; +use futures::future::BoxFuture; +use futures::FutureExt; use tracing::info; /// This is the message that the "launcher" spawned task receives from the main @@ -12,29 +14,53 @@ pub enum Halted { Dropped, } -/// Resolves on `ctrl_c` or the `terminate` signal. +/// Creates a Future to Await the Terminate Signal (unix only) /// /// # Panics /// -/// Will panic if the `ctrl_c` or `terminate` signal resolves with an error. -pub async fn global_shutdown_signal() { - let ctrl_c = async { - tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); - }; - +/// Panics if unable to connect to the global signal handle. +/// +#[must_use] +pub fn global_terminate_signal<'a>() -> BoxFuture<'a, ()> { #[cfg(unix)] - let terminate = async { + let terminate: BoxFuture<'a, ()> = async { tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; - }; + } + .boxed(); #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); + let terminate: BoxFuture<'a, ()> = std::future::pending::<()>().boxed(); + + terminate +} + +/// Creates a Future to Await the Interrupt, i.e `ctrl_c` Signal +/// +/// # Panics +/// +/// Panics if unable to connect to the global signal handle. +/// +#[must_use] +pub fn global_interrupt_signal<'a>() -> BoxFuture<'a, ()> { + let interrupt: BoxFuture<'a, ()> = async { + tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); + } + .boxed(); + + interrupt +} + +/// Resolves on `ctrl_c` or the `terminate` signal. +/// +pub async fn global_shutdown_signal() { + let interrupt = global_interrupt_signal(); + let terminate = global_terminate_signal(); tokio::select! { - () = ctrl_c => {}, + () = interrupt => {}, () = terminate => {} } } @@ -43,7 +69,8 @@ pub async fn global_shutdown_signal() { /// /// # Panics /// -/// Will panic if the `stop_receiver` resolves with an error. +/// Will panic if unable to connect to the receiving channel. +/// pub async fn shutdown_signal(rx_halt: tokio::sync::oneshot::Receiver) { let halt = async { match rx_halt.await { diff --git a/src/servers/udp/check.rs b/src/servers/udp/check.rs new file mode 100644 index 000000000..ecaa3bbb4 --- /dev/null +++ b/src/servers/udp/check.rs @@ -0,0 +1,35 @@ +use std::net::SocketAddr; + +use futures::{FutureExt as _, TryFutureExt as _}; +use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; +use tracing::info; + +use crate::servers::registar; +use crate::shared::bit_torrent::tracker::udp::Client; + +pub fn build(addr: SocketAddr) -> registar::HeathCheckFuture<'static> { + info!("checking udp: {addr}"); + + let client = Client::connect(addr, CLIENT_TIMEOUT_DEFAULT) + .map_err(move |e| registar::Error::UnableToConnectToRemote { + addr, + msg: "Udp Client".to_string(), + err: e.into(), + }) + .boxed(); + + let check = client + .and_then(move |c| { + c.check().map_err(move |e| registar::Error::UnableToPreformCheck { + addr, + msg: "Udp Client".to_string(), + err: e.into(), + }) + }) + .boxed(); + + check + .map_ok(move |msg| registar::Success::AllGood { addr, msg }) + .map(registar::HeathCheckResult::from) + .boxed() +} diff --git a/src/servers/udp/handle.rs b/src/servers/udp/handle.rs index 42bae2e1a..804273c6b 100644 --- a/src/servers/udp/handle.rs +++ b/src/servers/udp/handle.rs @@ -1,167 +1,273 @@ -//! Copy-Paste from axum. Thanks. - use std::net::SocketAddr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use std::thread::sleep; use std::time::Duration; -use tokio::sync::Notify; -use tokio::time::sleep; +use futures::future::BoxFuture; +use futures::FutureExt as _; +use tracing::{error, info, trace, warn}; + +use crate::servers::service; +use crate::servers::signals::{global_interrupt_signal, global_terminate_signal}; +use crate::shared::handle::{Handle as InnerHandle, NotifyOnce}; -/// A handle for [`Server`](crate::server::Server). -#[derive(Clone, Debug, Default)] +#[derive(Debug)] pub struct Handle { - inner: Arc, + inner: InnerHandle, + abort: Vec, + shutdown: Arc, + graceful: Arc, + conn_end: Arc, } -#[derive(Debug, Default)] -struct HandleInner { - addr: Mutex>, - addr_notify: Notify, - conn_count: AtomicUsize, - shutdown: NotifyOnce, - graceful: NotifyOnce, - graceful_dur: Mutex>, - conn_end: NotifyOnce, -} +impl Default for Handle { + fn default() -> Self { + let inner = InnerHandle::default(); -impl Handle { - /// Create a new handle. - #[must_use] - pub fn new() -> Self { - Self::default() + Self::new(inner) } +} - /// Get the number of connections. - #[must_use] - pub fn connection_count(&self) -> usize { - self.inner.conn_count.load(Ordering::SeqCst) +impl service::Handle for Handle { + fn stop(self) -> Result<(), service::Error> { + warn!("now triggering the udp tracker direct-shutdown process!"); + self.inner.shutdown(); + Ok(()) } - /// Shutdown the server. - pub fn shutdown(&self) { - self.inner.shutdown.notify_waiters(); + fn into_graceful_shutdown_future<'a>(self) -> BoxFuture<'a, Result<(), service::Error>> { + todo!(); } - /// Gracefully shutdown the server. - /// - /// `None` means indefinite grace period. - /// - #[allow(clippy::missing_panics_doc)] - pub fn graceful_shutdown(&self, duration: Option) { - *self.inner.graceful_dur.lock().unwrap() = duration; - - self.inner.graceful.notify_waiters(); + fn listening(&self) -> service::AddrFuture<'_> { + self.inner.listening().boxed() } +} - /// Returns local address and port when server starts listening. - /// - /// Returns `None` if server fails to bind. - /// - #[allow(clippy::missing_panics_doc)] - pub async fn listening(&self) -> Option { - let notified = self.inner.addr_notify.notified(); - - if let Some(addr) = *self.inner.addr.lock().unwrap() { - return Some(addr); - } - - notified.await; - - *self.inner.addr.lock().unwrap() +impl Drop for Handle { + fn drop(&mut self) { + warn!("triggering the direct-shutdown from the drop handler!"); + self.inner.shutdown(); + self.shutdown.notify_waiters(); + self.abort.iter().for_each(tokio::task::AbortHandle::abort); } +} - pub(crate) fn notify_listening(&self, addr: Option) { - *self.inner.addr.lock().unwrap() = addr; +#[allow(dead_code)] +enum Task { + Terminate, + Interrupt, + Shutdown, + Graceful, + Listening(Option), +} - self.inner.addr_notify.notify_waiters(); +impl Handle { + #[must_use] + pub fn new(inner: InnerHandle) -> Self { + let mut abort: Vec = Vec::default(); + let shutdown: Arc = Arc::default(); + let graceful: Arc = Arc::default(); + let conn_end: Arc = Arc::default(); + + abort.push(tokio::task::spawn(Self::notify_shutdown(inner.clone(), shutdown.clone())).abort_handle()); + abort.push(tokio::task::spawn(Self::notify_graceful(inner.clone(), graceful.clone())).abort_handle()); + abort.push(tokio::task::spawn(Self::notify_conn_end(inner.clone(), conn_end.clone())).abort_handle()); + + let mut handle = Handle { + inner, + abort, + shutdown, + graceful, + conn_end, + }; + + drop(handle.graceful_init()); + + handle } - pub(crate) fn watcher(&self) -> Watcher { - Watcher::new(self.clone()) + async fn notify_shutdown(inner: InnerHandle, shutdown: Arc) { + inner.wait_shutdown().await; + shutdown.notify_waiters(); } - pub(crate) async fn wait_shutdown(&self) { - self.inner.shutdown.notified().await; + async fn notify_graceful(inner: InnerHandle, graceful: Arc) { + inner.wait_graceful_shutdown().await; + graceful.notify_waiters(); } - pub(crate) async fn wait_graceful_shutdown(&self) { - self.inner.graceful.notified().await; + async fn notify_conn_end(inner: InnerHandle, conn_end: Arc) { + inner.wait_connections_end().await; + conn_end.notify_waiters(); } - #[allow(dead_code)] - pub(crate) async fn wait_connections_end(&self) { - if self.inner.conn_count.load(Ordering::SeqCst) == 0 { - return; - } - - let deadline = *self.inner.graceful_dur.lock().unwrap(); + fn graceful_init(&mut self) -> tokio::task::JoinSet<()> { + let handle = self.inner.clone(); + let shutdown = self.shutdown.clone(); + let graceful = self.graceful.clone(); + let conn_end = self.conn_end.clone(); - match deadline { - Some(duration) => tokio::select! { - biased; - () = sleep(duration) => self.shutdown(), - () = self.inner.conn_end.notified() => (), - }, - None => self.inner.conn_end.notified().await, - } - } -} + let mut jobs = tokio::task::JoinSet::default(); -#[derive(Debug)] -pub(crate) struct Watcher { - handle: Handle, -} + self.abort + .push(jobs.spawn(Self::graceful_start(handle.clone(), shutdown.clone(), graceful.clone()))); + self.abort + .push(jobs.spawn(Self::shutdown_loop(handle.clone(), shutdown.clone(), graceful.clone()))); + self.abort + .push(jobs.spawn_blocking(move || Self::graceful_exit_loop(&handle, &shutdown, &graceful, &conn_end))); -impl Watcher { - fn new(handle: Handle) -> Self { - handle.inner.conn_count.fetch_add(1, Ordering::SeqCst); - - Self { handle } + jobs } - #[allow(dead_code)] - pub(crate) async fn wait_graceful_shutdown(&self) { - self.handle.wait_graceful_shutdown().await; + async fn graceful_start(handle: InnerHandle, shutdown: Arc, graceful: Arc) { + let handle = handle.clone(); + + let shutdown_wait = shutdown.notified(); + let graceful_wait = graceful.notified(); + let listening_wait = handle.listening(); + + let start: Task = tokio::select! { + biased; + () = global_terminate_signal() => { Task::Terminate} + () = global_interrupt_signal() => { Task::Interrupt} + () = shutdown_wait => { Task::Shutdown} + () = graceful_wait => { Task::Graceful} + maybe_socket = listening_wait => { Task::Listening(maybe_socket)} + }; + + match start { + Task::Listening(maybe_socket) => { + if let Some(addr) = maybe_socket { + info!("Listening to: {addr}"); + } else { + warn!("Failed to start Listening"); + handle.shutdown(); + } + } + + Task::Terminate | Task::Interrupt => { + warn!("Global Signal Before Listening"); + handle.shutdown(); + } + + Task::Shutdown | Task::Graceful => { + warn!("Local Signal Before Listening"); + handle.shutdown(); + } + } } - pub(crate) async fn wait_shutdown(&self) { - self.handle.wait_shutdown().await; - } -} + async fn shutdown_loop(handle: InnerHandle, shutdown: Arc, graceful: Arc) { + const GRACEFUL_DURATION: Duration = Duration::from_secs(90); -impl Drop for Watcher { - fn drop(&mut self) { - let count = self.handle.inner.conn_count.fetch_sub(1, Ordering::SeqCst) - 1; + loop { + let handle = handle.clone(); + let shutdown = shutdown.clone(); + let graceful = graceful.clone(); - if count == 0 && self.handle.inner.graceful.is_notified() { - self.handle.inner.conn_end.notify_waiters(); + let shutdown_wait = shutdown.notified(); + + let exit: Task = tokio::select! { + biased; + () = global_terminate_signal() => { Task::Terminate} + () = global_interrupt_signal() => { Task::Interrupt} + () = shutdown_wait => { Task::Shutdown} + }; + + let () = match exit { + Task::Interrupt => { + if graceful.is_notified() { + error! {"interrupting graceful shutdown... exiting now!"}; + handle.shutdown(); + + break; + } + warn! {"caught interrupt: starting graceful shutdown... for {} sec..", GRACEFUL_DURATION.as_secs()}; + handle.graceful_shutdown(Some(GRACEFUL_DURATION)); + + continue; + } + + Task::Terminate => { + warn!("Got Terminate: Exiting Now!"); + handle.shutdown(); + + return; + } + Task::Shutdown => { + info!("Received Shutdown, Closing Service."); + return; + } + _ => unreachable!(), + }; } } -} - -#[derive(Debug, Default)] -pub(crate) struct NotifyOnce { - notified: AtomicBool, - notify: Notify, -} -impl NotifyOnce { - pub(crate) fn notify_waiters(&self) { - self.notified.store(true, Ordering::SeqCst); + fn graceful_exit_loop( + handle: &InnerHandle, + shutdown: &Arc, + graceful: &Arc, + conn_end: &Arc, + ) { + #[allow(clippy::useless_format)] + for n in 1_u64.. { + let listening_to = if let Some(addr) = handle.listening().now_or_never().flatten() { + format!("listening to: {addr}") + } else { + format!("(without listener)") + }; + + match (shutdown.is_notified(), graceful.is_notified()) { + (true, _) => { + error!("halting service {listening_to} without graceful shutdown!"); + return; + } + (false, true) => { + info!( + " + after running for {n} seconds... + now gracefully shutting down udp service {listening_to}... + + to cancel and exit now interrupt with \"ctrl+c\" + " + ); + break; + } + (false, false) => { + trace!("udp service {listening_to} tick no: {n}"); + sleep(Duration::from_millis(1000)); + continue; + } + } + } - self.notify.notify_waiters(); - } + for n in 1_u64.. { + if shutdown.is_notified() { + error!("graceful shutdown interrupted, exiting now!"); + return; + } - pub(crate) fn is_notified(&self) -> bool { - self.notified.load(Ordering::SeqCst) - } + info!( + "shutting down for ~{n} seconds, with {} remaining connections...", + handle.connection_count() + ); - pub(crate) async fn notified(&self) { - let future = self.notify.notified(); + if conn_end.is_notified() { + break; + }; - if !self.notified.load(Ordering::SeqCst) { - future.await; + sleep(Duration::from_secs(1)); } + + match handle.connection_count() { + 0 => { + info!("successfully finished shutting down udp service (:"); + } + + n => { + warn!("shutting down udp service timed out with {n} active connections"); + } + }; } } diff --git a/src/servers/udp/launcher.rs b/src/servers/udp/launcher.rs new file mode 100644 index 000000000..3a0787bf2 --- /dev/null +++ b/src/servers/udp/launcher.rs @@ -0,0 +1,52 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use derive_more::{Constructor, Display}; +use futures::{FutureExt as _, TryFutureExt as _}; +use tracing::instrument; + +use super::check::build; +use super::handle::Handle; +use super::server::Udp; +use crate::core::Tracker; +use crate::servers::{registar, service}; + +#[derive(Constructor, Clone, Debug, Display)] +#[display(fmt = "intended_address: {addr}, with tracker")] +pub struct Launcher { + pub tracker: Arc, + pub addr: SocketAddr, +} + +impl service::Launcher for Launcher { + #[instrument(err)] + fn start( + self, + ) -> Result< + ( + service::TaskFuture<'static, (), service::Error>, + Handle, + registar::HealthCheckFactory, + ), + service::Error, + > { + let std_socket = std::net::UdpSocket::bind(self.addr).map_err(|e| service::Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let socket = tokio::net::UdpSocket::from_std(std_socket).map_err(|e| service::Error::UnableToBindToSocket { + addr: self.addr, + err: e.into(), + })?; + + let (task, handle) = + Udp::make_task(self.tracker, socket.into()).map_err(|err| service::Error::UnableToStartUdpService { err })?; + + let task = task.map_err(|err| service::Error::UnableToStartUdpService { err }).boxed(); + + let check_factory = registar::HealthCheckFactory::new(build); + + Ok((task, handle, check_factory)) + } +} diff --git a/src/servers/udp/logging.rs b/src/servers/udp/logging.rs index 13a7b3eeb..5901d631c 100644 --- a/src/servers/udp/logging.rs +++ b/src/servers/udp/logging.rs @@ -6,7 +6,7 @@ use std::time::Duration; use aquatic_udp_protocol::{Request, Response, TransactionId}; use torrust_tracker_primitives::info_hash::InfoHash; -use super::handlers::RequestId; +use super::v0::handlers::RequestId; pub fn log_request(request: &Request, request_id: &RequestId, server_socket_addr: &SocketAddr) { let action = map_action_name(request); diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index 9c5dac58e..e6036d2bc 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -640,19 +640,18 @@ //! taken from the [libtorrent](https://www.rasterbar.com/products/libtorrent/udp_tracker_protocol.html). use std::net::SocketAddr; +use std::sync::Arc; -pub mod connection_cookie; -pub mod error; +use thiserror::Error; + +pub mod check; pub mod handle; -pub mod handlers; +pub mod launcher; pub mod logging; pub mod peer_builder; pub mod request; pub mod server; - -pub use handle::Handle; - -use self::handle::Watcher; +pub mod v0; /// Number of bytes. pub type Bytes = u64; @@ -668,17 +667,38 @@ pub enum Version { V0, } -pub(crate) struct UdpRequest { - payload: Vec, - from: SocketAddr, - watcher: Watcher, +#[derive(Error, Debug, Clone)] +pub enum Error { + #[error("Gracefully Canceled Processing Requests For: {addr}")] + GracefullyCanceled { addr: SocketAddr }, + #[error("Stopped Processing Requests For: {addr}")] + Stopped { addr: SocketAddr }, + #[error("Canceled Sending Response to Target: {target}")] + CanceledSending { target: SocketAddr }, + + #[error("Failed to Join Tokio Task: {err}")] + UnableToJoinTokioTask { err: Arc }, + + #[error("Stopped before starting")] + StopBeforeStarting {}, + + #[error("Failed to get Local Address from Socket: {err:?}")] + UnableToGetLocalAddress { err: Arc }, + + #[error("Failed to get Listening Address from Socket")] + UnableToGetListeningAddress {}, + #[error("Socket Errored when waiting to read from: {addr}, with error: {err:?}")] + UnableToGetReadableSocket { addr: SocketAddr, err: Arc }, + #[error("Socket Errored when reading from: {addr}, with error: {err:?}")] + UnableToReadFromSocket { addr: SocketAddr, err: Arc }, + #[error("Socket Errored when sending to: {target}, with error: {err:?}")] + UnableToSendToSocket { target: SocketAddr, err: Arc }, + #[error("Socket Errored when writing to: {target}, with error: {err:?}")] + UnableToWriteToSocket { target: SocketAddr, err: Arc }, } -impl std::fmt::Debug for UdpRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("UdpRequest") - .field("payload", &self.payload) - .field("from", &self.from) - .finish_non_exhaustive() +impl From for Error { + fn from(e: tokio::task::JoinError) -> Self { + Self::UnableToJoinTokioTask { err: e.into() } } } diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index cb6036d60..074d48ea1 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -21,140 +21,21 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; -use derive_more::{Constructor, Display}; use futures::future::BoxFuture; -use futures::{FutureExt, TryFutureExt as _}; +use futures::FutureExt; use ringbuf::{Rb, StaticRb}; -use thiserror::Error; use tokio::net::UdpSocket; use tokio::select; -use tokio::task::{AbortHandle, JoinHandle}; -use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE}; -use tracing::{debug, error, info, instrument, trace, Level}; +use tokio::task::AbortHandle; +use torrust_tracker_configuration::MAX_PACKET_SIZE; +use tracing::{debug, error, info, instrument, trace, warn, Level}; -use super::handle::Watcher; -use super::UdpRequest; +use super::v0::UdpRequest; +use super::Error; use crate::core::Tracker; -use crate::servers::registar::{FnSpawnServiceHeathCheck, ServiceHealthCheckJob}; -use crate::servers::service::{AddrFuture, Error, Handle, Launcher, TaskFuture}; -use crate::servers::signals::{shutdown_signal_with_message, Halted}; -use crate::servers::udp::handlers; -use crate::shared::bit_torrent::tracker::udp::Client; - -#[derive(Error, Debug, Clone)] -pub enum UdpError { - #[error("Gracefully Canceled Processing Requests For: {addr}")] - GracefullyCanceled { addr: SocketAddr }, - #[error("Canceled Sending Response to Target: {target}")] - CanceledSending { target: SocketAddr }, - - #[error("Failed to get Local Address from Socket: {err:?}")] - UnableToGetLocalAddress { err: Arc }, - - #[error("Failed to get Listening Address from Socket")] - UnableToGetListeningAddress {}, - #[error("Socket Errored when waiting to read from: {addr}, with error: {err:?}")] - UnableToGetReadableSocket { addr: SocketAddr, err: Arc }, - #[error("Socket Errored when reading from: {addr}, with error: {err:?}")] - UnableToReadFromSocket { addr: SocketAddr, err: Arc }, - #[error("Socket Errored when sending to: {target}, with error: {err:?}")] - UnableToSendToSocket { target: SocketAddr, err: Arc }, - #[error("Socket Errored when writing to: {target}, with error: {err:?}")] - UnableToWriteToSocket { target: SocketAddr, err: Arc }, -} - -fn check_fn(&addr: &SocketAddr) -> ServiceHealthCheckJob { - let info = format!("checking the udp tracker health check at: {addr}"); - - let job = tokio::spawn( - Client::connect(addr, CLIENT_TIMEOUT_DEFAULT) - .and_then(Client::check) - .map_err(|e| e.to_string()), - ); - - ServiceHealthCheckJob::new(addr, info, job) -} - -#[derive(Debug)] -pub struct UdpHandle { - tx_shutdown: Option>, - pub udp_handle: super::handle::Handle, -} - -impl UdpHandle { - #[instrument(err, ret)] - fn shutdown(&mut self) -> Result<(), Error> { - if let Some(tx) = self.tx_shutdown.take() { - tx.send(Halted::Normal) - .map_err(|err| Error::UnableToSendHaltingMessage { err })?; - } else { - panic!("it has already taken the channel?"); - }; - Ok(()) - } -} - -impl Default for UdpHandle { - fn default() -> Self { - let (tx_shutdown, rx_shutdown) = tokio::sync::oneshot::channel::(); - - let udp_handle = super::handle::Handle::default(); - - let () = Udp::graceful_udp_shutdown(udp_handle.clone(), rx_shutdown, "UDP service".to_string()); - - Self { - udp_handle: super::handle::Handle::new(), - tx_shutdown: Some(tx_shutdown), - } - } -} - -impl Handle for UdpHandle { - fn stop(mut self) -> Result<(), Error> { - self.shutdown() - } - - fn listening(&self) -> AddrFuture<'_> { - self.udp_handle.listening().boxed() - } -} - -impl Drop for UdpHandle { - fn drop(&mut self) { - self.shutdown().expect("it should shutdown when dropped"); - } -} - -#[derive(Constructor, Clone, Debug, Display)] -#[display(fmt = "intended_address: {addr}, with tracker")] -pub struct UdpLauncher { - pub tracker: Arc, - pub addr: SocketAddr, -} - -impl Launcher for UdpLauncher { - #[instrument(err)] - fn start(self) -> Result<(TaskFuture<'static, (), Error>, UdpHandle, FnSpawnServiceHeathCheck), Error> { - let handle = UdpHandle::default(); - - let std_socket = std::net::UdpSocket::bind(self.addr).map_err(|e| Error::UnableToBindToSocket { - addr: self.addr, - err: e.into(), - })?; - - let socket = UdpSocket::from_std(std_socket).map_err(|e| Error::UnableToBindToSocket { - addr: self.addr, - err: e.into(), - })?; - - let task = Udp::run_udp_server(self.tracker, socket, handle.udp_handle.clone()) - .map_err(|err| Error::UnableToStartUdpService { err })? - .map_err(|err| Error::UnableToStartUdpService { err }) - .boxed(); - - Ok((task, handle, check_fn)) - } -} +use crate::servers::signals::{global_interrupt_signal, global_shutdown_signal, global_terminate_signal}; +use crate::servers::udp::v0::handlers; +use crate::shared::handle::{Handle, Watcher}; #[derive(Default)] struct ActiveRequests { @@ -180,171 +61,381 @@ impl Drop for ActiveRequests { } /// A UDP server instance launcher. -#[derive(Constructor)] -pub(super) struct Udp {} +pub(super) struct Udp { + pub handle: Handle, + tracker: Arc, + socket: Arc, + addr: SocketAddr, + reqs: ActiveRequests, +} -impl Udp { - #[instrument(err)] - pub fn run_udp_server( - tracker: Arc, - socket: UdpSocket, - handle: super::Handle, - ) -> Result>, UdpError> { - let socket = Arc::new(socket); +enum Task { + BeforeListening, + Terminate, + Interrupt, + Shutdown, + Graceful, + Listening(Option), + Remaining, + Finished, + // Notified(Result), + // Ready(Result), + Readable(std::io::Result<()>), +} +impl Udp { + fn new(tracker: &Arc, socket: &Arc, handle: &Handle) -> Result { let addr = socket .local_addr() - .map_err(|e| UdpError::UnableToGetLocalAddress { err: e.into() })?; + .map_err(|e| Error::UnableToGetLocalAddress { err: e.into() })?; + + Ok(Self { + tracker: tracker.clone(), + socket: socket.clone(), + handle: handle.clone(), + reqs: ActiveRequests::default(), + addr, + }) + } + + async fn run(self) -> Result<(), Error> { + { + let listen_job = tokio::task::spawn(Self::wait_listening(self.handle.clone())); + + { + let notify = match Self::notify_listening(&self.handle, &self.socket) { + Ok(addr) => addr, + Err(e) => { + error!("failed to get active socket, aborting..."); + self.handle.shutdown(); + return Err(e); + } + }; - let () = handle.notify_listening(Some(addr)); + let listening = listen_job.await??; - Ok(async move { - let reqs = &mut ActiveRequests::default(); + assert_eq!(notify, listening, "it should never get a different socket"); + } + }; + + let () = tokio::task::spawn_local(self.main_loop()).await??; + + Ok(()) + } - // Main Waiting Loop, awaits on async [`receive_request`]. + fn main_loop<'a>(mut self) -> BoxFuture<'a, Result<(), Error>> { + async move { + let mut task: Task; loop { - let Ok(request) = Self::receive_request(socket.clone(), handle.clone()).await else { - break; - }; + let tracker = self.tracker.clone(); + let socket = self.socket.clone(); + + trace!("await readable socket"); + { + task = select! { + biased; + () = self.handle.wait_shutdown() => Task::Shutdown, + r = socket.readable() => Task::Readable(r), + () = self.handle.wait_graceful_shutdown() => Task::Graceful, + }; + } + + trace!("brake if interrupted"); + { + let () = match task { + Task::Shutdown | Task::Graceful => { + debug!("(shutting down): stop processing new requests"); + break; + } + Task::Readable(r) => r.map_err(|e| Error::UnableToGetReadableSocket { + addr: self.addr, + err: e.into(), + })?, + _ => unreachable!(), + }; + } - if let Some(h) = reqs - .rb - .push_overwrite(Self::spawn_request_processor(request, tracker.clone(), socket.clone()).abort_handle()) + trace!("read socket and fulfil request"); { - if !h.is_finished() { - // the task is still running, lets yield and give it a chance to flush. - let () = tokio::task::yield_now().await; - h.abort(); + trace!("receive request"); + let request = self.receive_request().await?; + + trace!("create new job to respond to {}", request); + let respond_job = + tokio::task::spawn(fulfil_request(tracker, socket, self.addr, request, self.handle.watcher())); + + trace!("limit number of active jobs"); + if let Some(h) = self.reqs.rb.push_overwrite(respond_job.abort_handle()) { + if !h.is_finished() { + debug!("job was still active and will be aborted"); + let () = tokio::task::yield_now().await; // yield to give it a chance... + h.abort(); + } } } } + let () = match task { + Task::Shutdown => { + warn!("main loop interrupted!"); + } + Task::Graceful => { + info!("main_loop finished, now wait for all connections to complete.."); - let () = tokio::task::yield_now().await; + let () = tokio::task::yield_now().await; - let () = handle.wait_connections_end().await; + let () = self.handle.wait_connections_end().await; + } + Task::Readable(_) => unreachable!("it should not return readable"), + _ => unreachable!(), + }; Ok(()) } - .boxed()) + .boxed() } - #[instrument(err, ret)] - async fn receive_request(socket: Arc, handle: super::Handle) -> Result { - let addr = handle - .listening() - .await - .ok_or_else(|| UdpError::UnableToGetListeningAddress {})?; - - let () = select! { - () = handle.wait_graceful_shutdown() => Err(UdpError::GracefullyCanceled { addr }), - r = socket.readable() => r.map_err(|e| UdpError::UnableToGetReadableSocket { addr ,err: e.into() }), - }?; - - let mut buf = Vec::with_capacity(MAX_PACKET_SIZE); - - let (n, from) = socket - .recv_buf_from(&mut buf) - .await - .map_err(|e| UdpError::UnableToReadFromSocket { addr, err: e.into() })?; - - Vec::truncate(&mut buf, n); - trace!("GOT {buf:?}"); - Ok(UdpRequest { - payload: buf, - from, - watcher: handle.watcher(), - }) - } + fn notify_listening(handle: &Handle, socket: &UdpSocket) -> Result { + trace!("try to get the local socket address to notify listeners"); - #[instrument(level = Level::TRACE,)] - fn spawn_request_processor(request: UdpRequest, tracker: Arc, socket: Arc) -> JoinHandle<()> { - tokio::task::spawn(Self::process_request(request, tracker, socket)) + match socket.local_addr() { + Ok(addr) => { + info!("the socket at: {addr} is ready"); + handle.notify_listening(Some(addr)); + Ok(addr) + } + Err(e) => { + error!("the socket is not available: {e}"); + handle.notify_listening(None); + Err(Error::UnableToGetLocalAddress { err: e.into() }) + } + } } - #[instrument(level = Level::TRACE,)] - async fn process_request<'a>(request: UdpRequest, tracker: Arc, socket: Arc) { - select! { - _ = Self::process_valid_request(tracker.clone(), socket.clone(), &request) => (), - () = request.watcher.wait_shutdown() => () + #[instrument(skip(handle))] + async fn wait_listening(handle: Handle) -> Result { + let wait = tokio::select! { + maybe = handle.listening() => {Task::Listening(maybe)} + () = handle.wait_shutdown() => { Task::Shutdown} + () = handle.wait_graceful_shutdown() => { Task::Graceful} }; - } - - #[instrument(err, ret)] - async fn process_valid_request(tracker: Arc, socket: Arc, request: &UdpRequest) -> Result<(), UdpError> { - trace!("Making Response to {request:?}"); - let target = request.from; - let response = handlers::handle_packet(request, &tracker.clone(), socket.clone()).await; - let _ = Self::send_response(&socket.clone(), target, response, &request.watcher).await?; - Ok(()) + match wait { + Task::Shutdown | Task::Graceful => { + warn!("shutdown called before listening"); + Err(Error::StopBeforeStarting {}) + } + Task::Listening(maybe) => { + if let Some(addr) = maybe { + debug!("socket ready and listening to: {addr}"); + Ok(addr) + } else { + error!("failed to open socket!"); + Err(Error::UnableToGetListeningAddress {}) + } + } + _ => unreachable!(), + } } - #[instrument(err, ret)] - async fn send_response( - socket: &Arc, - target: SocketAddr, - response: aquatic_udp_protocol::Response, - watcher: &Watcher, - ) -> Result { - trace!("Sending Response: {response:?} to: {target:?}"); - - let buffer = vec![0u8; MAX_PACKET_SIZE]; - let mut cursor = std::io::Cursor::new(buffer); + #[instrument(err)] + pub fn make_task( + tracker: Arc, + socket: Arc, + ) -> Result<(BoxFuture<'static, Result<(), Error>>, super::handle::Handle), Error> { + let handle = Handle::default(); + let server = Udp::new(&tracker, &socket, &handle)?; + let udp_handle = super::handle::Handle::new(handle); - let () = response - .write(&mut cursor) - .map_err(|e| UdpError::UnableToWriteToSocket { target, err: e.into() })?; + let task = server.run().boxed(); - #[allow(clippy::cast_possible_truncation)] - let position = cursor.position() as usize; - let inner = cursor.get_ref(); + Ok((task, udp_handle)) + } - debug!("Sending {} bytes ...", &inner[..position].len()); - debug!("To: {:?}", &target); - debug!("Payload: {:?}", &inner[..position]); + #[instrument(skip(self))] + async fn receive_request(&self) -> Result { + let mut payload = Vec::with_capacity(MAX_PACKET_SIZE); - let result = Self::send_packet(socket, &target, &inner[..position], watcher).await; + let (len, from) = self + .socket + .recv_buf_from(&mut payload) + .await + .map_err(|e| Error::UnableToReadFromSocket { + addr: self.addr, + err: e.into(), + })?; - debug!("{} bytes sent", &inner[..position].len()); + Vec::truncate(&mut payload, len); + trace!("GOT {payload:?}"); - result + Ok(UdpRequest { payload, from }) } +} - #[instrument(err, ret)] - async fn send_packet( - socket: &Arc, - &target: &SocketAddr, - payload: &[u8], - watcher: &Watcher, - ) -> Result { - trace!("Sending Packets: {payload:?} to: {target:?}"); - - select! { - r = socket.send_to(payload, target) => r.map_err(|e| UdpError::UnableToSendToSocket{ target, err: e.into()}), - () = watcher.wait_shutdown() => Err(UdpError::CanceledSending{target}) +#[instrument(, level = Level::TRACE)] +async fn fulfil_request(tracker: Arc, socket: Arc, addr: SocketAddr, request: UdpRequest, watcher: Watcher) { + let res_type = |response: &aquatic_udp_protocol::Response| -> Result { + use aquatic_udp_protocol::Response; + + match response { + Response::Connect(_) => Ok("connect".to_string()), + Response::AnnounceIpv4(_) => Ok("announce_ipv4".to_string()), + Response::AnnounceIpv6(_) => Ok("announce_ipv6".to_string()), + Response::Scrape(_) => Ok("scrape".to_string()), + Response::Error(e) => Err(format!("error: {}, id: {:?}", e.message, e.transaction_id)), + } + }; + let target = request.from; + + trace!("handling request into a response"); + let response = select! { + r = handlers::handle_packet(&tracker, addr, &request ) => r, + () = watcher.wait_shutdown() => return + }; + + let payload = get_response_payload(&response); + trace!("Got Response Payload: {payload:?}"); + + debug!("Sending {} bytes ... To: {:?}", payload.len(), &target); + let success = select! { + r = socket.send_to(&payload, target) => r, + () = watcher.wait_shutdown() => return + }; + + // lets trace the success value... + match success { + Ok(bytes) => match res_type(&response) { + Ok(r) => debug!("fulfilled an {r} request for {target} by sending {bytes} response"), + + Err(e) => debug!("sent {e} response to {target} by sending {bytes}"), + }, + Err(e) => { + error!( + "failed to send a {:?} response of {} bytes to {target}, with error {e}", + res_type(&response), + payload.len() + ); + debug!("unfulfilled response: {response:?}"); } } +} - #[instrument(ret)] - pub fn graceful_udp_shutdown(handle: super::Handle, rx_shutdown: tokio::sync::oneshot::Receiver, message: String) { - tokio::task::spawn(async move { - match handle.listening().await { - Some(addr) => { - shutdown_signal_with_message(rx_shutdown, format!("{message}, on socket address: {addr}")).await; +#[instrument] +fn get_response_payload(response: &aquatic_udp_protocol::Response) -> Vec { + let buffer = vec![0u8; MAX_PACKET_SIZE]; + let mut cursor = std::io::Cursor::new(buffer); - info!("Sending graceful shutdown signal"); - handle.graceful_shutdown(Some(Duration::from_secs(90))); + let () = response.write(&mut cursor).expect("it should be able to write to buffer"); - println!("!! shuting down in 90 seconds !!"); + #[allow(clippy::cast_possible_truncation)] + let len = cursor.position() as usize; + let mut payload = cursor.into_inner(); + payload.truncate(len); - loop { - tokio::time::sleep(Duration::from_secs(1)).await; + payload +} - info!("remaining alive connections: {}", handle.connection_count()); +#[instrument(ret)] +pub fn graceful_udp_shutdown(handle: &Handle, message: String) { + let handle = handle.clone(); + + tokio::task::spawn(async move { + { + let end = tokio::select! { + maybe = handle.listening() => { Task::Listening(maybe) }, + () = global_shutdown_signal() => { Task::BeforeListening } + }; + + match end { + Task::BeforeListening => { + warn!("Shutdown Before Listening"); + handle.shutdown(); + return; + } + Task::Listening(maybe) => { + if let Some(addr) = maybe { + info!("Listening to: {addr}"); + } else { + warn!("Failed to start Listening"); + handle.shutdown(); + return; } } - None => handle.shutdown(), + + _ => unreachable!(), } - }); - } + }; + + loop { + let end = tokio::select! { + biased; + () = global_terminate_signal() => { Task::Terminate} + () = global_interrupt_signal() => { Task::Interrupt} + () = handle.wait_shutdown() => { Task::Shutdown} + () = handle.wait_graceful_shutdown() => { Task::Graceful} + }; + + match end { + Task::Interrupt => { + if let Ok(()) = tokio::time::timeout(Duration::from_millis(100), handle.wait_graceful_shutdown()).await { + info! {"Got Second Interrupt: Exiting Now"}; + handle.shutdown(); + return; + } + info! {"Got Interrupt: Gracefully Exiting over the next 90 seconds..."}; + handle.graceful_shutdown(Some(Duration::from_secs(90))); + } + Task::Terminate => { + warn!("Got Terminate: Exiting Now!"); + handle.shutdown(); + return; + } + Task::Shutdown => { + info!("Received Shutdown, Closing Service."); + return; + } + Task::Graceful => loop { + let wait = tokio::select! { + biased; + () = handle.wait_connections_end() => Task::Finished, + () = tokio::time::sleep(Duration::from_secs(1)) => Task::Remaining, + }; + + match wait { + Task::Remaining => { + info!("Remaining Connections: {}", handle.connection_count()); + } + Task::Finished => { + info!("No More Connections, Closing Service"); + return; + } + _ => unreachable!(), + }; + }, + _ => unreachable!(), + } + } + }); } + +// match handle.listening().await { +// Some(addr) => { + +// handle.wait_graceful_shutdown() + +// shutdown_signal_with_message(rx_shutdown, format!("{message}, on socket address: {addr}")).await; + +// info!("Sending graceful shutdown signal"); +// handle.graceful_shutdown(Some(Duration::from_secs(90))); + +// println!("!! shuting down in 90 seconds !!"); + +// loop { +// tokio::time::sleep(Duration::from_secs(1)).await; + +// info!("remaining alive connections: {}", handle.connection_count()); +// } +// } +// None => handle.shutdown(), +// } +// }); +// } diff --git a/src/servers/udp/connection_cookie.rs b/src/servers/udp/v0/cookie.rs similarity index 99% rename from src/servers/udp/connection_cookie.rs rename to src/servers/udp/v0/cookie.rs index 49ea6261b..a005a969d 100644 --- a/src/servers/udp/connection_cookie.rs +++ b/src/servers/udp/v0/cookie.rs @@ -169,7 +169,7 @@ mod tests { use torrust_tracker_clock::time_extent::{self, Extent}; use super::cookie_builder::{self}; - use crate::servers::udp::connection_cookie::{check, make, Cookie, COOKIE_LIFETIME}; + use crate::servers::udp::v0::cookie::{check, make, Cookie, COOKIE_LIFETIME}; // #![feature(const_socketaddr)] // const REMOTE_ADDRESS_IPV4_ZERO: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); diff --git a/src/servers/udp/error.rs b/src/servers/udp/v0/error.rs similarity index 100% rename from src/servers/udp/error.rs rename to src/servers/udp/v0/error.rs diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/v0/handlers.rs similarity index 81% rename from src/servers/udp/handlers.rs rename to src/servers/udp/v0/handlers.rs index 4215b231a..98bede053 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -9,19 +9,18 @@ use aquatic_udp_protocol::{ AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; -use tokio::net::UdpSocket; use torrust_tracker_located_error::DynError; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::debug; use uuid::Uuid; -use super::connection_cookie::{check, from_connection_id, into_connection_id, make}; +use super::cookie::{check, from_connection_id, into_connection_id, make}; use super::UdpRequest; use crate::core::{statistics, ScrapeData, Tracker}; -use crate::servers::udp::error::Error; use crate::servers::udp::logging::{log_bad_request, log_error_response, log_request, log_response}; use crate::servers::udp::peer_builder; use crate::servers::udp::request::AnnounceWrapper; +use crate::servers::udp::v0::error::Error; use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; /// It handles the incoming UDP packets. @@ -33,13 +32,14 @@ use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; /// type. /// /// It will return an `Error` response if the request is invalid. -pub(crate) async fn handle_packet(udp_request: &UdpRequest, tracker: &Arc, socket: Arc) -> Response { +pub(crate) async fn handle_packet(tracker: &Arc, addr: SocketAddr, udp_request: &UdpRequest) -> Response { debug!("Handling Packets: {udp_request:?}"); + let target = udp_request.from; + let start_time = Instant::now(); let request_id = RequestId::make(udp_request); - let server_socket_addr = socket.local_addr().expect("Could not get local_addr for socket."); match Request::from_bytes(&udp_request.payload[..udp_request.payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| { Error::InternalServer { @@ -48,7 +48,7 @@ pub(crate) async fn handle_packet(udp_request: &UdpRequest, tracker: &Arc { - log_request(&request, &request_id, &server_socket_addr); + log_request(&request, &request_id, &addr); let transaction_id = match &request { Request::Connect(connect_request) => connect_request.transaction_id, @@ -56,14 +56,14 @@ pub(crate) async fn handle_packet(udp_request: &UdpRequest, tracker: &Arc scrape_request.transaction_id, }; - let response = match handle_request(request, udp_request.from, tracker).await { + let response = match handle_request(tracker, request, target).await { Ok(response) => response, Err(e) => handle_error(&e, transaction_id), }; let latency = start_time.elapsed(); - log_response(&response, &transaction_id, &request_id, &server_socket_addr, latency); + log_response(&response, &transaction_id, &request_id, &addr, latency); response } @@ -89,13 +89,13 @@ pub(crate) async fn handle_packet(udp_request: &UdpRequest, tracker: &Arc Result { - debug!("Handling Request: {request:?} to: {remote_addr:?}"); +pub async fn handle_request(tracker: &Tracker, request: Request, target: SocketAddr) -> Result { + debug!("Handling Request: {request:?} to: {target:?}"); match request { - Request::Connect(connect_request) => handle_connect(remote_addr, &connect_request, tracker).await, - Request::Announce(announce_request) => handle_announce(remote_addr, &announce_request, tracker).await, - Request::Scrape(scrape_request) => handle_scrape(remote_addr, &scrape_request, tracker).await, + Request::Connect(connect_request) => handle_connect(tracker, &connect_request, target).await, + Request::Announce(announce_request) => handle_announce(tracker, &announce_request, target).await, + Request::Scrape(scrape_request) => handle_scrape(tracker, &scrape_request, target).await, } } @@ -105,10 +105,10 @@ pub async fn handle_request(request: Request, remote_addr: SocketAddr, tracker: /// # Errors /// /// This function does not ever return an error. -pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, tracker: &Tracker) -> Result { +pub async fn handle_connect(tracker: &Tracker, request: &ConnectRequest, target: SocketAddr) -> Result { debug!("udp connect request: {:#?}", request); - let connection_cookie = make(&remote_addr); + let connection_cookie = make(&target); let connection_id = into_connection_id(&connection_cookie); let response = ConnectResponse { @@ -119,7 +119,7 @@ pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, t debug!("udp connect response: {:#?}", response); // send stats event - match remote_addr { + match target { SocketAddr::V4(_) => { tracker.send_stats_event(statistics::Event::Udp4Connect).await; } @@ -138,9 +138,9 @@ pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, t /// /// If a error happens in the `handle_announce` function, it will just return the `ServerError`. pub async fn handle_announce( - remote_addr: SocketAddr, - announce_request: &AnnounceRequest, tracker: &Tracker, + announce_request: &AnnounceRequest, + target: SocketAddr, ) -> Result { debug!("udp announce request: {:#?}", announce_request); @@ -151,12 +151,12 @@ pub async fn handle_announce( }); } - check(&remote_addr, &from_connection_id(&announce_request.connection_id))?; + check(&target, &from_connection_id(&announce_request.connection_id))?; let wrapped_announce_request = AnnounceWrapper::new(announce_request); let info_hash = wrapped_announce_request.info_hash; - let remote_client_ip = remote_addr.ip(); + let remote_client_ip = target.ip(); // Authorization tracker.authorize(&info_hash).await.map_err(|e| Error::TrackerError { @@ -177,7 +177,7 @@ pub async fn handle_announce( } #[allow(clippy::cast_possible_truncation)] - if remote_addr.is_ipv4() { + if target.is_ipv4() { let announce_response = AnnounceResponse { transaction_id: wrapped_announce_request.announce_request.transaction_id, announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32), @@ -236,7 +236,7 @@ pub async fn handle_announce( /// # Errors /// /// This function does not ever return an error. -pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tracker: &Tracker) -> Result { +pub async fn handle_scrape(tracker: &Tracker, request: &ScrapeRequest, target: SocketAddr) -> Result { debug!("udp scrape request: {:#?}", request); // Convert from aquatic infohashes @@ -269,7 +269,7 @@ pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tra } // send stats event - match remote_addr { + match target { SocketAddr::V4(_) => { tracker.send_stats_event(statistics::Event::Udp4Scrape).await; } @@ -351,11 +351,11 @@ mod tests { tracker_factory(configuration).into() } - fn sample_ipv4_remote_addr() -> SocketAddr { + fn sample_ipv4_target() -> SocketAddr { sample_ipv4_socket_address() } - fn sample_ipv6_remote_addr() -> SocketAddr { + fn sample_ipv6_target() -> SocketAddr { sample_ipv6_socket_address() } @@ -437,11 +437,11 @@ mod tests { use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Response, TransactionId}; use mockall::predicate::eq; - use super::{sample_ipv4_socket_address, sample_ipv6_remote_addr, tracker_configuration}; + use super::{sample_ipv4_socket_address, sample_ipv6_target, tracker_configuration}; use crate::core::{self, statistics}; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_connect; - use crate::servers::udp::handlers::tests::{public_tracker, sample_ipv4_remote_addr}; + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_connect; + use crate::servers::udp::v0::handlers::tests::{public_tracker, sample_ipv4_target}; fn sample_connect_request() -> ConnectRequest { ConnectRequest { @@ -455,14 +455,14 @@ mod tests { transaction_id: TransactionId(0i32), }; - let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) + let response = handle_connect(&public_tracker(), &request, sample_ipv4_target()) .await .unwrap(); assert_eq!( response, Response::Connect(ConnectResponse { - connection_id: into_connection_id(&make(&sample_ipv4_remote_addr())), + connection_id: into_connection_id(&make(&sample_ipv4_target())), transaction_id: request.transaction_id }) ); @@ -474,14 +474,14 @@ mod tests { transaction_id: TransactionId(0i32), }; - let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) + let response = handle_connect(&public_tracker(), &request, sample_ipv4_target()) .await .unwrap(); assert_eq!( response, Response::Connect(ConnectResponse { - connection_id: into_connection_id(&make(&sample_ipv4_remote_addr())), + connection_id: into_connection_id(&make(&sample_ipv4_target())), transaction_id: request.transaction_id }) ); @@ -497,14 +497,12 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let client_socket_address = sample_ipv4_socket_address(); + let target = sample_ipv4_socket_address(); - let torrent_tracker = Arc::new( + let tracker = Arc::new( core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_connect(client_socket_address, &sample_connect_request(), &torrent_tracker) - .await - .unwrap(); + handle_connect(&tracker, &sample_connect_request(), target).await.unwrap(); } #[tokio::test] @@ -517,10 +515,10 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let torrent_tracker = Arc::new( + let tracker = Arc::new( core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_connect(sample_ipv6_remote_addr(), &sample_connect_request(), &torrent_tracker) + handle_connect(&tracker, &sample_connect_request(), sample_ipv6_target()) .await .unwrap(); } @@ -535,8 +533,8 @@ mod tests { TransactionId, }; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::tests::sample_ipv4_remote_addr; + use super::sample_ipv4_target; + use crate::servers::udp::v0::cookie::{into_connection_id, make}; struct AnnounceRequestBuilder { request: AnnounceRequest, @@ -549,7 +547,7 @@ mod tests { let info_hash_aquatic = aquatic_udp_protocol::InfoHash([0u8; 20]); let default_request = AnnounceRequest { - connection_id: into_connection_id(&make(&sample_ipv4_remote_addr())), + connection_id: into_connection_id(&make(&sample_ipv4_target())), transaction_id: TransactionId(0i32), info_hash: info_hash_aquatic, peer_id: AquaticPeerId([255u8; 20]), @@ -611,10 +609,10 @@ mod tests { use torrust_tracker_primitives::peer; use crate::core::{self, statistics}; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; - use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; - use crate::servers::udp::handlers::tests::{ + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_announce; + use crate::servers::udp::v0::handlers::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::v0::handlers::tests::{ public_tracker, sample_ipv4_socket_address, tracker_configuration, TorrentPeerBuilder, }; @@ -627,17 +625,17 @@ mod tests { let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); - let remote_addr = SocketAddr::new(IpAddr::V4(client_ip), client_port); + let target = SocketAddr::new(IpAddr::V4(client_ip), client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(client_ip) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -651,13 +649,13 @@ mod tests { #[tokio::test] async fn the_announced_peer_should_not_be_included_in_the_response() { - let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080); + let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .into(); - let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); + let response = handle_announce(&public_tracker(), &request, target).await.unwrap(); let empty_peer_vector: Vec> = vec![]; assert_eq!( @@ -688,17 +686,17 @@ mod tests { let remote_client_port = 8081; let peer_address = Ipv4Addr::new(126, 0, 0, 2); - let remote_addr = SocketAddr::new(IpAddr::V4(remote_client_ip), remote_client_port); + let target = SocketAddr::new(IpAddr::V4(remote_client_ip), remote_client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(peer_address) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -724,12 +722,12 @@ mod tests { } async fn announce_a_new_peer_using_ipv4(tracker: Arc) -> Response { - let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080); + let target = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap() + handle_announce(&tracker, &request, target).await.unwrap() } #[tokio::test] @@ -764,9 +762,9 @@ mod tests { ); handle_announce( - sample_ipv4_socket_address(), - &AnnounceRequestBuilder::default().into(), &tracker, + &AnnounceRequestBuilder::default().into(), + sample_ipv4_socket_address(), ) .await .unwrap(); @@ -779,10 +777,10 @@ mod tests { use aquatic_udp_protocol::{InfoHash as AquaticInfoHash, PeerId as AquaticPeerId}; use torrust_tracker_primitives::peer; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; - use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; - use crate::servers::udp::handlers::tests::{public_tracker, TorrentPeerBuilder}; + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_announce; + use crate::servers::udp::v0::handlers::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::v0::handlers::tests::{public_tracker, TorrentPeerBuilder}; #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration_if_defined() { @@ -793,17 +791,17 @@ mod tests { let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); - let remote_addr = SocketAddr::new(IpAddr::V4(client_ip), client_port); + let target = SocketAddr::new(IpAddr::V4(client_ip), client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(client_ip) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -833,11 +831,11 @@ mod tests { use torrust_tracker_primitives::peer; use crate::core::{self, statistics}; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; - use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; - use crate::servers::udp::handlers::tests::{ - public_tracker, sample_ipv6_remote_addr, tracker_configuration, TorrentPeerBuilder, + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_announce; + use crate::servers::udp::v0::handlers::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::v0::handlers::tests::{ + public_tracker, sample_ipv6_target, tracker_configuration, TorrentPeerBuilder, }; #[tokio::test] @@ -850,17 +848,17 @@ mod tests { let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); - let remote_addr = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); + let target = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(client_ip_v4) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -877,13 +875,13 @@ mod tests { let client_ip_v4 = Ipv4Addr::new(126, 0, 0, 1); let client_ip_v6 = client_ip_v4.to_ipv6_compatible(); - let remote_addr = SocketAddr::new(IpAddr::V6(client_ip_v6), 8080); + let target = SocketAddr::new(IpAddr::V6(client_ip_v6), 8080); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .into(); - let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); + let response = handle_announce(&public_tracker(), &request, target).await.unwrap(); let empty_peer_vector: Vec> = vec![]; assert_eq!( @@ -914,17 +912,17 @@ mod tests { let remote_client_port = 8081; let peer_address = "126.0.0.1".parse().unwrap(); - let remote_addr = SocketAddr::new(IpAddr::V6(remote_client_ip), remote_client_port); + let target = SocketAddr::new(IpAddr::V6(remote_client_ip), remote_client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(peer_address) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -953,12 +951,12 @@ mod tests { let client_ip_v4 = Ipv4Addr::new(126, 0, 0, 1); let client_ip_v6 = client_ip_v4.to_ipv6_compatible(); let client_port = 8080; - let remote_addr = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); + let target = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap() + handle_announce(&tracker, &request, target).await.unwrap() } #[tokio::test] @@ -992,13 +990,13 @@ mod tests { core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - let remote_addr = sample_ipv6_remote_addr(); + let target = sample_ipv6_target(); let announce_request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .into(); - handle_announce(remote_addr, &announce_request, &tracker).await.unwrap(); + handle_announce(&tracker, &announce_request, target).await.unwrap(); } mod from_a_loopback_ip { @@ -1009,10 +1007,10 @@ mod tests { use crate::core; use crate::core::statistics::Keeper; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; - use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; - use crate::servers::udp::handlers::tests::TrackerConfigurationBuilder; + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_announce; + use crate::servers::udp::v0::handlers::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::v0::handlers::tests::TrackerConfigurationBuilder; #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration() { @@ -1031,17 +1029,17 @@ mod tests { let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); - let remote_addr = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); + let target = SocketAddr::new(IpAddr::V6(client_ip_v6), client_port); let request = AnnounceRequestBuilder::default() - .with_connection_id(into_connection_id(&make(&remote_addr))) + .with_connection_id(into_connection_id(&make(&target))) .with_info_hash(info_hash) .with_peer_id(peer_id) .with_ip_address(client_ip_v4) .with_port(client_port) .into(); - handle_announce(remote_addr, &request, &tracker).await.unwrap(); + handle_announce(&tracker, &request, target).await.unwrap(); let peers = tracker.get_torrent_peers(&info_hash.0.into()); @@ -1072,9 +1070,9 @@ mod tests { use super::TorrentPeerBuilder; use crate::core::{self}; - use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_scrape; - use crate::servers::udp::handlers::tests::{public_tracker, sample_ipv4_remote_addr}; + use crate::servers::udp::v0::cookie::{into_connection_id, make}; + use crate::servers::udp::v0::handlers::handle_scrape; + use crate::servers::udp::v0::handlers::tests::{public_tracker, sample_ipv4_target}; fn zeroed_torrent_statistics() -> TorrentScrapeStatistics { TorrentScrapeStatistics { @@ -1086,18 +1084,18 @@ mod tests { #[tokio::test] async fn should_return_no_stats_when_the_tracker_does_not_have_any_torrent() { - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let info_hash = InfoHash([0u8; 20]); let info_hashes = vec![info_hash]; let request = ScrapeRequest { - connection_id: into_connection_id(&make(&remote_addr)), + connection_id: into_connection_id(&make(&target)), transaction_id: TransactionId(0i32), info_hashes, }; - let response = handle_scrape(remote_addr, &request, &public_tracker()).await.unwrap(); + let response = handle_scrape(&public_tracker(), &request, target).await.unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1110,12 +1108,12 @@ mod tests { ); } - async fn add_a_seeder(tracker: Arc, remote_addr: &SocketAddr, info_hash: &InfoHash) { + async fn add_a_seeder(tracker: Arc, target: &SocketAddr, info_hash: &InfoHash) { let peer_id = peer::Id([255u8; 20]); let peer = TorrentPeerBuilder::new() .with_peer_id(peer::Id(peer_id.0)) - .with_peer_address(*remote_addr) + .with_peer_address(*target) .with_number_of_bytes_left(0) .into(); @@ -1124,25 +1122,25 @@ mod tests { .await; } - fn build_scrape_request(remote_addr: &SocketAddr, info_hash: &InfoHash) -> ScrapeRequest { + fn build_scrape_request(target: &SocketAddr, info_hash: &InfoHash) -> ScrapeRequest { let info_hashes = vec![*info_hash]; ScrapeRequest { - connection_id: into_connection_id(&make(remote_addr)), + connection_id: into_connection_id(&make(target)), transaction_id: TransactionId(0i32), info_hashes, } } async fn add_a_sample_seeder_and_scrape(tracker: Arc) -> Response { - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let info_hash = InfoHash([0u8; 20]); - add_a_seeder(tracker.clone(), &remote_addr, &info_hash).await; + add_a_seeder(tracker.clone(), &target, &info_hash).await; - let request = build_scrape_request(&remote_addr, &info_hash); + let request = build_scrape_request(&target, &info_hash); - handle_scrape(remote_addr, &request, &tracker).await.unwrap() + handle_scrape(&tracker, &request, target).await.unwrap() } fn match_scrape_response(response: Response) -> Option { @@ -1155,8 +1153,10 @@ mod tests { mod with_a_public_tracker { use aquatic_udp_protocol::{NumberOfDownloads, NumberOfPeers, TorrentScrapeStatistics}; - use crate::servers::udp::handlers::tests::public_tracker; - use crate::servers::udp::handlers::tests::scrape_request::{add_a_sample_seeder_and_scrape, match_scrape_response}; + use crate::servers::udp::v0::handlers::tests::public_tracker; + use crate::servers::udp::v0::handlers::tests::scrape_request::{ + add_a_sample_seeder_and_scrape, match_scrape_response, + }; #[tokio::test] async fn should_return_torrent_statistics_when_the_tracker_has_the_requested_torrent() { @@ -1178,22 +1178,22 @@ mod tests { use aquatic_udp_protocol::InfoHash; - use crate::servers::udp::handlers::handle_scrape; - use crate::servers::udp::handlers::tests::scrape_request::{ + use crate::servers::udp::v0::handlers::handle_scrape; + use crate::servers::udp::v0::handlers::tests::scrape_request::{ add_a_sample_seeder_and_scrape, build_scrape_request, match_scrape_response, zeroed_torrent_statistics, }; - use crate::servers::udp::handlers::tests::{private_tracker, sample_ipv4_remote_addr}; + use crate::servers::udp::v0::handlers::tests::{private_tracker, sample_ipv4_target}; #[tokio::test] async fn should_return_zeroed_statistics_when_the_tracker_does_not_have_the_requested_torrent() { let tracker = private_tracker(); - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let non_existing_info_hash = InfoHash([0u8; 20]); - let request = build_scrape_request(&remote_addr, &non_existing_info_hash); + let request = build_scrape_request(&target, &non_existing_info_hash); - let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(&tracker, &request, target).await.unwrap()).unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1216,26 +1216,26 @@ mod tests { mod with_a_whitelisted_tracker { use aquatic_udp_protocol::{InfoHash, NumberOfDownloads, NumberOfPeers, TorrentScrapeStatistics}; - use crate::servers::udp::handlers::handle_scrape; - use crate::servers::udp::handlers::tests::scrape_request::{ + use crate::servers::udp::v0::handlers::handle_scrape; + use crate::servers::udp::v0::handlers::tests::scrape_request::{ add_a_seeder, build_scrape_request, match_scrape_response, zeroed_torrent_statistics, }; - use crate::servers::udp::handlers::tests::{sample_ipv4_remote_addr, whitelisted_tracker}; + use crate::servers::udp::v0::handlers::tests::{sample_ipv4_target, whitelisted_tracker}; #[tokio::test] async fn should_return_the_torrent_statistics_when_the_requested_torrent_is_whitelisted() { let tracker = whitelisted_tracker(); - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let info_hash = InfoHash([0u8; 20]); - add_a_seeder(tracker.clone(), &remote_addr, &info_hash).await; + add_a_seeder(tracker.clone(), &target, &info_hash).await; tracker.add_torrent_to_memory_whitelist(&info_hash.0.into()).await; - let request = build_scrape_request(&remote_addr, &info_hash); + let request = build_scrape_request(&target, &info_hash); - let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(&tracker, &request, target).await.unwrap()).unwrap(); let expected_torrent_stats = vec![TorrentScrapeStatistics { seeders: NumberOfPeers(1), @@ -1250,14 +1250,14 @@ mod tests { async fn should_return_zeroed_statistics_when_the_requested_torrent_is_not_whitelisted() { let tracker = whitelisted_tracker(); - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let info_hash = InfoHash([0u8; 20]); - add_a_seeder(tracker.clone(), &remote_addr, &info_hash).await; + add_a_seeder(tracker.clone(), &target, &info_hash).await; - let request = build_scrape_request(&remote_addr, &info_hash); + let request = build_scrape_request(&target, &info_hash); - let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); + let torrent_stats = match_scrape_response(handle_scrape(&tracker, &request, target).await.unwrap()).unwrap(); let expected_torrent_stats = vec![zeroed_torrent_statistics()]; @@ -1265,12 +1265,12 @@ mod tests { } } - fn sample_scrape_request(remote_addr: &SocketAddr) -> ScrapeRequest { + fn sample_scrape_request(target: &SocketAddr) -> ScrapeRequest { let info_hash = InfoHash([0u8; 20]); let info_hashes = vec![info_hash]; ScrapeRequest { - connection_id: into_connection_id(&make(remote_addr)), + connection_id: into_connection_id(&make(target)), transaction_id: TransactionId(0i32), info_hashes, } @@ -1284,8 +1284,8 @@ mod tests { use super::sample_scrape_request; use crate::core::{self, statistics}; - use crate::servers::udp::handlers::handle_scrape; - use crate::servers::udp::handlers::tests::{sample_ipv4_remote_addr, tracker_configuration}; + use crate::servers::udp::v0::handlers::handle_scrape; + use crate::servers::udp::v0::handlers::tests::{sample_ipv4_target, tracker_configuration}; #[tokio::test] async fn should_send_the_upd4_scrape_event() { @@ -1297,12 +1297,12 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let remote_addr = sample_ipv4_remote_addr(); + let target = sample_ipv4_target(); let tracker = Arc::new( core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) + handle_scrape(&tracker, &sample_scrape_request(&target), target) .await .unwrap(); } @@ -1316,8 +1316,8 @@ mod tests { use super::sample_scrape_request; use crate::core::{self, statistics}; - use crate::servers::udp::handlers::handle_scrape; - use crate::servers::udp::handlers::tests::{sample_ipv6_remote_addr, tracker_configuration}; + use crate::servers::udp::v0::handlers::handle_scrape; + use crate::servers::udp::v0::handlers::tests::{sample_ipv6_target, tracker_configuration}; #[tokio::test] async fn should_send_the_upd6_scrape_event() { @@ -1329,12 +1329,12 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let remote_addr = sample_ipv6_remote_addr(); + let target = sample_ipv6_target(); let tracker = Arc::new( core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), ); - handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) + handle_scrape(&tracker, &sample_scrape_request(&target), target) .await .unwrap(); } diff --git a/src/servers/udp/v0/mod.rs b/src/servers/udp/v0/mod.rs new file mode 100644 index 000000000..4f20a35db --- /dev/null +++ b/src/servers/udp/v0/mod.rs @@ -0,0 +1,14 @@ +use std::net::SocketAddr; + +pub mod cookie; +pub mod error; +pub mod handlers; + +use derive_more::Display; + +#[derive(Display, Debug)] +#[display(fmt = "from (target): {from}")] +pub(crate) struct UdpRequest { + pub payload: Vec, + pub from: SocketAddr, +} diff --git a/src/shared/bit_torrent/tracker/udp/mod.rs b/src/shared/bit_torrent/tracker/udp/mod.rs index e384b4524..1ca918635 100644 --- a/src/shared/bit_torrent/tracker/udp/mod.rs +++ b/src/shared/bit_torrent/tracker/udp/mod.rs @@ -6,6 +6,7 @@ use thiserror::Error; mod client; pub use client::Client; +use torrust_tracker_located_error::DynError; #[derive(Debug, Clone, Error)] pub enum Error { @@ -49,6 +50,12 @@ pub enum Error { UnableToGetRemoteAddress { err: Arc }, } +impl From for DynError { + fn from(e: Error) -> Self { + Arc::new(Box::new(e)) + } +} + /// Generates the source address for the UDP client fn source_address(port: u16) -> SocketAddr { SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port) diff --git a/src/shared/handle.rs b/src/shared/handle.rs new file mode 100644 index 000000000..3539da31f --- /dev/null +++ b/src/shared/handle.rs @@ -0,0 +1,289 @@ +//! Copy-Paste from axum. Thanks. + +use std::net::SocketAddr; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use derive_more::Display; +use tokio::sync::Notify; +use tokio::time::sleep; + +/// A handle for [`Server`](crate::server::Server). +#[derive(Clone, Default, Display)] +#[display(fmt = "{}", "self.inner.get()")] +pub struct Handle { + inner: Arc, +} + +impl std::fmt::Debug for Handle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let state = self.inner.get(); + + f.debug_struct("Handle").field("state", &state).finish() + } +} + +#[derive(Default)] +struct HandleInner { + addr: Mutex>, + addr_notify: Notify, + conn_count: AtomicUsize, + shutdown: NotifyOnce, + graceful: NotifyOnce, + graceful_dur: Mutex>, + conn_end: NotifyOnce, +} + +impl Handle { + /// Create a new handle. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Get the number of connections. + #[must_use] + pub fn connection_count(&self) -> usize { + self.inner.conn_count.load(Ordering::SeqCst) + } + + /// Shutdown the server. + pub fn shutdown(&self) { + self.inner.shutdown.notify_waiters(); + } + + /// Gracefully shutdown the server. + /// + /// `None` means indefinite grace period. + /// + #[allow(clippy::missing_panics_doc)] + pub fn graceful_shutdown(&self, duration: Option) { + *self.inner.graceful_dur.lock().unwrap() = duration; + + self.inner.graceful.notify_waiters(); + } + + /// Returns local address and port when server starts listening. + /// + /// Returns `None` if server fails to bind. + /// + #[allow(clippy::missing_panics_doc)] + pub async fn listening(&self) -> Option { + let notified = self.inner.addr_notify.notified(); + + if let Some(addr) = *self.inner.addr.lock().unwrap() { + return Some(addr); + } + + notified.await; + + *self.inner.addr.lock().unwrap() + } + + pub(crate) fn notify_listening(&self, addr: Option) { + *self.inner.addr.lock().unwrap() = addr; + + self.inner.addr_notify.notify_waiters(); + } + + pub(crate) fn watcher(&self) -> Watcher { + Watcher::new(self.clone()) + } + + pub(crate) async fn wait_shutdown(&self) { + self.inner.shutdown.notified().await; + } + + pub(crate) async fn wait_graceful_shutdown(&self) { + self.inner.graceful.notified().await; + } + + #[allow(dead_code)] + /// Awaits the gracefully exiting connections. + /// + /// - Short-circuits if called when there are no connections. + /// - Upon elapsing of the deadline, shutdown is called. + /// + /// Note: this should be awaited after `wait_graceful_shutdown` returns, in a switch with `wait_shutdown`. + /// + pub(crate) async fn wait_connections_end(&self) { + if self.inner.conn_count.load(Ordering::SeqCst) == 0 { + return; + } + + let deadline = *self.inner.graceful_dur.lock().unwrap(); + + match deadline { + Some(duration) => tokio::select! { + biased; + () = sleep(duration) => self.shutdown(), + () = self.inner.conn_end.notified() => (), + }, + None => self.inner.conn_end.notified().await, + } + } +} + +#[derive(Debug)] +pub(crate) struct Watcher { + handle: Handle, +} + +impl Watcher { + fn new(handle: Handle) -> Self { + handle.inner.conn_count.fetch_add(1, Ordering::SeqCst); + + Self { handle } + } + + #[allow(dead_code)] + pub(crate) async fn wait_graceful_shutdown(&self) { + self.handle.wait_graceful_shutdown().await; + } + + pub(crate) async fn wait_shutdown(&self) { + self.handle.wait_shutdown().await; + } +} + +impl Drop for Watcher { + fn drop(&mut self) { + let count = self.handle.inner.conn_count.fetch_sub(1, Ordering::SeqCst) - 1; + + if count == 0 && self.handle.inner.graceful.is_notified() { + self.handle.inner.conn_end.notify_waiters(); + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct NotifyOnce { + notified: AtomicBool, + notify: Notify, +} + +impl NotifyOnce { + pub(crate) fn notify_waiters(&self) { + self.notified.store(true, Ordering::SeqCst); + + self.notify.notify_waiters(); + } + + pub(crate) fn is_notified(&self) -> bool { + self.notified.load(Ordering::SeqCst) + } + + pub(crate) async fn notified(&self) { + let future = self.notify.notified(); + + if !self.notified.load(Ordering::SeqCst) { + future.await; + } + } +} + +#[derive(Clone, Debug)] +struct HandleState { + addr: Option, + conn_count: usize, + shutdown: bool, + graceful: bool, + graceful_dur: Option, + conn_end: bool, +} + +impl HandleInner { + fn get(&self) -> HandleState { + let addr = *self.addr.lock().unwrap(); + let conn_count = self.conn_count.load(Ordering::SeqCst); + let shutdown = self.shutdown.notified.load(Ordering::SeqCst); + let graceful = self.graceful.notified.load(Ordering::SeqCst); + let graceful_dur = *self.graceful_dur.lock().unwrap(); + let conn_end = self.conn_end.notified.load(Ordering::SeqCst); + + HandleState { + addr, + conn_count, + shutdown, + graceful, + graceful_dur, + conn_end, + } + } +} + +impl std::fmt::Display for HandleState { + #[allow(clippy::useless_format)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let has_active_connections = match self.conn_count { + 0 => format!("has no active connection"), + 1 => format!("has a single active connection"), + n => format!("has {n} active connections"), + }; + + let listening_to = if let Some(addr) = self.addr { + format!("listening to: {addr}") + } else { + format!("(without listener)") + }; + + let with_a_timeout = if let Some(timeout) = self.graceful_dur { + format!("with timeout of {} seconds", timeout.as_secs_f32()) + } else { + format!("of an indefinite timeout") + }; + + let status = match (self.shutdown, self.graceful, self.conn_end, self.graceful_dur) { + // shutdown gracefully + (true, true, true, _) => format!("stopped after a graceful shutdown {with_a_timeout}"), + + // shutdown but gracefully was interrupted (with timeout) + (true, true, false, Some(_)) => { + format!("interrupted (or deadline passed) a graceful shutdown {with_a_timeout}") + } + + // shutdown but gracefully interrupted (without timeout) + (true, true, false, None) => format!("interrupted a graceful shutdown {with_a_timeout}") + , + + // shutdown abruptly + (true, false, _, _) => format!("abruptly interrupted"), + + // not shutdown and gracefully completed + (false, true, true, _) => format!("finished gracefully shutting down {with_a_timeout}"), + + // not shutdown and gracefully exiting + (false, true, false, _) => format!("is gracefully shutting down {with_a_timeout}"), + + // not shutdown and not exiting + (false, false, _, _) => format!("running"), + }; + + let state = match (self.shutdown || self.conn_end, self.addr, self.conn_count) { + // stopped and without connections. + // running and listening with active connections. + (true, _, 0) | (false, Some(_), _) => format!("{listening_to}, was {status}, {has_active_connections}."), + + // stopped, is listening, but with connections! + (true, Some(_), 1..) => format!("{listening_to}, was {status}, !unexpectedly! {has_active_connections}!"), + + // stopped, not listening, but somehow has connections!! + (true, None, 1..) => { + format!("{listening_to}, was {status}, !strangely and unexpectedly! {has_active_connections}!!") + } + + // running, not listening, and without connections. + (false, None, 0) => { + format!("{listening_to}, was {status}, {has_active_connections}!") + } + + // running, not listening, somehow without connections! + (false, None, 1..) => { + format!("{listening_to}, was {status}, !strangely! {has_active_connections}!") + } + }; + + f.write_fmt(format_args!("state: {state}")) + } +} diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 8c95effe1..49911bf37 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -4,3 +4,4 @@ //! - [`crypto`]: Encryption related logic. pub mod bit_torrent; pub mod crypto; +pub mod handle; diff --git a/tests/common/udp/environment.rs b/tests/common/udp/environment.rs index 50c11d543..fed84164d 100644 --- a/tests/common/udp/environment.rs +++ b/tests/common/udp/environment.rs @@ -6,7 +6,8 @@ use torrust_tracker::bootstrap::app::tracker; use torrust_tracker::core::Tracker; use torrust_tracker::servers::registar::Registar; use torrust_tracker::servers::service::{Service, Started, Stopped}; -use torrust_tracker::servers::udp::server::{UdpHandle, UdpLauncher}; +use torrust_tracker::servers::udp::handle::Handle; +use torrust_tracker::servers::udp::launcher::Launcher; use torrust_tracker_configuration::{Configuration, UdpTracker}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; @@ -15,7 +16,7 @@ pub struct Environment { pub config: Arc, pub tracker: Arc, pub registar: Registar, - pub server: Service, + pub server: Service, pub addr: Option, } @@ -39,7 +40,7 @@ impl Environment { .parse::() .expect("Tracker API bind_address invalid."); - let server = Service::new(UdpLauncher::new(tracker.clone(), addr)); + let server = Service::new(Launcher::new(tracker.clone(), addr)); Self { config, @@ -51,12 +52,12 @@ impl Environment { } #[allow(dead_code)] - pub async fn start(self) -> Environment> { + pub async fn start(self) -> Environment> { let server = self.server.start().unwrap(); // reg_form wait for the service to be ready before proceeding let () = server - .reg_form(self.registar.give_form()) + .reg_form(self.registar.form()) .await .expect("it should register a form"); @@ -72,7 +73,7 @@ impl Environment { } } -impl Environment> { +impl Environment> { pub async fn new(configuration: &Arc) -> Self { Environment::::new(configuration).start().await } diff --git a/tests/common/udp/mod.rs b/tests/common/udp/mod.rs index 2ff0347bc..8532eaf0c 100644 --- a/tests/common/udp/mod.rs +++ b/tests/common/udp/mod.rs @@ -1,9 +1,9 @@ use torrust_tracker::servers::service; -use torrust_tracker::servers::udp::server::UdpHandle; +use torrust_tracker::servers::udp::handle::Handle; pub mod environment; -pub type Started<'a> = environment::Environment>; +pub type Started<'a> = environment::Environment>; //pub type Stopped<'a> = environment::Environment; #[cfg(test)] diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 147a65e44..1cb07d76a 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -61,7 +61,7 @@ impl Environment { // reg_form wait for the service to be ready before proceeding let () = server - .reg_form(self.registar.give_form()) + .reg_form(self.registar.form()) .await .expect("it should register a form"); diff --git a/tests/servers/health_check_api/contract.rs b/tests/servers/health_check_api/contract.rs index 19844b5da..8be5cdff0 100644 --- a/tests/servers/health_check_api/contract.rs +++ b/tests/servers/health_check_api/contract.rs @@ -1,4 +1,4 @@ -use torrust_tracker::servers::health_check_api::resources::{Report, Status}; +use torrust_tracker::servers::health_check_api::v0::resources::{Report, Status}; use torrust_tracker::servers::registar::Registar; use torrust_tracker_test_helpers::configuration; @@ -31,7 +31,7 @@ async fn health_check_endpoint_should_return_status_ok_when_there_is_no_services mod api { use std::sync::Arc; - use torrust_tracker::servers::health_check_api::resources::{Report, Status}; + use torrust_tracker::servers::health_check_api::v0::resources::{Report, Status}; use torrust_tracker_test_helpers::configuration; use crate::servers::api; @@ -136,7 +136,7 @@ mod api { mod http { use std::sync::Arc; - use torrust_tracker::servers::health_check_api::resources::{Report, Status}; + use torrust_tracker::servers::health_check_api::v0::resources::{Report, Status}; use torrust_tracker_test_helpers::configuration; use crate::servers::health_check_api::client::get; @@ -240,7 +240,7 @@ mod http { mod udp { use std::sync::Arc; - use torrust_tracker::servers::health_check_api::resources::{Report, Status}; + use torrust_tracker::servers::health_check_api::v0::resources::{Report, Status}; use torrust_tracker_test_helpers::configuration; use crate::common::udp; diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs index 02c13e48a..886a24b0f 100644 --- a/tests/servers/health_check_api/environment.rs +++ b/tests/servers/health_check_api/environment.rs @@ -1,12 +1,13 @@ use std::net::SocketAddr; use std::sync::Arc; -use torrust_tracker::servers::health_check_api::server::{HealthCheckHandle, HealthCheckLauncher}; -use torrust_tracker::servers::registar::{Registar, ServiceRegistrationForm, ServiceRegistry}; +use torrust_tracker::servers::health_check_api::handle::Handle; +use torrust_tracker::servers::health_check_api::launcher::Launcher; +use torrust_tracker::servers::registar::{Form, Registar, Registry}; use torrust_tracker::servers::service; use torrust_tracker_configuration::HealthCheckApi; -type Started = service::Service, HealthCheckLauncher, HealthCheckHandle>; +type Started = service::Service, Launcher, Handle>; pub struct Environment { service: Started, @@ -20,16 +21,16 @@ impl Environment { /// /// Panics if something goes wrong... pub async fn new(config: &Arc, registar: &Registar) -> Self { - let registry = registar.clone().get_registry(); + let registry = registar.as_ref().clone(); let addr = config .bind_address .parse::() .expect("it should have a valid http tracker bind address"); - let form = registar.give_form(); + let form = registar.form(); - let (started, listening) = Self::start_v0(addr, ®istry, form).await; + let (started, listening) = Self::start_v0(addr, registry, form).await; Self { service: started, @@ -42,8 +43,8 @@ impl Environment { /// # Panics /// /// Panics if something goes wrong... - async fn start_v0(addr: SocketAddr, registry: &ServiceRegistry, form: ServiceRegistrationForm) -> (Started, SocketAddr) { - let service = service::Service::new(HealthCheckLauncher::new(addr, registry.clone())); + async fn start_v0(addr: SocketAddr, registry: Arc, form: Form) -> (Started, SocketAddr) { + let service = service::Service::new(Launcher::new(addr, registry)); let started: Started = service.start().expect("it should start"); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 71e2bc248..f6034c264 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -6,7 +6,8 @@ use futures::executor::block_on; use torrust_tracker::bootstrap::app::tracker; use torrust_tracker::bootstrap::jobs::make_rust_tls; use torrust_tracker::core::Tracker; -use torrust_tracker::servers::http::service::{HttpHandle, HttpLauncher}; +use torrust_tracker::servers::http::handle::Handle; +use torrust_tracker::servers::http::launcher::Launcher; use torrust_tracker::servers::registar::Registar; use torrust_tracker::servers::service::{Service, Started, Stopped}; use torrust_tracker_configuration::{Configuration, HttpTracker}; @@ -17,7 +18,7 @@ pub struct Environment { pub config: Arc, pub tracker: Arc, pub registar: Registar, - pub server: Service, + pub server: Service, pub addr: Option, } @@ -43,7 +44,7 @@ impl Environment { let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) .map(|tls| tls.expect("tls config failed")); - let server = Service::new(HttpLauncher::new(tracker.clone(), bind_to, tls)); + let server = Service::new(Launcher::new(tracker.clone(), bind_to, tls)); Self { config, @@ -55,12 +56,12 @@ impl Environment { } #[allow(dead_code)] - pub async fn start(self) -> Environment> { + pub async fn start(self) -> Environment> { let server = self.server.start().unwrap(); // reg_form wait for the service to be ready before proceeding let () = server - .reg_form(self.registar.give_form()) + .reg_form(self.registar.form()) .await .expect("it should register a form"); @@ -76,7 +77,7 @@ impl Environment { } } -impl Environment> { +impl Environment> { pub async fn new(configuration: &Arc) -> Self { Environment::::new(configuration).start().await } diff --git a/tests/servers/http/mod.rs b/tests/servers/http/mod.rs index 26f26da20..747e3de13 100644 --- a/tests/servers/http/mod.rs +++ b/tests/servers/http/mod.rs @@ -1,10 +1,10 @@ -use torrust_tracker::servers::http::service::HttpHandle; +use torrust_tracker::servers::http::handle::Handle; use torrust_tracker::servers::service; pub mod asserts; pub mod environment; pub mod v1; -pub type Started<'a> = environment::Environment>; +pub type Started<'a> = environment::Environment>; //pub type Stopped<'a> = environment::Environment; From 12f54e703e78af677fbd4456c359f580e83b1c44 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 8 Apr 2024 14:07:15 +0100 Subject: [PATCH 013/217] test: add tests for new torrent repository using SkipMap --- .../src/repository/skip_map_mutex_std.rs | 2 +- .../torrent-repository/tests/common/repo.rs | 165 +++++++++++------- .../tests/repository/mod.rs | 108 ++++++++++-- 3 files changed, 193 insertions(+), 82 deletions(-) diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index aa1f43826..0c0127b15 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -15,7 +15,7 @@ use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { - torrents: SkipMap, + pub torrents: SkipMap, } impl Repository for CrossbeamSkipList diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 3a4b53d2f..5a86aa3cf 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -7,49 +7,54 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; #[derive(Debug)] pub(crate) enum Repo { - Std(TorrentsRwLockStd), - StdMutexStd(TorrentsRwLockStdMutexStd), - StdMutexTokio(TorrentsRwLockStdMutexTokio), - Tokio(TorrentsRwLockTokio), - TokioMutexStd(TorrentsRwLockTokioMutexStd), - TokioMutexTokio(TorrentsRwLockTokioMutexTokio), + RwLockStd(TorrentsRwLockStd), + RwLockStdMutexStd(TorrentsRwLockStdMutexStd), + RwLockStdMutexTokio(TorrentsRwLockStdMutexTokio), + RwLockTokio(TorrentsRwLockTokio), + RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), + RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), + SkipMapMutexStd(TorrentsSkipMapMutexStd), } impl Repo { pub(crate) async fn get(&self, key: &InfoHash) -> Option { match self { - Repo::Std(repo) => repo.get(key), - Repo::StdMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), - Repo::Tokio(repo) => repo.get(key).await, - Repo::TokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::RwLockStd(repo) => repo.get(key), + Repo::RwLockStdMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::RwLockStdMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::RwLockTokio(repo) => repo.get(key).await, + Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), + Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } + pub(crate) async fn get_metrics(&self) -> TorrentsMetrics { match self { - Repo::Std(repo) => repo.get_metrics(), - Repo::StdMutexStd(repo) => repo.get_metrics(), - Repo::StdMutexTokio(repo) => repo.get_metrics().await, - Repo::Tokio(repo) => repo.get_metrics().await, - Repo::TokioMutexStd(repo) => repo.get_metrics().await, - Repo::TokioMutexTokio(repo) => repo.get_metrics().await, + Repo::RwLockStd(repo) => repo.get_metrics(), + Repo::RwLockStdMutexStd(repo) => repo.get_metrics(), + Repo::RwLockStdMutexTokio(repo) => repo.get_metrics().await, + Repo::RwLockTokio(repo) => repo.get_metrics().await, + Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, + Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, + Repo::SkipMapMutexStd(repo) => repo.get_metrics(), } } + pub(crate) async fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntrySingle)> { match self { - Repo::Std(repo) => repo.get_paginated(pagination), - Repo::StdMutexStd(repo) => repo + Repo::RwLockStd(repo) => repo.get_paginated(pagination), + Repo::RwLockStdMutexStd(repo) => repo .get_paginated(pagination) .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), - Repo::StdMutexTokio(repo) => { + Repo::RwLockStdMutexTokio(repo) => { let mut v: Vec<(InfoHash, EntrySingle)> = vec![]; for (i, t) in repo.get_paginated(pagination).await { @@ -57,14 +62,14 @@ impl Repo { } v } - Repo::Tokio(repo) => repo.get_paginated(pagination).await, - Repo::TokioMutexStd(repo) => repo + Repo::RwLockTokio(repo) => repo.get_paginated(pagination).await, + Repo::RwLockTokioMutexStd(repo) => repo .get_paginated(pagination) .await .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), - Repo::TokioMutexTokio(repo) => { + Repo::RwLockTokioMutexTokio(repo) => { let mut v: Vec<(InfoHash, EntrySingle)> = vec![]; for (i, t) in repo.get_paginated(pagination).await { @@ -72,76 +77,102 @@ impl Repo { } v } + Repo::SkipMapMutexStd(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) + .collect(), } } + pub(crate) async fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { match self { - Repo::Std(repo) => repo.import_persistent(persistent_torrents), - Repo::StdMutexStd(repo) => repo.import_persistent(persistent_torrents), - Repo::StdMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, - Repo::Tokio(repo) => repo.import_persistent(persistent_torrents).await, - Repo::TokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, - Repo::TokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockStd(repo) => repo.import_persistent(persistent_torrents), + Repo::RwLockStdMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::RwLockStdMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } + pub(crate) async fn remove(&self, key: &InfoHash) -> Option { match self { - Repo::Std(repo) => repo.remove(key), - Repo::StdMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), - Repo::Tokio(repo) => repo.remove(key).await, - Repo::TokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::RwLockStd(repo) => repo.remove(key), + Repo::RwLockStdMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::RwLockStdMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::RwLockTokio(repo) => repo.remove(key).await, + Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), + Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } + pub(crate) async fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { match self { - Repo::Std(repo) => repo.remove_inactive_peers(current_cutoff), - Repo::StdMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), - Repo::StdMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::Tokio(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::TokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::TokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::RwLockStdMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::RwLockStdMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } + pub(crate) async fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { match self { - Repo::Std(repo) => repo.remove_peerless_torrents(policy), - Repo::StdMutexStd(repo) => repo.remove_peerless_torrents(policy), - Repo::StdMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, - Repo::Tokio(repo) => repo.remove_peerless_torrents(policy).await, - Repo::TokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, - Repo::TokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockStd(repo) => repo.remove_peerless_torrents(policy), + Repo::RwLockStdMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::RwLockStdMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } + pub(crate) async fn update_torrent_with_peer_and_get_stats( &self, info_hash: &InfoHash, peer: &peer::Peer, ) -> (bool, SwarmMetadata) { match self { - Repo::Std(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::StdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::StdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::Tokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::TokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::TokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::RwLockStdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::RwLockStdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), } } + pub(crate) async fn insert(&self, info_hash: &InfoHash, torrent: EntrySingle) -> Option { match self { - Repo::Std(repo) => repo.write().insert(*info_hash, torrent), - Repo::StdMutexStd(repo) => Some(repo.write().insert(*info_hash, torrent.into())?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => { - let r = repo.write().insert(*info_hash, torrent.into()); - match r { - Some(t) => Some(t.lock().await.clone()), - None => None, - } + Repo::RwLockStd(repo) => { + repo.write().insert(*info_hash, torrent); } - Repo::Tokio(repo) => repo.write().await.insert(*info_hash, torrent), - Repo::TokioMutexStd(repo) => Some(repo.write().await.insert(*info_hash, torrent.into())?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.write().await.insert(*info_hash, torrent.into())?.lock().await.clone()), - } + Repo::RwLockStdMutexStd(repo) => { + repo.write().insert(*info_hash, torrent.into()); + } + Repo::RwLockStdMutexTokio(repo) => { + repo.write().insert(*info_hash, torrent.into()); + } + Repo::RwLockTokio(repo) => { + repo.write().await.insert(*info_hash, torrent); + } + Repo::RwLockTokioMutexStd(repo) => { + repo.write().await.insert(*info_hash, torrent.into()); + } + Repo::RwLockTokioMutexTokio(repo) => { + repo.write().await.insert(*info_hash, torrent.into()); + } + Repo::SkipMapMutexStd(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } + }; + self.get(info_hash).await } } diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 117f3c0a6..ab9648584 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -10,6 +10,7 @@ use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; +use torrust_tracker_torrent_repository::repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_torrent_repository::EntrySingle; use crate::common::repo::Repo; @@ -17,30 +18,37 @@ use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; #[fixture] fn standard() -> Repo { - Repo::Std(RwLockStd::default()) + Repo::RwLockStd(RwLockStd::default()) } + #[fixture] fn standard_mutex() -> Repo { - Repo::StdMutexStd(RwLockStd::default()) + Repo::RwLockStdMutexStd(RwLockStd::default()) } #[fixture] fn standard_tokio() -> Repo { - Repo::StdMutexTokio(RwLockStd::default()) + Repo::RwLockStdMutexTokio(RwLockStd::default()) } #[fixture] fn tokio_std() -> Repo { - Repo::Tokio(RwLockTokio::default()) + Repo::RwLockTokio(RwLockTokio::default()) } + #[fixture] fn tokio_mutex() -> Repo { - Repo::TokioMutexStd(RwLockTokio::default()) + Repo::RwLockTokioMutexStd(RwLockTokio::default()) } #[fixture] fn tokio_tokio() -> Repo { - Repo::TokioMutexTokio(RwLockTokio::default()) + Repo::RwLockTokioMutexTokio(RwLockTokio::default()) +} + +#[fixture] +fn skip_list_std() -> Repo { + Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } type Entries = Vec<(InfoHash, EntrySingle)>; @@ -224,7 +232,16 @@ fn policy_remove_persist() -> TrackerPolicy { #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_a_torrent_entry( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { make(&repo, &entries).await; @@ -247,7 +264,16 @@ async fn it_should_get_a_torrent_entry( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, many_out_of_order: Entries, ) { @@ -280,7 +306,16 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_paginated( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(paginated_limit_zero(), paginated_limit_one(), paginated_limit_one_offset_one())] paginated: Pagination, ) { @@ -328,7 +363,16 @@ async fn it_should_get_paginated( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_metrics( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; @@ -360,7 +404,16 @@ async fn it_should_get_metrics( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_import_persistent_torrents( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(persistent_empty(), persistent_single(), persistent_three())] persistent_torrents: PersistentTorrents, ) { @@ -389,7 +442,16 @@ async fn it_should_import_persistent_torrents( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_an_entry( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { make(&repo, &entries).await; @@ -416,7 +478,16 @@ async fn it_should_remove_an_entry( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_inactive_peers( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { use std::ops::Sub as _; @@ -489,7 +560,16 @@ async fn it_should_remove_inactive_peers( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_peerless_torrents( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { From 537349376e4b5cc1d8b0347e2806ceef7a5e9353 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 8 Apr 2024 16:39:06 +0100 Subject: [PATCH 014/217] chore(deps): update dependencies ```output cargo update Updating crates.io index Updating async-compression v0.4.6 -> v0.4.8 Updating async-executor v1.8.0 -> v1.10.0 Updating autocfg v1.1.0 -> v1.2.0 Adding base64 v0.22.0 Updating borsh v1.3.1 -> v1.4.0 Updating borsh-derive v1.3.1 -> v1.4.0 Updating brotli v3.5.0 -> v4.0.0 Updating brotli-decompressor v2.5.1 -> v3.0.0 Updating bumpalo v3.15.4 -> v3.16.0 Updating cc v1.0.90 -> v1.0.91 Updating chrono v0.4.35 -> v0.4.37 Updating event-listener v5.2.0 -> v5.3.0 Updating event-listener-strategy v0.5.0 -> v0.5.1 Updating getrandom v0.2.12 -> v0.2.14 Updating h2 v0.4.3 -> v0.4.4 Updating half v2.4.0 -> v2.4.1 Updating itoa v1.0.10 -> v1.0.11 Updating memchr v2.7.1 -> v2.7.2 Updating openssl-sys v0.9.101 -> v0.9.102 Updating pest v2.7.8 -> v2.7.9 Updating pest_derive v2.7.8 -> v2.7.9 Updating pest_generator v2.7.8 -> v2.7.9 Updating pest_meta v2.7.8 -> v2.7.9 Updating pin-project-lite v0.2.13 -> v0.2.14 Updating regex-syntax v0.8.2 -> v0.8.3 Updating reqwest v0.12.2 -> v0.12.3 Updating rust_decimal v1.34.3 -> v1.35.0 Removing rustls-pemfile v1.0.4 Removing rustls-pemfile v2.1.1 Adding rustls-pemfile v2.1.2 Updating rustls-pki-types v1.4.0 -> v1.4.1 Updating rustversion v1.0.14 -> v1.0.15 Updating security-framework v2.9.2 -> v2.10.0 Updating security-framework-sys v2.9.1 -> v2.10.0 Updating serde_html_form v0.2.5 -> v0.2.6 Updating serde_json v1.0.114 -> v1.0.115 Updating strsim v0.11.0 -> v0.11.1 Updating syn v2.0.55 -> v2.0.58 Updating sync_wrapper v1.0.0 -> v1.0.1 Updating tokio v1.36.0 -> v1.37.0 Updating winreg v0.50.0 -> v0.52.0 Updating zstd v0.13.0 -> v0.13.1 Updating zstd-safe v7.0.0 -> v7.1.0 Updating zstd-sys v2.0.9+zstd.1.5.5 -> v2.0.10+zstd.1.5.6 ``` --- Cargo.lock | 261 ++++++++++++++++++++++++++--------------------------- 1 file changed, 129 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74d9aaafd..853d21533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,17 +195,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener 5.3.0", + "event-listener-strategy 0.5.1", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" dependencies = [ "brotli", "flate2", @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "5f98c37cf288e302c16ef6c8472aad1e034c6c84ce5ea7b8101c98eb4a802fee" dependencies = [ "async-lock 3.3.0", "async-task", @@ -347,7 +347,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -358,9 +358,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" @@ -389,7 +389,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.0", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -461,7 +461,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -480,7 +480,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls", - "rustls-pemfile 2.1.1", + "rustls-pemfile", "tokio", "tokio-rustls", "tower", @@ -508,6 +508,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -542,7 +548,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -599,9 +605,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" dependencies = [ "borsh-derive", "cfg_aliases", @@ -609,23 +615,23 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "syn_derive", ] [[package]] name = "brotli" -version = "3.5.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -634,9 +640,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -650,9 +656,9 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -696,9 +702,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "1fd97381a8cc6493395a5afc4c691c1084b3768db713b73aa215217aa245d153" dependencies = [ "jobserver", "libc", @@ -727,9 +733,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -795,7 +801,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim 0.11.1", ] [[package]] @@ -807,7 +813,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1080,7 +1086,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1091,7 +1097,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1125,7 +1131,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1213,9 +1219,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", @@ -1234,11 +1240,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.3.0", "pin-project-lite", ] @@ -1360,7 +1366,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1372,7 +1378,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1384,7 +1390,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1477,7 +1483,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1528,9 +1534,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1563,9 +1569,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -1582,9 +1588,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1887,9 +1893,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -2118,9 +2124,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" @@ -2178,7 +2184,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2229,7 +2235,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "termcolor", "thiserror", ] @@ -2240,7 +2246,7 @@ version = "0.30.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57349d5a326b437989b6ee4dc8f2f34b0cc131202748414712a8e7d98952fc8c" dependencies = [ - "base64", + "base64 0.21.7", "bigdecimal", "bindgen", "bitflags 2.5.0", @@ -2429,7 +2435,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2440,9 +2446,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2501,7 +2507,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" dependencies = [ - "base64", + "base64 0.21.7", "serde", ] @@ -2513,9 +2519,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" dependencies = [ "memchr", "thiserror", @@ -2524,9 +2530,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" dependencies = [ "pest", "pest_generator", @@ -2534,22 +2540,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" dependencies = [ "once_cell", "pest", @@ -2611,14 +2617,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2943,9 +2949,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "relative-path" @@ -2964,11 +2970,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" dependencies = [ - "base64", + "base64 0.22.0", "bytes", "encoding_rs", "futures-core", @@ -2988,7 +2994,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", @@ -3063,7 +3069,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.5.0", "serde", "serde_derive", @@ -3094,7 +3100,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.55", + "syn 2.0.58", "unicode-ident", ] @@ -3124,9 +3130,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.3" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" dependencies = [ "arrayvec", "borsh", @@ -3200,28 +3206,19 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64", -] - -[[package]] -name = "rustls-pemfile" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" -dependencies = [ - "base64", + "base64 0.22.0", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" [[package]] name = "rustls-webpki" @@ -3235,9 +3232,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" @@ -3302,9 +3299,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3315,9 +3312,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3365,14 +3362,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "serde_html_form" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50437e6a58912eecc08865e35ea2e8d365fbb2db0debb1c8bb43bf1faf055f25" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" dependencies = [ "form_urlencoded", "indexmap 2.2.6", @@ -3383,9 +3380,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -3410,7 +3407,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3440,7 +3437,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ - "base64", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", @@ -3461,7 +3458,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3568,9 +3565,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subprocess" @@ -3595,9 +3592,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -3613,7 +3610,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3624,9 +3621,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "system-configuration" @@ -3716,7 +3713,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3786,9 +3783,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -3810,7 +3807,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4114,7 +4111,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -4285,7 +4282,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -4319,7 +4316,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4532,9 +4529,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -4575,32 +4572,32 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "zstd" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.10+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" dependencies = [ "cc", "pkg-config", From 78b46c41a5c50f5016f745634fe5101969e5a828 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 13:24:41 +0100 Subject: [PATCH 015/217] chore(deps): add cargo dependency: dashmap It will be used to create a new torrent repository implementation, using a DashMap for the torrent list. DashMap crate: https://crates.io/crates/dashmap --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + cSpell.json | 1 + packages/torrent-repository/Cargo.toml | 1 + 4 files changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 853d21533..6b9be523c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,6 +1100,19 @@ dependencies = [ "syn 2.0.58", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" @@ -3916,6 +3929,7 @@ dependencies = [ "colored", "config", "crossbeam-skiplist", + "dashmap", "derive_more", "fern", "futures", @@ -4022,6 +4036,7 @@ dependencies = [ "async-std", "criterion", "crossbeam-skiplist", + "dashmap", "futures", "rstest", "tokio", diff --git a/Cargo.toml b/Cargo.toml index f440799cc..dfb06168d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ clap = { version = "4", features = ["derive", "env"] } colored = "2" config = "0" crossbeam-skiplist = "0.1" +dashmap = "5.5.3" derive_more = "0" fern = "0" futures = "0" diff --git a/cSpell.json b/cSpell.json index 0480590af..24ef6b0a0 100644 --- a/cSpell.json +++ b/cSpell.json @@ -163,6 +163,7 @@ "Weidendorfer", "Werror", "whitespaces", + "Xacrimon", "XBTT", "Xdebug", "Xeon", diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 5f1a20d32..6bc8bfcdd 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -17,6 +17,7 @@ version.workspace = true [dependencies] crossbeam-skiplist = "0.1" +dashmap = "5.5.3" futures = "0.3.29" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } From 00ee9db340c5274a63aa5f321e3bd5064e967b6c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 14:17:10 +0100 Subject: [PATCH 016/217] feat: [#565] new torrent repository implementation usind DashMap It's not enabled as the deafult repository becuase DashMap does not return the items in order. Some tests fail: ``` output failures: ---- core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash stdout ---- thread 'core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash' panicked at src/core/services/torrent.rs:303:13: assertion `left == right` failed left: [BasicInfo { info_hash: InfoHash([158, 2, 23, 208, 250, 113, 200, 115, 50, 205, 139, 249, 219, 234, 188, 178, 194, 207, 60, 77]), seeders: 1, completed: 0, leechers: 0 }, BasicInfo { info_hash: InfoHash([3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58]), seeders: 1, completed: 0, leechers: 0 }] right: [BasicInfo { info_hash: InfoHash([3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58]), seeders: 1, completed: 0, leechers: 0 }, BasicInfo { info_hash: InfoHash([158, 2, 23, 208, 250, 113, 200, 115, 50, 205, 139, 249, 219, 234, 188, 178, 194, 207, 60, 77]), seeders: 1, completed: 0, leechers: 0 }] note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash test result: FAILED. 212 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.18s error: test failed, to rerun pass `--lib` ``` On the other hand, to use it, a new data strcuture should be added to the repo: An Index with sorted torrents by Infohash The API uses pagination returning torrents in alphabetically order by InfoHash. Adding such an Index would probably decrease the performace of this repository implementation. And it's performace looks similar to the current SkipMap implementation. SkipMap performace with Aquatic UDP load test: ``` Requests out: 396788.68/second Responses in: 357105.27/second - Connect responses: 176662.91 - Announce responses: 176863.44 - Scrape responses: 3578.91 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 105 - p99.9: 287 - p100: 351 ``` DashMap performace with Aquatic UDP load test: ``` Requests out: 410658.38/second Responses in: 365892.86/second - Connect responses: 181258.91 - Announce responses: 181005.95 - Scrape responses: 3628.00 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 295 - p100: 363 ``` --- .../benches/repository_benchmark.rs | 23 +++- packages/torrent-repository/src/lib.rs | 2 + .../src/repository/dash_map_mutex_std.rs | 106 ++++++++++++++++++ .../torrent-repository/src/repository/mod.rs | 1 + .../torrent-repository/tests/common/repo.rs | 20 +++- .../tests/repository/mod.rs | 30 +++-- 6 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 packages/torrent-repository/src/repository/dash_map_mutex_std.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 65608c86c..58cd70d9a 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -4,8 +4,8 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ - TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, - TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; use crate::helpers::{asyn, sync}; @@ -49,6 +49,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("DashMapMutexStd", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.finish(); } @@ -98,6 +102,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -147,6 +156,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -197,6 +211,11 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index ccaf579e3..7a6d209b9 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use repository::dash_map_mutex_std::XacrimonDashMap; use repository::rw_lock_std::RwLockStd; use repository::rw_lock_tokio::RwLockTokio; use repository::skip_map_mutex_std::CrossbeamSkipList; @@ -20,6 +21,7 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsDashMapMutexStd = XacrimonDashMap; /// This code needs to be copied into each crate. /// Working version, for production. diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs new file mode 100644 index 000000000..67c47973e --- /dev/null +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use dashmap::DashMap; +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; + +use super::Repository; +use crate::entry::{Entry, EntrySync}; +use crate::{EntryMutexStd, EntrySingle}; + +#[derive(Default, Debug)] +pub struct XacrimonDashMap { + pub torrents: DashMap, +} + +impl Repository for XacrimonDashMap +where + EntryMutexStd: EntrySync, + EntrySingle: Entry, +{ + fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + if let Some(entry) = self.torrents.get(info_hash) { + entry.insert_or_update_peer_and_get_stats(peer) + } else { + let _unused = self.torrents.insert(*info_hash, Arc::default()); + + match self.torrents.get(info_hash) { + Some(entry) => entry.insert_or_update_peer_and_get_stats(peer), + None => (false, SwarmMetadata::zeroed()), + } + } + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().expect("it should get a lock").get_stats(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexStd::new( + EntrySingle { + peers: BTreeMap::default(), + downloaded: *completed, + } + .into(), + ); + + self.torrents.insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|(_key, value)| value.clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + self.torrents.retain(|_, entry| entry.is_good(policy)); + } +} diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 975a876d8..c7c64c54a 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -5,6 +5,7 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; +pub mod dash_map_mutex_std; pub mod rw_lock_std; pub mod rw_lock_std_mutex_std; pub mod rw_lock_std_mutex_tokio; diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 5a86aa3cf..5a6eddf97 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -6,8 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ - EntrySingle, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, + TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; #[derive(Debug)] @@ -19,6 +19,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + DashMapMutexStd(TorrentsDashMapMutexStd), } impl Repo { @@ -31,6 +32,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } @@ -43,6 +45,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::DashMapMutexStd(repo) => repo.get_metrics(), } } @@ -82,6 +85,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::DashMapMutexStd(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) + .collect(), } } @@ -94,6 +102,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } @@ -106,6 +115,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } @@ -118,6 +128,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } @@ -130,6 +141,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } @@ -146,6 +158,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::DashMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), } } @@ -172,6 +185,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::DashMapMutexStd(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } }; self.get(info_hash).await } diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index ab9648584..1854b89ac 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -8,6 +8,7 @@ use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; +use torrust_tracker_torrent_repository::repository::dash_map_mutex_std::XacrimonDashMap; use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; use torrust_tracker_torrent_repository::repository::skip_map_mutex_std::CrossbeamSkipList; @@ -51,6 +52,11 @@ fn skip_list_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn dash_map_std() -> Repo { + Repo::DashMapMutexStd(XacrimonDashMap::default()) +} + type Entries = Vec<(InfoHash, EntrySingle)>; #[fixture] @@ -239,7 +245,8 @@ async fn it_should_get_a_torrent_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -271,7 +278,8 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -313,7 +321,8 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -370,7 +379,8 @@ async fn it_should_get_metrics( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -411,7 +421,8 @@ async fn it_should_import_persistent_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -449,7 +460,8 @@ async fn it_should_remove_an_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -485,7 +497,8 @@ async fn it_should_remove_inactive_peers( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -567,7 +580,8 @@ async fn it_should_remove_peerless_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, From 1e76c1700cd1cb5e69e01df34dfe19a7a9828153 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 16:35:35 +0100 Subject: [PATCH 017/217] chore: add dashmap cargo dep to cargp machete It's only used for benchmarking. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dfb06168d..ef0c39d4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ url = "2" uuid = { version = "1", features = ["v4"] } [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist"] +ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap"] [dev-dependencies] local-ip-address = "0" From 4030fd12b575c039dbb1507fbe99e0aaec58fd5f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 17:12:07 +0100 Subject: [PATCH 018/217] fix: torrent repository tests. DashMap is not ordered DashMap implementation does not support returning torrent ordered, so we have to skip those tests for this reporitoy implementation. --- packages/torrent-repository/tests/repository/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 1854b89ac..a6784bf57 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -278,8 +278,7 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), - dash_map_std() + skip_list_std() )] repo: Repo, #[case] entries: Entries, @@ -321,8 +320,7 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), - dash_map_std() + skip_list_std() )] repo: Repo, #[case] entries: Entries, From aa4bfbaf5496de32a02bc1d23779ba200974f2a0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 12 Apr 2024 15:54:49 +0100 Subject: [PATCH 019/217] refactor: segregate command and query for announce request This changes the API of the torrent repository. The method: ``` fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata); ``` is replaced with: ``` fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer); fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option; ``` The performance is not affected. Benchmaring is still using both methods in order to simulate `announce` requests. 1. The interface is simpler (command/query segregation. 2. In the long-term: - Returning swarm metadata in the announce request could be optional. The announce request process would be faster if the tracker does not have to mantain the swarm data. This is not likely to happen becuase the scrape request needs this metadata. - New repository performance improvements could be implemented. This allow decoupling peer lists from swarm metadata. The repository internally can have two data strcutures one for the peer list and another for the swarm metatada. Both using different locks. --- .../benches/helpers/asyn.rs | 34 +++++------ .../benches/helpers/sync.rs | 22 +++++-- packages/torrent-repository/src/entry/mod.rs | 20 ++---- .../torrent-repository/src/entry/mutex_std.rs | 14 ++--- .../src/entry/mutex_tokio.rs | 12 ++-- .../torrent-repository/src/entry/single.rs | 10 +-- .../src/repository/dash_map_mutex_std.rs | 16 ++--- .../torrent-repository/src/repository/mod.rs | 10 ++- .../src/repository/rw_lock_std.rs | 10 ++- .../src/repository/rw_lock_std_mutex_std.rs | 12 +++- .../src/repository/rw_lock_std_mutex_tokio.rs | 16 ++++- .../src/repository/rw_lock_tokio.rs | 11 +++- .../src/repository/rw_lock_tokio_mutex_std.rs | 11 +++- .../repository/rw_lock_tokio_mutex_tokio.rs | 14 ++++- .../src/repository/skip_map_mutex_std.rs | 10 ++- .../torrent-repository/tests/common/repo.rs | 43 +++++++------ .../tests/common/torrent.rs | 22 +++---- .../torrent-repository/tests/entry/mod.rs | 41 +++++++------ .../tests/repository/mod.rs | 40 ++++++++---- src/core/mod.rs | 61 +++++++++++-------- src/core/services/torrent.rs | 38 ++++-------- src/servers/udp/handlers.rs | 12 +--- tests/servers/api/environment.rs | 2 +- tests/servers/http/environment.rs | 2 +- tests/servers/udp/environment.rs | 2 +- 25 files changed, 259 insertions(+), 226 deletions(-) diff --git a/packages/torrent-repository/benches/helpers/asyn.rs b/packages/torrent-repository/benches/helpers/asyn.rs index 80f70cdc2..1c6d9d915 100644 --- a/packages/torrent-repository/benches/helpers/asyn.rs +++ b/packages/torrent-repository/benches/helpers/asyn.rs @@ -18,9 +18,9 @@ where let info_hash = InfoHash([0; 20]); - torrent_repository - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(&info_hash, &DEFAULT_PEER).await; + + torrent_repository.get_swarm_metadata(&info_hash).await; } start.elapsed() @@ -37,9 +37,9 @@ where let handles = FuturesUnordered::new(); // Add the torrent/peer to the torrent repository - torrent_repository - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER).await; + + torrent_repository.get_swarm_metadata(info_hash).await; let start = Instant::now(); @@ -47,9 +47,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(info_hash, &DEFAULT_PEER).await; + + torrent_repository_clone.get_swarm_metadata(info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -87,9 +87,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER).await; + + torrent_repository_clone.get_swarm_metadata(&info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -123,9 +123,8 @@ where // Add the torrents/peers to the torrent repository for info_hash in &info_hashes { - torrent_repository - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER).await; + torrent_repository.get_swarm_metadata(info_hash).await; } let start = Instant::now(); @@ -134,9 +133,8 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER).await; + torrent_repository_clone.get_swarm_metadata(&info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); diff --git a/packages/torrent-repository/benches/helpers/sync.rs b/packages/torrent-repository/benches/helpers/sync.rs index 0523f4141..63fccfc77 100644 --- a/packages/torrent-repository/benches/helpers/sync.rs +++ b/packages/torrent-repository/benches/helpers/sync.rs @@ -20,7 +20,9 @@ where let info_hash = InfoHash([0; 20]); - torrent_repository.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(&info_hash, &DEFAULT_PEER); + + torrent_repository.get_swarm_metadata(&info_hash); } start.elapsed() @@ -37,7 +39,9 @@ where let handles = FuturesUnordered::new(); // Add the torrent/peer to the torrent repository - torrent_repository.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER); + + torrent_repository.get_swarm_metadata(info_hash); let start = Instant::now(); @@ -45,7 +49,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(info_hash, &DEFAULT_PEER); + + torrent_repository_clone.get_swarm_metadata(info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -83,7 +89,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER); + + torrent_repository_clone.get_swarm_metadata(&info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -117,7 +125,8 @@ where // Add the torrents/peers to the torrent repository for info_hash in &info_hashes { - torrent_repository.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER); + torrent_repository.get_swarm_metadata(info_hash); } let start = Instant::now(); @@ -126,7 +135,8 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.get_swarm_metadata(&info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 4c39af829..d72ff254b 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -15,7 +15,7 @@ pub trait Entry { /// It returns the swarm metadata (statistics) as a struct: /// /// `(seeders, completed, leechers)` - fn get_stats(&self) -> SwarmMetadata; + fn get_swarm_metadata(&self) -> SwarmMetadata; /// Returns True if Still a Valid Entry according to the Tracker Policy fn is_good(&self, policy: &TrackerPolicy) -> bool; @@ -40,10 +40,7 @@ pub trait Entry { /// /// The number of peers that have complete downloading is synchronously updated when peers are updated. /// That's the total torrent downloads counter. - fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool; - - // It preforms a combined operation of `insert_or_update_peer` and `get_stats`. - fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&mut self, peer: &peer::Peer) -> bool; /// It removes peer from the swarm that have not been updated for more than `current_cutoff` seconds fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch); @@ -51,20 +48,19 @@ pub trait Entry { #[allow(clippy::module_name_repetitions)] pub trait EntrySync { - fn get_stats(&self) -> SwarmMetadata; + fn get_swarm_metadata(&self) -> SwarmMetadata; fn is_good(&self, policy: &TrackerPolicy) -> bool; fn peers_is_empty(&self) -> bool; fn get_peers_len(&self) -> usize; fn get_peers(&self, limit: Option) -> Vec>; fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec>; - fn insert_or_update_peer(&self, peer: &peer::Peer) -> bool; - fn insert_or_update_peer_and_get_stats(&self, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&self, peer: &peer::Peer) -> bool; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch); } #[allow(clippy::module_name_repetitions)] pub trait EntryAsync { - fn get_stats(&self) -> impl std::future::Future + Send; + fn get_swarm_metadata(&self) -> impl std::future::Future + Send; fn check_good(self, policy: &TrackerPolicy) -> impl std::future::Future + Send; fn peers_is_empty(&self) -> impl std::future::Future + Send; fn get_peers_len(&self) -> impl std::future::Future + Send; @@ -74,11 +70,7 @@ pub trait EntryAsync { client: &SocketAddr, limit: Option, ) -> impl std::future::Future>> + Send; - fn insert_or_update_peer(self, peer: &peer::Peer) -> impl std::future::Future + Send; - fn insert_or_update_peer_and_get_stats( - self, - peer: &peer::Peer, - ) -> impl std::future::Future + std::marker::Send; + fn upsert_peer(self, peer: &peer::Peer) -> impl std::future::Future + Send; fn remove_inactive_peers(self, current_cutoff: DurationSinceUnixEpoch) -> impl std::future::Future + Send; } diff --git a/packages/torrent-repository/src/entry/mutex_std.rs b/packages/torrent-repository/src/entry/mutex_std.rs index b4b823909..990d8ab76 100644 --- a/packages/torrent-repository/src/entry/mutex_std.rs +++ b/packages/torrent-repository/src/entry/mutex_std.rs @@ -9,8 +9,8 @@ use super::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; impl EntrySync for EntryMutexStd { - fn get_stats(&self) -> SwarmMetadata { - self.lock().expect("it should get a lock").get_stats() + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.lock().expect("it should get a lock").get_swarm_metadata() } fn is_good(&self, policy: &TrackerPolicy) -> bool { @@ -33,14 +33,8 @@ impl EntrySync for EntryMutexStd { self.lock().expect("it should get lock").get_peers_for_client(client, limit) } - fn insert_or_update_peer(&self, peer: &peer::Peer) -> bool { - self.lock().expect("it should lock the entry").insert_or_update_peer(peer) - } - - fn insert_or_update_peer_and_get_stats(&self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - self.lock() - .expect("it should lock the entry") - .insert_or_update_peer_and_get_stats(peer) + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.lock().expect("it should lock the entry").upsert_peer(peer) } fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { diff --git a/packages/torrent-repository/src/entry/mutex_tokio.rs b/packages/torrent-repository/src/entry/mutex_tokio.rs index 34f4a4e92..c5363e51a 100644 --- a/packages/torrent-repository/src/entry/mutex_tokio.rs +++ b/packages/torrent-repository/src/entry/mutex_tokio.rs @@ -9,8 +9,8 @@ use super::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle}; impl EntryAsync for EntryMutexTokio { - async fn get_stats(&self) -> SwarmMetadata { - self.lock().await.get_stats() + async fn get_swarm_metadata(&self) -> SwarmMetadata { + self.lock().await.get_swarm_metadata() } async fn check_good(self, policy: &TrackerPolicy) -> bool { @@ -33,12 +33,8 @@ impl EntryAsync for EntryMutexTokio { self.lock().await.get_peers_for_client(client, limit) } - async fn insert_or_update_peer(self, peer: &peer::Peer) -> bool { - self.lock().await.insert_or_update_peer(peer) - } - - async fn insert_or_update_peer_and_get_stats(self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - self.lock().await.insert_or_update_peer_and_get_stats(peer) + async fn upsert_peer(self, peer: &peer::Peer) -> bool { + self.lock().await.upsert_peer(peer) } async fn remove_inactive_peers(self, current_cutoff: DurationSinceUnixEpoch) { diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index c1041e9a2..a38b54023 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -12,7 +12,7 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] - fn get_stats(&self) -> SwarmMetadata { + fn get_swarm_metadata(&self) -> SwarmMetadata { let complete: u32 = self.peers.values().filter(|peer| peer.is_seeder()).count() as u32; let incomplete: u32 = self.peers.len() as u32 - complete; @@ -70,7 +70,7 @@ impl Entry for EntrySingle { } } - fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool { + fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { let mut downloaded_stats_updated: bool = false; match peer::ReadInfo::get_event(peer) { @@ -93,12 +93,6 @@ impl Entry for EntrySingle { downloaded_stats_updated } - fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - let changed = self.insert_or_update_peer(peer); - let stats = self.get_stats(); - (changed, stats) - } - fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { self.peers .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 67c47973e..b398b09dc 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -23,19 +23,21 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { if let Some(entry) = self.torrents.get(info_hash) { - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } else { let _unused = self.torrents.insert(*info_hash, Arc::default()); - - match self.torrents.get(info_hash) { - Some(entry) => entry.insert_or_update_peer_and_get_stats(peer), - None => (false, SwarmMetadata::zeroed()), + if let Some(entry) = self.torrents.get(info_hash) { + entry.upsert_peer(peer); } } } + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + fn get(&self, key: &InfoHash) -> Option { let maybe_entry = self.torrents.get(key); maybe_entry.map(|entry| entry.clone()) @@ -45,7 +47,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in &self.torrents { - let stats = entry.value().lock().expect("it should get a lock").get_stats(); + let stats = entry.value().lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index c7c64c54a..f198288f8 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -24,7 +24,8 @@ pub trait Repository: Debug + Default + Sized + 'static { fn remove(&self, key: &InfoHash) -> Option; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch); fn remove_peerless_torrents(&self, policy: &TrackerPolicy); - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer); + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option; } #[allow(clippy::module_name_repetitions)] @@ -36,9 +37,6 @@ pub trait RepositoryAsync: Debug + Default + Sized + 'static { fn remove(&self, key: &InfoHash) -> impl std::future::Future> + Send; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) -> impl std::future::Future + Send; fn remove_peerless_torrents(&self, policy: &TrackerPolicy) -> impl std::future::Future + Send; - fn update_torrent_with_peer_and_get_stats( - &self, - info_hash: &InfoHash, - peer: &peer::Peer, - ) -> impl std::future::Future + Send; + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) -> impl std::future::Future + Send; + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> impl std::future::Future> + Send; } diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index e9074a271..af48428e4 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -47,12 +47,16 @@ impl Repository for TorrentsRwLockStd where EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let mut db = self.get_torrents_mut(); let entry = db.entry(*info_hash).or_insert(EntrySingle::default()); - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).map(|entry| entry.get_swarm_metadata()) } fn get(&self, key: &InfoHash) -> Option { @@ -64,7 +68,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 0b65234e3..74cdc4475 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -33,7 +33,7 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -44,7 +44,13 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get_torrents() + .get(info_hash) + .map(super::super::entry::EntrySync::get_swarm_metadata) } fn get(&self, key: &InfoHash) -> Option { @@ -56,7 +62,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().values() { - let stats = entry.lock().expect("it should get a lock").get_stats(); + let stats = entry.lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 5394abb6a..83ac02c91 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -37,7 +37,7 @@ where EntryMutexTokio: EntryAsync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -48,8 +48,18 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer).await + entry.upsert_peer(peer).await; } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + let maybe_entry = self.get_torrents().get(info_hash).cloned(); + + match maybe_entry { + Some(entry) => Some(entry.get_swarm_metadata().await), + None => None, + } + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents(); db.get(key).cloned() @@ -75,7 +85,7 @@ where let entries: Vec<_> = self.get_torrents().values().cloned().collect(); for entry in entries { - let stats = entry.lock().await.get_stats(); + let stats = entry.lock().await.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index d84074eaf..b95f1e31e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -51,13 +51,18 @@ impl RepositoryAsync for TorrentsRwLockTokio where EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let mut db = self.get_torrents_mut().await; let entry = db.entry(*info_hash).or_insert(EntrySingle::default()); - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).await.map(|entry| entry.get_swarm_metadata()) + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -81,7 +86,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index fbbc51a09..bde959940 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -35,7 +35,7 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().await.get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -46,8 +46,13 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).await.map(|entry| entry.get_swarm_metadata()) + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -71,7 +76,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index bc7fd61e8..1d002e317 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -35,7 +35,7 @@ where EntryMutexTokio: EntryAsync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().await.get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -46,8 +46,16 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer).await + entry.upsert_peer(peer).await; } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + match self.get(info_hash).await { + Some(entry) => Some(entry.get_swarm_metadata().await), + None => None, + } + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -71,7 +79,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats().await; + let stats = entry.get_swarm_metadata().await; metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 0c0127b15..ef3e7e478 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -23,9 +23,13 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); - entry.value().insert_or_update_peer_and_get_stats(peer) + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) } fn get(&self, key: &InfoHash) -> Option { @@ -37,7 +41,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in &self.torrents { - let stats = entry.value().lock().expect("it should get a lock").get_stats(); + let stats = entry.value().lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 5a6eddf97..7c245fe04 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -23,6 +23,32 @@ pub(crate) enum Repo { } impl Repo { + pub(crate) async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + match self { + Repo::RwLockStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::RwLockStdMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::RwLockStdMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + } + } + + pub(crate) async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + match self { + Repo::RwLockStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::RwLockStdMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::RwLockStdMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + } + } + pub(crate) async fn get(&self, key: &InfoHash) -> Option { match self { Repo::RwLockStd(repo) => repo.get(key), @@ -145,23 +171,6 @@ impl Repo { } } - pub(crate) async fn update_torrent_with_peer_and_get_stats( - &self, - info_hash: &InfoHash, - peer: &peer::Peer, - ) -> (bool, SwarmMetadata) { - match self { - Repo::RwLockStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::RwLockStdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::RwLockStdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::DashMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - } - } - pub(crate) async fn insert(&self, info_hash: &InfoHash, torrent: EntrySingle) -> Option { match self { Repo::RwLockStd(repo) => { diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index 33264c443..c0699479e 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -17,9 +17,9 @@ pub(crate) enum Torrent { impl Torrent { pub(crate) async fn get_stats(&self) -> SwarmMetadata { match self { - Torrent::Single(entry) => entry.get_stats(), - Torrent::MutexStd(entry) => entry.get_stats(), - Torrent::MutexTokio(entry) => entry.clone().get_stats().await, + Torrent::Single(entry) => entry.get_swarm_metadata(), + Torrent::MutexStd(entry) => entry.get_swarm_metadata(), + Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, } } @@ -63,19 +63,11 @@ impl Torrent { } } - pub(crate) async fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool { + pub(crate) async fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { match self { - Torrent::Single(entry) => entry.insert_or_update_peer(peer), - Torrent::MutexStd(entry) => entry.insert_or_update_peer(peer), - Torrent::MutexTokio(entry) => entry.clone().insert_or_update_peer(peer).await, - } - } - - pub(crate) async fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - match self { - Torrent::Single(entry) => entry.insert_or_update_peer_and_get_stats(peer), - Torrent::MutexStd(entry) => entry.insert_or_update_peer_and_get_stats(peer), - Torrent::MutexTokio(entry) => entry.clone().insert_or_update_peer_and_get_stats(peer).await, + Torrent::Single(entry) => entry.upsert_peer(peer), + Torrent::MutexStd(entry) => entry.upsert_peer(peer), + Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index c39bef636..3c564c6f8 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -62,34 +62,34 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { Makes::Empty => vec![], Makes::Started => { let peer = a_started_peer(1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Completed => { let peer = a_completed_peer(2); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Downloaded => { let mut peer = a_started_peer(3); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; peer.event = AnnounceEvent::Completed; peer.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Three => { let peer_1 = a_started_peer(1); - torrent.insert_or_update_peer(&peer_1).await; + torrent.upsert_peer(&peer_1).await; let peer_2 = a_completed_peer(2); - torrent.insert_or_update_peer(&peer_2).await; + torrent.upsert_peer(&peer_2).await; let mut peer_3 = a_started_peer(3); - torrent.insert_or_update_peer(&peer_3).await; + torrent.upsert_peer(&peer_3).await; peer_3.event = AnnounceEvent::Completed; peer_3.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer_3).await; + torrent.upsert_peer(&peer_3).await; vec![peer_1, peer_2, peer_3] } } @@ -182,7 +182,7 @@ async fn it_should_update_a_peer( // Make and insert a new peer. let mut peer = a_started_peer(-1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // Get the Inserted Peer by Id. let peers = torrent.get_peers(None).await; @@ -195,7 +195,7 @@ async fn it_should_update_a_peer( // Announce "Completed" torrent download event. peer.event = AnnounceEvent::Completed; - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // Get the Updated Peer by Id. let peers = torrent.get_peers(None).await; @@ -224,7 +224,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( let mut peer = a_started_peer(-1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // The started peer should be inserted. let peers = torrent.get_peers(None).await; @@ -237,7 +237,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( // Change peer to "Stopped" and insert. peer.event = AnnounceEvent::Stopped; - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // It should be removed now. let peers = torrent.get_peers(None).await; @@ -270,13 +270,12 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade // Announce "Completed" torrent download event. peer.event = AnnounceEvent::Completed; - let (updated, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if is_already_completed { - assert!(!updated); assert_eq!(stats.downloaded, downloaded); } else { - assert!(updated); assert_eq!(stats.downloaded, downloaded + 1); } } @@ -301,7 +300,8 @@ async fn it_should_update_a_peer_as_a_seeder( // Set Bytes Left to Zero peer.left = NumberOfBytes(0); - let (_, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; // Add the peer + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if is_already_non_left { // it was already complete @@ -332,7 +332,8 @@ async fn it_should_update_a_peer_as_incomplete( // Set Bytes Left to no Zero peer.left = NumberOfBytes(1); - let (_, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; // Add the peer + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if completed_already { // now it is incomplete @@ -368,7 +369,7 @@ async fn it_should_get_peers_excluding_the_client_socket( // set the address to the socket. peer.peer_addr = socket; - torrent.insert_or_update_peer(&peer).await; // Add peer + torrent.upsert_peer(&peer).await; // Add peer // It should not include the peer that has the same socket. assert!(!torrent.get_peers_for_client(&socket, None).await.contains(&peer.into())); @@ -391,7 +392,7 @@ async fn it_should_limit_the_number_of_peers_returned( for peer_number in 1..=74 + 1 { let mut peer = a_started_peer(1); peer.peer_id = peer::Id::from(peer_number); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; } let peers = torrent.get_peers(Some(TORRENT_PEERS_LIMIT)).await; @@ -422,7 +423,7 @@ async fn it_should_remove_inactive_peers_beyond_cutoff( peer.updated = now.sub(EXPIRE); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; assert_eq!(torrent.get_peers_len().await, peers.len() + 1); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index a6784bf57..fde34467e 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -6,6 +6,7 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; use torrust_tracker_torrent_repository::repository::dash_map_mutex_std::XacrimonDashMap; @@ -72,14 +73,14 @@ fn default() -> Entries { #[fixture] fn started() -> Entries { let mut torrent = EntrySingle::default(); - torrent.insert_or_update_peer(&a_started_peer(1)); + torrent.upsert_peer(&a_started_peer(1)); vec![(InfoHash::default(), torrent)] } #[fixture] fn completed() -> Entries { let mut torrent = EntrySingle::default(); - torrent.insert_or_update_peer(&a_completed_peer(2)); + torrent.upsert_peer(&a_completed_peer(2)); vec![(InfoHash::default(), torrent)] } @@ -87,10 +88,10 @@ fn completed() -> Entries { fn downloaded() -> Entries { let mut torrent = EntrySingle::default(); let mut peer = a_started_peer(3); - torrent.insert_or_update_peer(&peer); + torrent.upsert_peer(&peer); peer.event = AnnounceEvent::Completed; peer.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer); + torrent.upsert_peer(&peer); vec![(InfoHash::default(), torrent)] } @@ -98,21 +99,21 @@ fn downloaded() -> Entries { fn three() -> Entries { let mut started = EntrySingle::default(); let started_h = &mut DefaultHasher::default(); - started.insert_or_update_peer(&a_started_peer(1)); + started.upsert_peer(&a_started_peer(1)); started.hash(started_h); let mut completed = EntrySingle::default(); let completed_h = &mut DefaultHasher::default(); - completed.insert_or_update_peer(&a_completed_peer(2)); + completed.upsert_peer(&a_completed_peer(2)); completed.hash(completed_h); let mut downloaded = EntrySingle::default(); let downloaded_h = &mut DefaultHasher::default(); let mut downloaded_peer = a_started_peer(3); - downloaded.insert_or_update_peer(&downloaded_peer); + downloaded.upsert_peer(&downloaded_peer); downloaded_peer.event = AnnounceEvent::Completed; downloaded_peer.left = NumberOfBytes(0); - downloaded.insert_or_update_peer(&downloaded_peer); + downloaded.upsert_peer(&downloaded_peer); downloaded.hash(downloaded_h); vec![ @@ -128,7 +129,7 @@ fn many_out_of_order() -> Entries { for i in 0..408 { let mut entry = EntrySingle::default(); - entry.insert_or_update_peer(&a_started_peer(i)); + entry.upsert_peer(&a_started_peer(i)); entries.insert((InfoHash::from(&i), entry)); } @@ -143,7 +144,7 @@ fn many_hashed_in_order() -> Entries { for i in 0..408 { let mut entry = EntrySingle::default(); - entry.insert_or_update_peer(&a_started_peer(i)); + entry.upsert_peer(&a_started_peer(i)); let hash: &mut DefaultHasher = &mut DefaultHasher::default(); hash.write_i32(i); @@ -390,7 +391,7 @@ async fn it_should_get_metrics( let mut metrics = TorrentsMetrics::default(); for (_, torrent) in entries { - let stats = torrent.get_stats(); + let stats = torrent.get_swarm_metadata(); metrics.torrents += 1; metrics.incomplete += u64::from(stats.incomplete); @@ -537,10 +538,25 @@ async fn it_should_remove_inactive_peers( // Insert the infohash and peer into the repository // and verify there is an extra torrent entry. { - repo.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + repo.upsert_peer(&info_hash, &peer).await; assert_eq!(repo.get_metrics().await.torrents, entries.len() as u64 + 1); } + // Insert the infohash and peer into the repository + // and verify the swarm metadata was updated. + { + repo.upsert_peer(&info_hash, &peer).await; + let stats = repo.get_swarm_metadata(&info_hash).await; + assert_eq!( + stats, + Some(SwarmMetadata { + downloaded: 0, + complete: 1, + incomplete: 0 + }) + ); + } + // Verify that this new peer was inserted into the repository. { let entry = repo.get(&info_hash).await.expect("it_should_get_some"); diff --git a/src/core/mod.rs b/src/core/mod.rs index 6628426c1..83813a863 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -626,10 +626,9 @@ impl Tracker { peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.external_ip)); debug!("After: {peer:?}"); - // we should update the torrent and get the stats before we get the peer list. - let stats = self.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + let stats = self.upsert_peer_and_get_stats(info_hash, peer).await; - let peers = self.get_torrent_peers_for_peer(info_hash, peer); + let peers = self.get_peers_for(info_hash, peer); AnnounceData { peers, @@ -660,7 +659,7 @@ impl Tracker { /// It returns the data for a `scrape` response. fn get_swarm_metadata(&self, info_hash: &InfoHash) -> SwarmMetadata { match self.torrents.get(info_hash) { - Some(torrent_entry) => torrent_entry.get_stats(), + Some(torrent_entry) => torrent_entry.get_swarm_metadata(), None => SwarmMetadata::default(), } } @@ -681,7 +680,7 @@ impl Tracker { Ok(()) } - fn get_torrent_peers_for_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec> { + fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec> { match self.torrents.get(info_hash) { None => vec![], Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(TORRENT_PEERS_LIMIT)), @@ -703,20 +702,36 @@ impl Tracker { /// needed for a `announce` request response. /// /// # Context: Tracker - pub async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> SwarmMetadata { - // code-review: consider splitting the function in two (command and query segregation). - // `update_torrent_with_peer` and `get_stats` + pub async fn upsert_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> SwarmMetadata { + let swarm_metadata_before = match self.torrents.get_swarm_metadata(info_hash) { + Some(swarm_metadata) => swarm_metadata, + None => SwarmMetadata::zeroed(), + }; - let (stats_updated, stats) = self.torrents.update_torrent_with_peer_and_get_stats(info_hash, peer); + self.torrents.upsert_peer(info_hash, peer); - if self.policy.persistent_torrent_completed_stat && stats_updated { - let completed = stats.downloaded; + let swarm_metadata_after = match self.torrents.get_swarm_metadata(info_hash) { + Some(swarm_metadata) => swarm_metadata, + None => SwarmMetadata::zeroed(), + }; + + if swarm_metadata_before != swarm_metadata_after { + self.persist_stats(info_hash, &swarm_metadata_after).await; + } + + swarm_metadata_after + } + + /// It stores the torrents stats into the database (if persistency is enabled). + /// + /// # Context: Tracker + async fn persist_stats(&self, info_hash: &InfoHash, swarm_metadata: &SwarmMetadata) { + if self.policy.persistent_torrent_completed_stat { + let completed = swarm_metadata.downloaded; let info_hash = *info_hash; drop(self.database.save_persistent_torrent(&info_hash, completed).await); } - - stats } /// It calculates and returns the general `Tracker` @@ -1130,7 +1145,7 @@ mod tests { let info_hash = sample_info_hash(); let peer = sample_peer(); - tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; let peers = tracker.get_torrent_peers(&info_hash); @@ -1144,9 +1159,9 @@ mod tests { let info_hash = sample_info_hash(); let peer = sample_peer(); - tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; - let peers = tracker.get_torrent_peers_for_peer(&info_hash, &peer); + let peers = tracker.get_peers_for(&info_hash, &peer); assert_eq!(peers, vec![]); } @@ -1155,9 +1170,7 @@ mod tests { async fn it_should_return_the_torrent_metrics() { let tracker = public_tracker(); - tracker - .update_torrent_with_peer_and_get_stats(&sample_info_hash(), &leecher()) - .await; + tracker.upsert_peer_and_get_stats(&sample_info_hash(), &leecher()).await; let torrent_metrics = tracker.get_torrents_metrics(); @@ -1178,9 +1191,7 @@ mod tests { let start_time = std::time::Instant::now(); for i in 0..1_000_000 { - tracker - .update_torrent_with_peer_and_get_stats(&gen_seeded_infohash(&i), &leecher()) - .await; + tracker.upsert_peer_and_get_stats(&gen_seeded_infohash(&i), &leecher()).await; } let result_a = start_time.elapsed(); @@ -1704,11 +1715,11 @@ mod tests { let mut peer = sample_peer(); peer.event = AnnounceEvent::Started; - let swarm_stats = tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + let swarm_stats = tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; assert_eq!(swarm_stats.downloaded, 0); peer.event = AnnounceEvent::Completed; - let swarm_stats = tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + let swarm_stats = tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; assert_eq!(swarm_stats.downloaded, 1); // Remove the newly updated torrent from memory @@ -1719,7 +1730,7 @@ mod tests { let torrent_entry = tracker.torrents.get(&info_hash).expect("it should be able to get entry"); // It persists the number of completed peers. - assert_eq!(torrent_entry.get_stats().downloaded, 1); + assert_eq!(torrent_entry.get_swarm_metadata().downloaded, 1); // It does not persist the peers assert!(torrent_entry.peers_is_empty()); diff --git a/src/core/services/torrent.rs b/src/core/services/torrent.rs index ce44af3a8..9cba5de25 100644 --- a/src/core/services/torrent.rs +++ b/src/core/services/torrent.rs @@ -50,7 +50,7 @@ pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Op let torrent_entry = torrent_entry_option?; - let stats = torrent_entry.get_stats(); + let stats = torrent_entry.get_swarm_metadata(); let peers = torrent_entry.get_peers(None); @@ -70,7 +70,7 @@ pub async fn get_torrents_page(tracker: Arc, pagination: Option<&Pagina let mut basic_infos: Vec = vec![]; for (info_hash, torrent_entry) in tracker.torrents.get_paginated(pagination) { - let stats = torrent_entry.get_stats(); + let stats = torrent_entry.get_swarm_metadata(); basic_infos.push(BasicInfo { info_hash, @@ -88,7 +88,7 @@ pub async fn get_torrents(tracker: Arc, info_hashes: &[InfoHash]) -> Ve let mut basic_infos: Vec = vec![]; for info_hash in info_hashes { - if let Some(stats) = tracker.torrents.get(info_hash).map(|t| t.get_stats()) { + if let Some(stats) = tracker.torrents.get(info_hash).map(|t| t.get_swarm_metadata()) { basic_infos.push(BasicInfo { info_hash: *info_hash, seeders: u64::from(stats.complete), @@ -156,9 +156,7 @@ mod tests { let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer()).await; let torrent_info = get_torrent_info(tracker.clone(), &info_hash).await.unwrap(); @@ -208,9 +206,7 @@ mod tests { let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer()).await; let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await; @@ -234,12 +230,8 @@ mod tests { let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let offset = 0; let limit = 1; @@ -258,12 +250,8 @@ mod tests { let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let offset = 1; let limit = 4000; @@ -288,15 +276,11 @@ mod tests { let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash1 = InfoHash::from_str(&hash1).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await; diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 2d5038ec3..122e666a8 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -718,9 +718,7 @@ mod tests { .with_peer_address(SocketAddr::new(IpAddr::V6(client_ip_v6), client_port)) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv6) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv6).await; } async fn announce_a_new_peer_using_ipv4(tracker: Arc) -> Response { @@ -944,9 +942,7 @@ mod tests { .with_peer_address(SocketAddr::new(IpAddr::V4(client_ip_v4), client_port)) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv4) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv4).await; } async fn announce_a_new_peer_using_ipv6(tracker: Arc) -> Response { @@ -1119,9 +1115,7 @@ mod tests { .with_number_of_bytes_left(0) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer).await; } fn build_scrape_request(remote_addr: &SocketAddr, info_hash: &InfoHash) -> ScrapeRequest { diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 8d91f3ae8..dec4ccff2 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -23,7 +23,7 @@ pub struct Environment { impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 5638713aa..f00da293e 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -20,7 +20,7 @@ pub struct Environment { impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/servers/udp/environment.rs b/tests/servers/udp/environment.rs index 12f4aeb9e..6ced1dbb7 100644 --- a/tests/servers/udp/environment.rs +++ b/tests/servers/udp/environment.rs @@ -20,7 +20,7 @@ impl Environment { /// Add a torrent to the tracker #[allow(dead_code)] pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } From 5fa01e76a2d17337e450e2c54c41a473ee215353 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 08:50:47 +0100 Subject: [PATCH 020/217] chore(deps): update dependencies ```output Updating crates.io index Updating allocator-api2 v0.2.16 -> v0.2.18 Updating anyhow v1.0.81 -> v1.0.82 Updating async-channel v2.2.0 -> v2.2.1 Updating async-executor v1.10.0 -> v1.11.0 Updating async-trait v0.1.79 -> v0.1.80 Updating axum-client-ip v0.5.1 -> v0.6.0 Updating cc v1.0.91 -> v1.0.94 Updating either v1.10.0 -> v1.11.0 Updating encoding_rs v0.8.33 -> v0.8.34 Updating jobserver v0.1.28 -> v0.1.30 Updating proc-macro2 v1.0.79 -> v1.0.80 Updating quote v1.0.35 -> v1.0.36 Updating rstest v0.18.2 -> v0.19.0 Updating rstest_macros v0.18.2 -> v0.19.0 Updating serde_repr v0.1.18 -> v0.1.19 Updating syn v2.0.58 -> v2.0.59 Updating time v0.3.34 -> v0.3.36 Updating time-macros v0.2.17 -> v0.2.18 Updating windows-targets v0.52.4 -> v0.52.5 Updating windows_aarch64_gnullvm v0.52.4 -> v0.52.5 Updating windows_aarch64_msvc v0.52.4 -> v0.52.5 Updating windows_i686_gnu v0.52.4 -> v0.52.5 Adding windows_i686_gnullvm v0.52.5 Updating windows_i686_msvc v0.52.4 -> v0.52.5 Updating windows_x86_64_gnu v0.52.4 -> v0.52.5 Updating windows_x86_64_gnullvm v0.52.4 -> v0.52.5 Updating windows_x86_64_msvc v0.52.4 -> v0.52.5 Updating winnow v0.6.5 -> v0.6.6 ``` --- Cargo.lock | 200 +++++++++++++++++++++++++++-------------------------- 1 file changed, 103 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b9be523c..f13ed1482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "aquatic_udp_protocol" @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", "event-listener 5.3.0", @@ -219,11 +219,10 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98c37cf288e302c16ef6c8472aad1e034c6c84ce5ea7b8101c98eb4a802fee" +checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ - "async-lock 3.3.0", "async-task", "concurrent-queue", "fastrand 2.0.2", @@ -237,7 +236,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.0", + "async-channel 2.2.1", "async-executor", "async-io 2.3.2", "async-lock 3.3.0", @@ -341,13 +340,13 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -399,9 +398,9 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e7c467bdcd2bd982ce5c8742a1a178aba7b03db399fd18f5d5d438f5aa91cb4" +checksum = "72188bed20deb981f3a4a9fe674e5980fd9e9c2bd880baa94715ad5d60d64c67" dependencies = [ "axum", "forwarded-header-value", @@ -461,7 +460,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -548,7 +547,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -593,7 +592,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel 2.2.0", + "async-channel 2.2.1", "async-lock 3.3.0", "async-task", "fastrand 2.0.2", @@ -623,7 +622,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "syn_derive", ] @@ -702,9 +701,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.91" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd97381a8cc6493395a5afc4c691c1084b3768db713b73aa215217aa245d153" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" dependencies = [ "jobserver", "libc", @@ -741,7 +740,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -813,7 +812,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1086,7 +1085,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1097,7 +1096,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1144,7 +1143,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1174,15 +1173,15 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1379,7 +1378,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1391,7 +1390,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1403,7 +1402,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1496,7 +1495,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1912,9 +1911,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" dependencies = [ "libc", ] @@ -2046,7 +2045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -2197,7 +2196,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -2248,7 +2247,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "termcolor", "thiserror", ] @@ -2448,7 +2447,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -2561,7 +2560,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -2630,7 +2629,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -2804,9 +2803,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" dependencies = [ "unicode-ident", ] @@ -2833,9 +2832,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3090,9 +3089,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" dependencies = [ "futures", "futures-timer", @@ -3102,9 +3101,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" dependencies = [ "cfg-if", "glob", @@ -3113,7 +3112,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.58", + "syn 2.0.59", "unicode-ident", ] @@ -3375,7 +3374,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -3414,13 +3413,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -3471,7 +3470,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -3605,9 +3604,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", @@ -3623,7 +3622,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -3726,14 +3725,14 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3752,9 +3751,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3820,7 +3819,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -3910,7 +3909,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.6", ] [[package]] @@ -4126,7 +4125,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -4297,7 +4296,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "wasm-bindgen-shared", ] @@ -4331,7 +4330,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4389,7 +4388,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4407,7 +4406,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4427,17 +4426,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -4448,9 +4448,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -4460,9 +4460,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -4472,9 +4472,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -4484,9 +4490,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -4496,9 +4502,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -4508,9 +4514,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -4520,9 +4526,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -4535,9 +4541,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] @@ -4587,7 +4593,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] From 4a567cd631cc6bdd220904b1af6fdf524192769b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:39:50 +0100 Subject: [PATCH 021/217] refactor: extract PeerList Extract a type for a collection of peers. The performance adter the exatract is similar: ```output Requests out: 415067.21/second Responses in: 369397.08/second - Connect responses: 183049.81 - Announce responses: 182717.15 - Scrape responses: 3630.12 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 297 - p100: 375 ``` --- packages/torrent-repository/src/entry/mod.rs | 68 ++++++++++++++++++- .../torrent-repository/src/entry/single.rs | 32 ++------- .../src/repository/dash_map_mutex_std.rs | 5 +- .../src/repository/rw_lock_std.rs | 6 +- .../src/repository/rw_lock_std_mutex_std.rs | 5 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 5 +- .../src/repository/rw_lock_tokio.rs | 6 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 5 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 5 +- .../src/repository/skip_map_mutex_std.rs | 5 +- 10 files changed, 88 insertions(+), 54 deletions(-) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index d72ff254b..ee80305ee 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -82,8 +82,72 @@ pub trait EntryAsync { #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Torrent { /// The swarm: a network of peers that are all trying to download the torrent associated to this entry - // #[serde(skip)] - pub(crate) peers: std::collections::BTreeMap>, + pub(crate) peers: PeerList, /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PeerList { + peers: std::collections::BTreeMap>, +} + +impl PeerList { + fn len(&self) -> usize { + self.peers.len() + } + + fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { + self.peers.insert(key, value) + } + + fn remove(&mut self, key: &peer::Id) -> Option> { + self.peers.remove(key) + } + + fn retain(&mut self, f: F) + where + F: FnMut(&peer::Id, &mut Arc) -> bool, + { + self.peers.retain(f); + } + + fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + fn get_peers(&self, limit: Option) -> Vec> { + match limit { + Some(limit) => self.peers.values().take(limit).cloned().collect(), + None => self.peers.values().cloned().collect(), + } + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + match limit { + Some(limit) => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + // Limit the number of peers on the result + .take(limit) + .cloned() + .collect(), + None => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .cloned() + .collect(), + } + } +} diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index a38b54023..36d04c3cf 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -13,13 +13,12 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] fn get_swarm_metadata(&self) -> SwarmMetadata { - let complete: u32 = self.peers.values().filter(|peer| peer.is_seeder()).count() as u32; - let incomplete: u32 = self.peers.len() as u32 - complete; + let (seeders, leechers) = self.peers.seeders_and_leechers(); SwarmMetadata { downloaded: self.downloaded, - complete, - incomplete, + complete: seeders as u32, + incomplete: leechers as u32, } } @@ -42,32 +41,13 @@ impl Entry for EntrySingle { fn get_peers_len(&self) -> usize { self.peers.len() } + fn get_peers(&self, limit: Option) -> Vec> { - match limit { - Some(limit) => self.peers.values().take(limit).cloned().collect(), - None => self.peers.values().cloned().collect(), - } + self.peers.get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - match limit { - Some(limit) => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - // Limit the number of peers on the result - .take(limit) - .cloned() - .collect(), - None => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - .cloned() - .collect(), - } + self.peers.get_peers_for_client(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index b398b09dc..2aba7e54f 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use dashmap::DashMap; @@ -10,7 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] @@ -82,7 +81,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index af48428e4..7d8055fca 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,7 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::Entry; +use crate::entry::{Entry, PeerList}; use crate::{EntrySingle, TorrentsRwLockStd}; #[derive(Default, Debug)] @@ -102,7 +100,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *downloaded, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 74cdc4475..629f3484e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockStdMutexStd}; impl TorrentsRwLockStdMutexStd { @@ -97,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 83ac02c91..3cc0f53a1 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::iter::zip; use std::pin::Pin; use std::sync::Arc; @@ -13,7 +12,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync}; +use crate::entry::{Entry, EntryAsync, PeerList}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockStdMutexTokio}; impl TorrentsRwLockStdMutexTokio { @@ -106,7 +105,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index b95f1e31e..0a481fdde 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,7 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::Entry; +use crate::entry::{Entry, PeerList}; use crate::{EntrySingle, TorrentsRwLockTokio}; #[derive(Default, Debug)] @@ -106,7 +104,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index bde959940..d3b17c2d2 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockTokioMutexStd}; impl TorrentsRwLockTokioMutexStd { @@ -97,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index 1d002e317..875a890ea 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync}; +use crate::entry::{Entry, EntryAsync, PeerList}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockTokioMutexTokio}; impl TorrentsRwLockTokioMutexTokio { @@ -100,7 +99,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index ef3e7e478..b3c71a4de 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use crossbeam_skiplist::SkipMap; @@ -10,7 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] @@ -76,7 +75,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), From 922afda1e689faa62a54cb29ccc6bd7619a4f018 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:48:08 +0100 Subject: [PATCH 022/217] refactor: rename field from peers to swarm --- packages/torrent-repository/src/entry/mod.rs | 4 ++-- .../torrent-repository/src/entry/single.rs | 20 +++++++++---------- .../src/repository/dash_map_mutex_std.rs | 2 +- .../src/repository/rw_lock_std.rs | 2 +- .../src/repository/rw_lock_std_mutex_std.rs | 2 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 2 +- .../src/repository/rw_lock_tokio.rs | 2 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 2 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 2 +- .../src/repository/skip_map_mutex_std.rs | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index ee80305ee..648ded98a 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -81,8 +81,8 @@ pub trait EntryAsync { /// The tracker keeps one entry like this for every torrent. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Torrent { - /// The swarm: a network of peers that are all trying to download the torrent associated to this entry - pub(crate) peers: PeerList, + /// A network of peers that are all trying to download the torrent associated to this entry + pub(crate) swarm: PeerList, /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index 36d04c3cf..169ee2fbb 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -13,7 +13,7 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] fn get_swarm_metadata(&self) -> SwarmMetadata { - let (seeders, leechers) = self.peers.seeders_and_leechers(); + let (seeders, leechers) = self.swarm.seeders_and_leechers(); SwarmMetadata { downloaded: self.downloaded, @@ -27,7 +27,7 @@ impl Entry for EntrySingle { return true; } - if policy.remove_peerless_torrents && self.peers.is_empty() { + if policy.remove_peerless_torrents && self.swarm.is_empty() { return false; } @@ -35,19 +35,19 @@ impl Entry for EntrySingle { } fn peers_is_empty(&self) -> bool { - self.peers.is_empty() + self.swarm.is_empty() } fn get_peers_len(&self) -> usize { - self.peers.len() + self.swarm.len() } fn get_peers(&self, limit: Option) -> Vec> { - self.peers.get_peers(limit) + self.swarm.get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.peers.get_peers_for_client(client, limit) + self.swarm.get_peers_for_client(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { @@ -55,10 +55,10 @@ impl Entry for EntrySingle { match peer::ReadInfo::get_event(peer) { AnnounceEvent::Stopped => { - drop(self.peers.remove(&peer::ReadInfo::get_id(peer))); + drop(self.swarm.remove(&peer::ReadInfo::get_id(peer))); } AnnounceEvent::Completed => { - let previous = self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); + let previous = self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); // Don't count if peer was not previously known and not already completed. if previous.is_some_and(|p| p.event != AnnounceEvent::Completed) { self.downloaded += 1; @@ -66,7 +66,7 @@ impl Entry for EntrySingle { } } _ => { - drop(self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); + drop(self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); } } @@ -74,7 +74,7 @@ impl Entry for EntrySingle { } fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { - self.peers + self.swarm .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); } } diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 2aba7e54f..7bc60dbc6 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -81,7 +81,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 7d8055fca..800b1b31f 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -100,7 +100,7 @@ where } let entry = EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *downloaded, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 629f3484e..9fefc9115 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -96,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 3cc0f53a1..31ccf2a50 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -105,7 +105,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index 0a481fdde..0987b064a 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -104,7 +104,7 @@ where } let entry = EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index d3b17c2d2..77a82e445 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -96,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index 875a890ea..fc7608010 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -99,7 +99,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index b3c71a4de..7f84dae2a 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -75,7 +75,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), From 42f1b309a68d746f221ed377133cb9f2fa5e6208 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:58:20 +0100 Subject: [PATCH 023/217] refactor: extract mod peer_list --- packages/torrent-repository/src/entry/mod.rs | 69 +---------------- .../torrent-repository/src/entry/peer_list.rs | 74 +++++++++++++++++++ .../src/repository/dash_map_mutex_std.rs | 3 +- .../src/repository/rw_lock_std.rs | 3 +- .../src/repository/rw_lock_std_mutex_std.rs | 3 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 3 +- .../src/repository/rw_lock_tokio.rs | 3 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 3 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 3 +- .../src/repository/skip_map_mutex_std.rs | 3 +- 10 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 packages/torrent-repository/src/entry/peer_list.rs diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 648ded98a..40fa4efd5 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -2,13 +2,15 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; -//use serde::{Deserialize, Serialize}; use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; +use self::peer_list::PeerList; + pub mod mutex_std; pub mod mutex_tokio; +pub mod peer_list; pub mod single; pub trait Entry { @@ -86,68 +88,3 @@ pub struct Torrent { /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } - -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PeerList { - peers: std::collections::BTreeMap>, -} - -impl PeerList { - fn len(&self) -> usize { - self.peers.len() - } - - fn is_empty(&self) -> bool { - self.peers.is_empty() - } - - fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { - self.peers.insert(key, value) - } - - fn remove(&mut self, key: &peer::Id) -> Option> { - self.peers.remove(key) - } - - fn retain(&mut self, f: F) - where - F: FnMut(&peer::Id, &mut Arc) -> bool, - { - self.peers.retain(f); - } - - fn seeders_and_leechers(&self) -> (usize, usize) { - let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); - let leechers = self.len() - seeders; - - (seeders, leechers) - } - - fn get_peers(&self, limit: Option) -> Vec> { - match limit { - Some(limit) => self.peers.values().take(limit).cloned().collect(), - None => self.peers.values().cloned().collect(), - } - } - - fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - match limit { - Some(limit) => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - // Limit the number of peers on the result - .take(limit) - .cloned() - .collect(), - None => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - .cloned() - .collect(), - } - } -} diff --git a/packages/torrent-repository/src/entry/peer_list.rs b/packages/torrent-repository/src/entry/peer_list.rs new file mode 100644 index 000000000..4af9b1d77 --- /dev/null +++ b/packages/torrent-repository/src/entry/peer_list.rs @@ -0,0 +1,74 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_primitives::peer; + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PeerList { + peers: std::collections::BTreeMap>, +} + +impl PeerList { + #[must_use] + pub fn len(&self) -> usize { + self.peers.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + pub fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { + self.peers.insert(key, value) + } + + pub fn remove(&mut self, key: &peer::Id) -> Option> { + self.peers.remove(key) + } + + pub fn retain(&mut self, f: F) + where + F: FnMut(&peer::Id, &mut Arc) -> bool, + { + self.peers.retain(f); + } + + #[must_use] + pub fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + #[must_use] + pub fn get_peers(&self, limit: Option) -> Vec> { + match limit { + Some(limit) => self.peers.values().take(limit).cloned().collect(), + None => self.peers.values().cloned().collect(), + } + } + + #[must_use] + pub fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + match limit { + Some(limit) => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + // Limit the number of peers on the result + .take(limit) + .cloned() + .collect(), + None => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .cloned() + .collect(), + } + } +} diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 7bc60dbc6..a38205205 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -9,7 +9,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 800b1b31f..0d96a2375 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -6,7 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockStd}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 9fefc9115..76d5e8f1e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockStdMutexStd}; impl TorrentsRwLockStdMutexStd { diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 31ccf2a50..e527d6b59 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -12,7 +12,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockStdMutexTokio}; impl TorrentsRwLockStdMutexTokio { diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index 0987b064a..c360106b8 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -6,7 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockTokio}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index 77a82e445..9fce79b44 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockTokioMutexStd}; impl TorrentsRwLockTokioMutexStd { diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index fc7608010..c7e0d4054 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockTokioMutexTokio}; impl TorrentsRwLockTokioMutexTokio { diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 7f84dae2a..bc9ecd066 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -9,7 +9,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] From 40182b49e0c221ed85fb78f4243cc180cc96ed1e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 12:00:28 +0100 Subject: [PATCH 024/217] test: add tests for PeerList type --- packages/primitives/src/peer.rs | 39 +++ .../torrent-repository/src/entry/peer_list.rs | 249 ++++++++++++++++-- .../torrent-repository/src/entry/single.rs | 11 +- 3 files changed, 276 insertions(+), 23 deletions(-) diff --git a/packages/primitives/src/peer.rs b/packages/primitives/src/peer.rs index f5b009f2a..ab7559508 100644 --- a/packages/primitives/src/peer.rs +++ b/packages/primitives/src/peer.rs @@ -362,6 +362,38 @@ pub mod fixture { } impl PeerBuilder { + #[allow(dead_code)] + #[must_use] + pub fn seeder() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000001"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Completed, + }; + + Self { peer } + } + + #[allow(dead_code)] + #[must_use] + pub fn leecher() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000002"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(10), + event: AnnounceEvent::Started, + }; + + Self { peer } + } + #[allow(dead_code)] #[must_use] pub fn with_peer_id(mut self, peer_id: &Id) -> Self { @@ -390,6 +422,13 @@ pub mod fixture { self } + #[allow(dead_code)] + #[must_use] + pub fn last_updated_on(mut self, updated: DurationSinceUnixEpoch) -> Self { + self.peer.updated = updated; + self + } + #[allow(dead_code)] #[must_use] pub fn build(self) -> Peer { diff --git a/packages/torrent-repository/src/entry/peer_list.rs b/packages/torrent-repository/src/entry/peer_list.rs index 4af9b1d77..3f69edbb5 100644 --- a/packages/torrent-repository/src/entry/peer_list.rs +++ b/packages/torrent-repository/src/entry/peer_list.rs @@ -1,7 +1,13 @@ +//! A peer list. use std::net::SocketAddr; use std::sync::Arc; -use torrust_tracker_primitives::peer; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +// code-review: the current implementation uses the peer Id as the ``BTreeMap`` +// key. That would allow adding two identical peers except for the Id. +// For example, two peers with the same socket address but a different peer Id +// would be allowed. That would lead to duplicated peers in the tracker responses. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PeerList { @@ -19,31 +25,26 @@ impl PeerList { self.peers.is_empty() } - pub fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { - self.peers.insert(key, value) + pub fn upsert(&mut self, value: Arc) -> Option> { + self.peers.insert(value.peer_id, value) } pub fn remove(&mut self, key: &peer::Id) -> Option> { self.peers.remove(key) } - pub fn retain(&mut self, f: F) - where - F: FnMut(&peer::Id, &mut Arc) -> bool, - { - self.peers.retain(f); + pub fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { + self.peers + .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); } #[must_use] - pub fn seeders_and_leechers(&self) -> (usize, usize) { - let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); - let leechers = self.len() - seeders; - - (seeders, leechers) + pub fn get(&self, peer_id: &peer::Id) -> Option<&Arc> { + self.peers.get(peer_id) } #[must_use] - pub fn get_peers(&self, limit: Option) -> Vec> { + pub fn get_all(&self, limit: Option) -> Vec> { match limit { Some(limit) => self.peers.values().take(limit).cloned().collect(), None => self.peers.values().cloned().collect(), @@ -51,13 +52,21 @@ impl PeerList { } #[must_use] - pub fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + pub fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + #[must_use] + pub fn get_peers_excluding_addr(&self, peer_addr: &SocketAddr, limit: Option) -> Vec> { match limit { Some(limit) => self .peers .values() // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) // Limit the number of peers on the result .take(limit) .cloned() @@ -66,9 +75,215 @@ impl PeerList { .peers .values() // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) .cloned() .collect(), } } } + +#[cfg(test)] +mod tests { + + mod it_should { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::Arc; + + use torrust_tracker_primitives::peer::fixture::PeerBuilder; + use torrust_tracker_primitives::peer::{self}; + use torrust_tracker_primitives::DurationSinceUnixEpoch; + + use crate::entry::peer_list::PeerList; + + #[test] + fn be_empty_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert!(peer_list.is_empty()); + } + + #[test] + fn have_zero_length_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn allow_inserting_a_new_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + assert_eq!(peer_list.upsert(peer.into()), None); + } + + #[test] + fn allow_updating_a_preexisting_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.upsert(peer.into()), Some(Arc::new(peer))); + } + + #[test] + fn allow_getting_all_peers() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get_all(None), [Arc::new(peer)]); + } + + #[test] + fn allow_getting_one_peer_by_id() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get(&peer.peer_id), Some(Arc::new(peer)).as_ref()); + } + + #[test] + fn increase_the_number_of_peers_after_inserting_a_new_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn decrease_the_number_of_peers_after_removing_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert!(peer_list.is_empty()); + } + + #[test] + fn allow_removing_an_existing_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert_eq!(peer_list.get(&peer.peer_id), None); + } + + #[test] + fn allow_getting_all_peers_excluding_peers_with_a_given_address() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969)) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 6969)) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.get_peers_excluding_addr(&peer2.peer_addr, None), [Arc::new(peer1)]); + } + + #[test] + fn return_the_number_of_seeders_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (seeders, _leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(seeders, 1); + } + + #[test] + fn return_the_number_of_leechers_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (_seeders, leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(leechers, 1); + } + + #[test] + fn remove_inactive_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second after inserting the peer + peer_list.remove_inactive_peers(last_update_time + one_second); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn not_remove_active_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second before inserting the peer. + peer_list.remove_inactive_peers(last_update_time - one_second); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn allow_inserting_two_identical_peers_except_for_the_id() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.len(), 2); + } + } +} diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index 169ee2fbb..a01124454 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -43,11 +43,11 @@ impl Entry for EntrySingle { } fn get_peers(&self, limit: Option) -> Vec> { - self.swarm.get_peers(limit) + self.swarm.get_all(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.swarm.get_peers_for_client(client, limit) + self.swarm.get_peers_excluding_addr(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { @@ -58,7 +58,7 @@ impl Entry for EntrySingle { drop(self.swarm.remove(&peer::ReadInfo::get_id(peer))); } AnnounceEvent::Completed => { - let previous = self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); + let previous = self.swarm.upsert(Arc::new(*peer)); // Don't count if peer was not previously known and not already completed. if previous.is_some_and(|p| p.event != AnnounceEvent::Completed) { self.downloaded += 1; @@ -66,7 +66,7 @@ impl Entry for EntrySingle { } } _ => { - drop(self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); + drop(self.swarm.upsert(Arc::new(*peer))); } } @@ -74,7 +74,6 @@ impl Entry for EntrySingle { } fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { - self.swarm - .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); + self.swarm.remove_inactive_peers(current_cutoff); } } From 5750e2c22d52dfa976150e363d3f400b93efd31e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 14:16:13 +0100 Subject: [PATCH 025/217] chore(deps): add dependency parking_lot It provides implementations of Mutex and RwLock that are smaller, faster and more flexible than those in the Rust standard library. It will be used to check if a new torrent repo implementation using these lock is faster. --- Cargo.lock | 2 ++ Cargo.toml | 1 + packages/torrent-repository/Cargo.toml | 1 + 3 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index f13ed1482..dc6db21c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3939,6 +3939,7 @@ dependencies = [ "log", "mockall", "multimap", + "parking_lot", "percent-encoding", "r2d2", "r2d2_mysql", @@ -4037,6 +4038,7 @@ dependencies = [ "crossbeam-skiplist", "dashmap", "futures", + "parking_lot", "rstest", "tokio", "torrust-tracker-clock", diff --git a/Cargo.toml b/Cargo.toml index ef0c39d4b..57c18453b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ hyper = "1" lazy_static = "1" log = { version = "0", features = ["release_max_level_info"] } multimap = "0" +parking_lot = "0.12.1" percent-encoding = "2" r2d2 = "0" r2d2_mysql = "24" diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 6bc8bfcdd..937ec11e2 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -19,6 +19,7 @@ version.workspace = true crossbeam-skiplist = "0.1" dashmap = "5.5.3" futures = "0.3.29" +parking_lot = "0.12.1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } From 9258ac0cf2b42530950e7cd0cd40792b45c8f7b9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 14:21:10 +0100 Subject: [PATCH 026/217] feat: new torrent repo implementation using parking_lot RwLock --- .../benches/repository_benchmark.rs | 22 ++++- packages/torrent-repository/src/entry/mod.rs | 1 + .../src/entry/mutex_parking_lot.rs | 49 ++++++++++ packages/torrent-repository/src/lib.rs | 7 ++ .../src/repository/skip_map_mutex_std.rs | 93 ++++++++++++++++++- .../torrent-repository/tests/common/repo.rs | 18 ++++ .../tests/common/torrent.rs | 11 ++- .../torrent-repository/tests/entry/mod.rs | 34 +++---- .../tests/repository/mod.rs | 31 +++++-- 9 files changed, 238 insertions(+), 28 deletions(-) create mode 100644 packages/torrent-repository/src/entry/mutex_parking_lot.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 58cd70d9a..75e7fd5b8 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,7 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, }; use crate::helpers::{asyn, sync}; @@ -49,6 +49,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.bench_function("DashMapMutexStd", |b| { b.iter_custom(sync::add_one_torrent::); }); @@ -102,6 +106,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); @@ -156,6 +165,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); @@ -211,6 +225,12 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt).iter_custom(|iters| { + sync::update_multiple_torrents_in_parallel::(&rt, iters, None) + }); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 40fa4efd5..dbe1416be 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -8,6 +8,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use self::peer_list::PeerList; +pub mod mutex_parking_lot; pub mod mutex_std; pub mod mutex_tokio; pub mod peer_list; diff --git a/packages/torrent-repository/src/entry/mutex_parking_lot.rs b/packages/torrent-repository/src/entry/mutex_parking_lot.rs new file mode 100644 index 000000000..ef0e958d5 --- /dev/null +++ b/packages/torrent-repository/src/entry/mutex_parking_lot.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +use super::{Entry, EntrySync}; +use crate::{EntryRwLockParkingLot, EntrySingle}; + +impl EntrySync for EntryRwLockParkingLot { + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.read().get_swarm_metadata() + } + + fn is_good(&self, policy: &TrackerPolicy) -> bool { + self.read().is_good(policy) + } + + fn peers_is_empty(&self) -> bool { + self.read().peers_is_empty() + } + + fn get_peers_len(&self) -> usize { + self.read().get_peers_len() + } + + fn get_peers(&self, limit: Option) -> Vec> { + self.read().get_peers(limit) + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + self.read().get_peers_for_client(client, limit) + } + + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.write().upsert_peer(peer) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + self.write().remove_inactive_peers(current_cutoff); + } +} + +impl From for EntryRwLockParkingLot { + fn from(entry: EntrySingle) -> Self { + Arc::new(parking_lot::RwLock::new(entry)) + } +} diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 7a6d209b9..5d3a7ed45 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -9,9 +9,14 @@ use torrust_tracker_clock::clock; pub mod entry; pub mod repository; +// Repo Entries + pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; +pub type EntryRwLockParkingLot = Arc>; + +// Repos pub type TorrentsRwLockStd = RwLockStd; pub type TorrentsRwLockStdMutexStd = RwLockStd; @@ -21,6 +26,8 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsSkipMapRwLockParkingLot = CrossbeamSkipList; + pub type TorrentsDashMapMutexStd = XacrimonDashMap; /// This code needs to be copied into each crate. diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index bc9ecd066..0a2a566e7 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -11,7 +11,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use super::Repository; use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; -use crate::{EntryMutexStd, EntrySingle}; +use crate::{EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { @@ -108,3 +108,94 @@ where } } } + +impl Repository for CrossbeamSkipList +where + EntryRwLockParkingLot: EntrySync, + EntrySingle: Entry, +{ + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().read().get_swarm_metadata(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryRwLockParkingLot)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryRwLockParkingLot::new( + EntrySingle { + swarm: PeerList::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 7c245fe04..c5da6258d 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -8,6 +8,7 @@ use torrust_tracker_torrent_repository::repository::{Repository as _, Repository use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsSkipMapRwLockParkingLot, }; #[derive(Debug)] @@ -19,6 +20,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + SkipMapRwLockParkingLot(TorrentsSkipMapRwLockParkingLot), DashMapMutexStd(TorrentsDashMapMutexStd), } @@ -32,6 +34,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::SkipMapRwLockParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), } } @@ -45,6 +48,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::SkipMapRwLockParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), } } @@ -58,6 +62,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::SkipMapRwLockParkingLot(repo) => Some(repo.get(key)?.read().clone()), Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } @@ -71,6 +76,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::SkipMapRwLockParkingLot(repo) => repo.get_metrics(), Repo::DashMapMutexStd(repo) => repo.get_metrics(), } } @@ -111,6 +117,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::SkipMapRwLockParkingLot(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.read().clone())) + .collect(), Repo::DashMapMutexStd(repo) => repo .get_paginated(pagination) .iter() @@ -128,6 +139,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::SkipMapRwLockParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } @@ -141,6 +153,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::SkipMapRwLockParkingLot(repo) => Some(repo.remove(key)?.write().clone()), Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } @@ -154,6 +167,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::SkipMapRwLockParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } @@ -167,6 +181,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::SkipMapRwLockParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } @@ -194,6 +209,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::SkipMapRwLockParkingLot(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } Repo::DashMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index c0699479e..f672d14ef 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -5,13 +5,14 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_torrent_repository::entry::{Entry as _, EntryAsync as _, EntrySync as _}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntrySingle}; +use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; #[derive(Debug, Clone)] pub(crate) enum Torrent { Single(EntrySingle), MutexStd(EntryMutexStd), MutexTokio(EntryMutexTokio), + RwLockParkingLot(EntryRwLockParkingLot), } impl Torrent { @@ -20,6 +21,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_swarm_metadata(), Torrent::MutexStd(entry) => entry.get_swarm_metadata(), Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, + Torrent::RwLockParkingLot(entry) => entry.clone().get_swarm_metadata(), } } @@ -28,6 +30,7 @@ impl Torrent { Torrent::Single(entry) => entry.is_good(policy), Torrent::MutexStd(entry) => entry.is_good(policy), Torrent::MutexTokio(entry) => entry.clone().check_good(policy).await, + Torrent::RwLockParkingLot(entry) => entry.is_good(policy), } } @@ -36,6 +39,7 @@ impl Torrent { Torrent::Single(entry) => entry.peers_is_empty(), Torrent::MutexStd(entry) => entry.peers_is_empty(), Torrent::MutexTokio(entry) => entry.clone().peers_is_empty().await, + Torrent::RwLockParkingLot(entry) => entry.peers_is_empty(), } } @@ -44,6 +48,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_len(), Torrent::MutexStd(entry) => entry.get_peers_len(), Torrent::MutexTokio(entry) => entry.clone().get_peers_len().await, + Torrent::RwLockParkingLot(entry) => entry.get_peers_len(), } } @@ -52,6 +57,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers(limit), Torrent::MutexStd(entry) => entry.get_peers(limit), Torrent::MutexTokio(entry) => entry.clone().get_peers(limit).await, + Torrent::RwLockParkingLot(entry) => entry.get_peers(limit), } } @@ -60,6 +66,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexStd(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexTokio(entry) => entry.clone().get_peers_for_client(client, limit).await, + Torrent::RwLockParkingLot(entry) => entry.get_peers_for_client(client, limit), } } @@ -68,6 +75,7 @@ impl Torrent { Torrent::Single(entry) => entry.upsert_peer(peer), Torrent::MutexStd(entry) => entry.upsert_peer(peer), Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, + Torrent::RwLockParkingLot(entry) => entry.upsert_peer(peer), } } @@ -76,6 +84,7 @@ impl Torrent { Torrent::Single(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexStd(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexTokio(entry) => entry.clone().remove_inactive_peers(current_cutoff).await, + Torrent::RwLockParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), } } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index 3c564c6f8..aa3126000 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -9,7 +9,7 @@ use torrust_tracker_configuration::{TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::peer::Peer; use torrust_tracker_primitives::{peer, NumberOfBytes}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntrySingle}; +use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; use crate::common::torrent::Torrent; use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; @@ -20,7 +20,7 @@ fn single() -> Torrent { Torrent::Single(EntrySingle::default()) } #[fixture] -fn standard_mutex() -> Torrent { +fn mutex_std() -> Torrent { Torrent::MutexStd(EntryMutexStd::default()) } @@ -29,6 +29,11 @@ fn mutex_tokio() -> Torrent { Torrent::MutexTokio(EntryMutexTokio::default()) } +#[fixture] +fn rw_lock_parking_lot() -> Torrent { + Torrent::RwLockParkingLot(EntryRwLockParkingLot::default()) +} + #[fixture] fn policy_none() -> TrackerPolicy { TrackerPolicy::new(false, 0, false) @@ -99,7 +104,7 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { #[case::empty(&Makes::Empty)] #[tokio::test] async fn it_should_be_empty_by_default( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -115,7 +120,7 @@ async fn it_should_be_empty_by_default( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_check_if_entry_is_good( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { @@ -153,7 +158,7 @@ async fn it_should_check_if_entry_is_good( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_for_torrent_entry( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -174,10 +179,7 @@ async fn it_should_get_peers_for_torrent_entry( #[case::downloaded(&Makes::Downloaded)] #[case::three(&Makes::Three)] #[tokio::test] -async fn it_should_update_a_peer( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, - #[case] makes: &Makes, -) { +async fn it_should_update_a_peer(#[values(single(), mutex_std(), mutex_tokio())] mut torrent: Torrent, #[case] makes: &Makes) { make(&mut torrent, makes).await; // Make and insert a new peer. @@ -215,7 +217,7 @@ async fn it_should_update_a_peer( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_a_peer_upon_stopped_announcement( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { use torrust_tracker_primitives::peer::ReadInfo as _; @@ -256,7 +258,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloaded_statistic( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -287,7 +289,7 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_a_seeder( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -319,7 +321,7 @@ async fn it_should_update_a_peer_as_a_seeder( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_incomplete( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -351,7 +353,7 @@ async fn it_should_update_a_peer_as_incomplete( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_excluding_the_client_socket( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -383,7 +385,7 @@ async fn it_should_get_peers_excluding_the_client_socket( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_limit_the_number_of_peers_returned( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -408,7 +410,7 @@ async fn it_should_limit_the_number_of_peers_returned( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_inactive_peers_beyond_cutoff( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { const TIMEOUT: Duration = Duration::from_secs(120); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index fde34467e..ac53e6510 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -49,10 +49,15 @@ fn tokio_tokio() -> Repo { } #[fixture] -fn skip_list_std() -> Repo { +fn skip_list_mutex_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn skip_list_rw_lock_parking_lot() -> Repo { + Repo::SkipMapRwLockParkingLot(CrossbeamSkipList::default()) +} + #[fixture] fn dash_map_std() -> Repo { Repo::DashMapMutexStd(XacrimonDashMap::default()) @@ -246,7 +251,8 @@ async fn it_should_get_a_torrent_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -279,7 +285,8 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot() )] repo: Repo, #[case] entries: Entries, @@ -321,7 +328,8 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot() )] repo: Repo, #[case] entries: Entries, @@ -378,7 +386,8 @@ async fn it_should_get_metrics( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -420,7 +429,8 @@ async fn it_should_import_persistent_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -459,7 +469,8 @@ async fn it_should_remove_an_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -496,7 +507,8 @@ async fn it_should_remove_inactive_peers( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -594,7 +606,8 @@ async fn it_should_remove_peerless_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, From 0fa396cc34ae457ff2855bd339317b3b1dc15672 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 16:18:44 +0100 Subject: [PATCH 027/217] chore(deps): add parking_lot to cargo machete It's used for benchmarking in the torrent-repository workspace package. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 57c18453b..94889cbf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ url = "2" uuid = { version = "1", features = ["v4"] } [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap"] +ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] [dev-dependencies] local-ip-address = "0" From 0058e72550d827da08071b63961ce78596d758e1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 16:28:38 +0100 Subject: [PATCH 028/217] feat: new torrent repo implementation using parking_lot Mutex --- .../benches/repository_benchmark.rs | 23 ++++- packages/torrent-repository/src/entry/mod.rs | 1 + .../src/entry/mutex_parking_lot.rs | 24 ++--- .../src/entry/rw_lock_parking_lot.rs | 49 ++++++++++ packages/torrent-repository/src/lib.rs | 2 + .../src/repository/skip_map_mutex_std.rs | 93 ++++++++++++++++++- .../torrent-repository/tests/common/repo.rs | 21 ++++- .../tests/common/torrent.rs | 13 ++- .../torrent-repository/tests/entry/mod.rs | 29 +++--- .../tests/repository/mod.rs | 13 +++ 10 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 packages/torrent-repository/src/entry/rw_lock_parking_lot.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 75e7fd5b8..4e50f1454 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,8 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexParkingLot, TorrentsSkipMapMutexStd, + TorrentsSkipMapRwLockParkingLot, }; use crate::helpers::{asyn, sync}; @@ -49,6 +50,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.iter_custom(sync::add_one_torrent::); }); @@ -106,6 +111,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt) .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); @@ -165,6 +175,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); @@ -225,6 +240,12 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt).iter_custom(|iters| { + sync::update_multiple_torrents_in_parallel::(&rt, iters, None) + }); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt).iter_custom(|iters| { sync::update_multiple_torrents_in_parallel::(&rt, iters, None) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index dbe1416be..b811d3262 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -12,6 +12,7 @@ pub mod mutex_parking_lot; pub mod mutex_std; pub mod mutex_tokio; pub mod peer_list; +pub mod rw_lock_parking_lot; pub mod single; pub trait Entry { diff --git a/packages/torrent-repository/src/entry/mutex_parking_lot.rs b/packages/torrent-repository/src/entry/mutex_parking_lot.rs index ef0e958d5..4f3921ea7 100644 --- a/packages/torrent-repository/src/entry/mutex_parking_lot.rs +++ b/packages/torrent-repository/src/entry/mutex_parking_lot.rs @@ -6,44 +6,44 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use super::{Entry, EntrySync}; -use crate::{EntryRwLockParkingLot, EntrySingle}; +use crate::{EntryMutexParkingLot, EntrySingle}; -impl EntrySync for EntryRwLockParkingLot { +impl EntrySync for EntryMutexParkingLot { fn get_swarm_metadata(&self) -> SwarmMetadata { - self.read().get_swarm_metadata() + self.lock().get_swarm_metadata() } fn is_good(&self, policy: &TrackerPolicy) -> bool { - self.read().is_good(policy) + self.lock().is_good(policy) } fn peers_is_empty(&self) -> bool { - self.read().peers_is_empty() + self.lock().peers_is_empty() } fn get_peers_len(&self) -> usize { - self.read().get_peers_len() + self.lock().get_peers_len() } fn get_peers(&self, limit: Option) -> Vec> { - self.read().get_peers(limit) + self.lock().get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.read().get_peers_for_client(client, limit) + self.lock().get_peers_for_client(client, limit) } fn upsert_peer(&self, peer: &peer::Peer) -> bool { - self.write().upsert_peer(peer) + self.lock().upsert_peer(peer) } fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { - self.write().remove_inactive_peers(current_cutoff); + self.lock().remove_inactive_peers(current_cutoff); } } -impl From for EntryRwLockParkingLot { +impl From for EntryMutexParkingLot { fn from(entry: EntrySingle) -> Self { - Arc::new(parking_lot::RwLock::new(entry)) + Arc::new(parking_lot::Mutex::new(entry)) } } diff --git a/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs b/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs new file mode 100644 index 000000000..ef0e958d5 --- /dev/null +++ b/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +use super::{Entry, EntrySync}; +use crate::{EntryRwLockParkingLot, EntrySingle}; + +impl EntrySync for EntryRwLockParkingLot { + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.read().get_swarm_metadata() + } + + fn is_good(&self, policy: &TrackerPolicy) -> bool { + self.read().is_good(policy) + } + + fn peers_is_empty(&self) -> bool { + self.read().peers_is_empty() + } + + fn get_peers_len(&self) -> usize { + self.read().get_peers_len() + } + + fn get_peers(&self, limit: Option) -> Vec> { + self.read().get_peers(limit) + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + self.read().get_peers_for_client(client, limit) + } + + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.write().upsert_peer(peer) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + self.write().remove_inactive_peers(current_cutoff); + } +} + +impl From for EntryRwLockParkingLot { + fn from(entry: EntrySingle) -> Self { + Arc::new(parking_lot::RwLock::new(entry)) + } +} diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 5d3a7ed45..a8955808e 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -14,6 +14,7 @@ pub mod repository; pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; +pub type EntryMutexParkingLot = Arc>; pub type EntryRwLockParkingLot = Arc>; // Repos @@ -26,6 +27,7 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsSkipMapMutexParkingLot = CrossbeamSkipList; pub type TorrentsSkipMapRwLockParkingLot = CrossbeamSkipList; pub type TorrentsDashMapMutexStd = XacrimonDashMap; diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 0a2a566e7..9960b0c30 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -11,7 +11,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use super::Repository; use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; -use crate::{EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; +use crate::{EntryMutexParkingLot, EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { @@ -199,3 +199,94 @@ where } } } + +impl Repository for CrossbeamSkipList +where + EntryMutexParkingLot: EntrySync, + EntrySingle: Entry, +{ + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().get_swarm_metadata(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexParkingLot)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexParkingLot::new( + EntrySingle { + swarm: PeerList::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index c5da6258d..f317d0d17 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -7,8 +7,8 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, - TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, - TorrentsSkipMapRwLockParkingLot, + TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexParkingLot, + TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, }; #[derive(Debug)] @@ -20,6 +20,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + SkipMapMutexParkingLot(TorrentsSkipMapMutexParkingLot), SkipMapRwLockParkingLot(TorrentsSkipMapRwLockParkingLot), DashMapMutexStd(TorrentsDashMapMutexStd), } @@ -34,6 +35,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::SkipMapMutexParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::SkipMapRwLockParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), } @@ -48,6 +50,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::SkipMapMutexParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::SkipMapRwLockParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), } @@ -62,6 +65,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::SkipMapMutexParkingLot(repo) => Some(repo.get(key)?.lock().clone()), Repo::SkipMapRwLockParkingLot(repo) => Some(repo.get(key)?.read().clone()), Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } @@ -76,6 +80,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::SkipMapMutexParkingLot(repo) => repo.get_metrics(), Repo::SkipMapRwLockParkingLot(repo) => repo.get_metrics(), Repo::DashMapMutexStd(repo) => repo.get_metrics(), } @@ -117,6 +122,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::SkipMapMutexParkingLot(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().clone())) + .collect(), Repo::SkipMapRwLockParkingLot(repo) => repo .get_paginated(pagination) .iter() @@ -139,6 +149,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::SkipMapMutexParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::SkipMapRwLockParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } @@ -153,6 +164,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::SkipMapMutexParkingLot(repo) => Some(repo.remove(key)?.lock().clone()), Repo::SkipMapRwLockParkingLot(repo) => Some(repo.remove(key)?.write().clone()), Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } @@ -167,6 +179,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::SkipMapMutexParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::SkipMapRwLockParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } @@ -181,6 +194,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::SkipMapMutexParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::SkipMapRwLockParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } @@ -209,6 +223,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::SkipMapMutexParkingLot(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } Repo::SkipMapRwLockParkingLot(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index f672d14ef..abcf5525e 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -5,13 +5,16 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_torrent_repository::entry::{Entry as _, EntryAsync as _, EntrySync as _}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; +use torrust_tracker_torrent_repository::{ + EntryMutexParkingLot, EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle, +}; #[derive(Debug, Clone)] pub(crate) enum Torrent { Single(EntrySingle), MutexStd(EntryMutexStd), MutexTokio(EntryMutexTokio), + MutexParkingLot(EntryMutexParkingLot), RwLockParkingLot(EntryRwLockParkingLot), } @@ -21,6 +24,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_swarm_metadata(), Torrent::MutexStd(entry) => entry.get_swarm_metadata(), Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, + Torrent::MutexParkingLot(entry) => entry.clone().get_swarm_metadata(), Torrent::RwLockParkingLot(entry) => entry.clone().get_swarm_metadata(), } } @@ -30,6 +34,7 @@ impl Torrent { Torrent::Single(entry) => entry.is_good(policy), Torrent::MutexStd(entry) => entry.is_good(policy), Torrent::MutexTokio(entry) => entry.clone().check_good(policy).await, + Torrent::MutexParkingLot(entry) => entry.is_good(policy), Torrent::RwLockParkingLot(entry) => entry.is_good(policy), } } @@ -39,6 +44,7 @@ impl Torrent { Torrent::Single(entry) => entry.peers_is_empty(), Torrent::MutexStd(entry) => entry.peers_is_empty(), Torrent::MutexTokio(entry) => entry.clone().peers_is_empty().await, + Torrent::MutexParkingLot(entry) => entry.peers_is_empty(), Torrent::RwLockParkingLot(entry) => entry.peers_is_empty(), } } @@ -48,6 +54,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_len(), Torrent::MutexStd(entry) => entry.get_peers_len(), Torrent::MutexTokio(entry) => entry.clone().get_peers_len().await, + Torrent::MutexParkingLot(entry) => entry.get_peers_len(), Torrent::RwLockParkingLot(entry) => entry.get_peers_len(), } } @@ -57,6 +64,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers(limit), Torrent::MutexStd(entry) => entry.get_peers(limit), Torrent::MutexTokio(entry) => entry.clone().get_peers(limit).await, + Torrent::MutexParkingLot(entry) => entry.get_peers(limit), Torrent::RwLockParkingLot(entry) => entry.get_peers(limit), } } @@ -66,6 +74,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexStd(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexTokio(entry) => entry.clone().get_peers_for_client(client, limit).await, + Torrent::MutexParkingLot(entry) => entry.get_peers_for_client(client, limit), Torrent::RwLockParkingLot(entry) => entry.get_peers_for_client(client, limit), } } @@ -75,6 +84,7 @@ impl Torrent { Torrent::Single(entry) => entry.upsert_peer(peer), Torrent::MutexStd(entry) => entry.upsert_peer(peer), Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, + Torrent::MutexParkingLot(entry) => entry.upsert_peer(peer), Torrent::RwLockParkingLot(entry) => entry.upsert_peer(peer), } } @@ -84,6 +94,7 @@ impl Torrent { Torrent::Single(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexStd(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexTokio(entry) => entry.clone().remove_inactive_peers(current_cutoff).await, + Torrent::MutexParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::RwLockParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index aa3126000..3b9f3e3ad 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -9,7 +9,9 @@ use torrust_tracker_configuration::{TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::peer::Peer; use torrust_tracker_primitives::{peer, NumberOfBytes}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; +use torrust_tracker_torrent_repository::{ + EntryMutexParkingLot, EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle, +}; use crate::common::torrent::Torrent; use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; @@ -29,6 +31,11 @@ fn mutex_tokio() -> Torrent { Torrent::MutexTokio(EntryMutexTokio::default()) } +#[fixture] +fn mutex_parking_lot() -> Torrent { + Torrent::MutexParkingLot(EntryMutexParkingLot::default()) +} + #[fixture] fn rw_lock_parking_lot() -> Torrent { Torrent::RwLockParkingLot(EntryRwLockParkingLot::default()) @@ -104,7 +111,7 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { #[case::empty(&Makes::Empty)] #[tokio::test] async fn it_should_be_empty_by_default( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -120,7 +127,7 @@ async fn it_should_be_empty_by_default( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_check_if_entry_is_good( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { @@ -158,7 +165,7 @@ async fn it_should_check_if_entry_is_good( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_for_torrent_entry( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -217,7 +224,7 @@ async fn it_should_update_a_peer(#[values(single(), mutex_std(), mutex_tokio())] #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_a_peer_upon_stopped_announcement( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { use torrust_tracker_primitives::peer::ReadInfo as _; @@ -258,7 +265,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloaded_statistic( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -289,7 +296,7 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_a_seeder( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -321,7 +328,7 @@ async fn it_should_update_a_peer_as_a_seeder( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_incomplete( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -353,7 +360,7 @@ async fn it_should_update_a_peer_as_incomplete( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_excluding_the_client_socket( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -385,7 +392,7 @@ async fn it_should_get_peers_excluding_the_client_socket( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_limit_the_number_of_peers_returned( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -410,7 +417,7 @@ async fn it_should_limit_the_number_of_peers_returned( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_inactive_peers_beyond_cutoff( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { const TIMEOUT: Duration = Duration::from_secs(120); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index ac53e6510..dd9893cc9 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -53,6 +53,11 @@ fn skip_list_mutex_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn skip_list_mutex_parking_lot() -> Repo { + Repo::SkipMapMutexParkingLot(CrossbeamSkipList::default()) +} + #[fixture] fn skip_list_rw_lock_parking_lot() -> Repo { Repo::SkipMapRwLockParkingLot(CrossbeamSkipList::default()) @@ -252,6 +257,7 @@ async fn it_should_get_a_torrent_entry( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -286,6 +292,7 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot() )] repo: Repo, @@ -329,6 +336,7 @@ async fn it_should_get_paginated( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot() )] repo: Repo, @@ -387,6 +395,7 @@ async fn it_should_get_metrics( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -430,6 +439,7 @@ async fn it_should_import_persistent_torrents( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -470,6 +480,7 @@ async fn it_should_remove_an_entry( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -508,6 +519,7 @@ async fn it_should_remove_inactive_peers( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -607,6 +619,7 @@ async fn it_should_remove_peerless_torrents( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] From 87c9834ed31dc89af2b41aa0ec21101bdf648fda Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 19 Apr 2024 08:13:21 +0100 Subject: [PATCH 029/217] chore(deps): update dependencies ```ouput Updating crates.io index Updating chrono v0.4.37 -> v0.4.38 Updating hyper v1.2.0 -> v1.3.1 Updating proc-macro2 v1.0.80 -> v1.0.81 Updating serde v1.0.197 -> v1.0.198 Updating serde_derive v1.0.197 -> v1.0.198 Updating serde_json v1.0.115 -> v1.0.116 Updating syn v2.0.59 -> v2.0.60 Updating toml_edit v0.22.9 -> v0.22.11 ``` --- Cargo.lock | 90 +++++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc6db21c1..70ac685a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -460,7 +460,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -547,7 +547,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -622,7 +622,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "syn_derive", ] @@ -732,9 +732,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -812,7 +812,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1085,7 +1085,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1096,7 +1096,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1143,7 +1143,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1378,7 +1378,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1390,7 +1390,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1402,7 +1402,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1495,7 +1495,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -1723,9 +1723,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -2196,7 +2196,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2247,7 +2247,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "termcolor", "thiserror", ] @@ -2447,7 +2447,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2560,7 +2560,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2629,7 +2629,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -2803,9 +2803,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -3112,7 +3112,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.59", + "syn 2.0.60", "unicode-ident", ] @@ -3340,9 +3340,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] @@ -3368,13 +3368,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3392,9 +3392,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -3419,7 +3419,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3470,7 +3470,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3604,9 +3604,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -3622,7 +3622,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3725,7 +3725,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3819,7 +3819,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -3865,7 +3865,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.11", ] [[package]] @@ -3901,9 +3901,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "fb686a972ccef8537b39eead3968b0e8616cb5040dbb9bba93007c8e07c9215f" dependencies = [ "indexmap 2.2.6", "serde", @@ -4127,7 +4127,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] @@ -4298,7 +4298,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -4332,7 +4332,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4595,7 +4595,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.59", + "syn 2.0.60", ] [[package]] From b3015968ab9b95077a0a5626901a7ee570e6a8aa Mon Sep 17 00:00:00 2001 From: ngthhu Date: Wed, 24 Apr 2024 15:27:39 +0700 Subject: [PATCH 030/217] chore:[#674] Tracker Checker: Ouput in JSON --- Cargo.lock | 1 + Cargo.toml | 2 +- src/console/clients/checker/checks/health.rs | 53 +++++++++---------- src/console/clients/checker/checks/http.rs | 30 +++++++---- src/console/clients/checker/checks/mod.rs | 1 + src/console/clients/checker/checks/structs.rs | 12 +++++ src/console/clients/checker/checks/udp.rs | 42 +++++++++------ src/console/clients/checker/service.rs | 13 +++-- 8 files changed, 95 insertions(+), 59 deletions(-) create mode 100644 src/console/clients/checker/checks/structs.rs diff --git a/Cargo.lock b/Cargo.lock index 70ac685a1..f71275517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3396,6 +3396,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index 94889cbf0..e201f5ba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ ringbuf = "0" serde = { version = "1", features = ["derive"] } serde_bencode = "0" serde_bytes = "0" -serde_json = "1" +serde_json = { version = "1", features = ["preserve_order"] } serde_repr = "0" thiserror = "1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index 9c28da514..bc545510b 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -1,51 +1,50 @@ use std::time::Duration; -use colored::Colorize; use reqwest::{Client as HttpClient, Url, Url as ServiceUrl}; -use crate::console::clients::checker::console::Console; -use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; -pub async fn run(health_checks: &Vec, console: &Console, check_results: &mut Vec) { - console.println("Health checks ..."); +use super::structs::{CheckerOutput, Status}; + +#[allow(clippy::missing_panics_doc)] +pub async fn run(health_checks: &Vec, check_results: &mut Vec) -> Vec { + let mut health_checkers: Vec = Vec::new(); for health_check_url in health_checks { - match run_health_check(health_check_url.clone(), console).await { - Ok(()) => check_results.push(Ok(())), - Err(err) => check_results.push(Err(err)), + let mut health_checker = CheckerOutput { + url: health_check_url.to_string(), + status: Status { + code: String::new(), + message: String::new(), + }, + }; + match run_health_check(health_check_url.clone()).await { + Ok(()) => { + check_results.push(Ok(())); + health_checker.status.code = "ok".to_string(); + } + Err(err) => { + check_results.push(Err(err)); + health_checker.status.code = "error".to_string(); + health_checker.status.message = "Health API is failing.".to_string(); + } } + health_checkers.push(health_checker); } + health_checkers } -async fn run_health_check(url: Url, console: &Console) -> Result<(), CheckError> { +async fn run_health_check(url: Url) -> Result<(), CheckError> { let client = HttpClient::builder().timeout(Duration::from_secs(5)).build().unwrap(); - let colored_url = url.to_string().yellow(); - match client.get(url.clone()).send().await { Ok(response) => { if response.status().is_success() { - console.println(&format!("{} - Health API at {} is OK", "✓".green(), colored_url)); Ok(()) } else { - console.eprintln(&format!( - "{} - Health API at {} is failing: {:?}", - "✗".red(), - colored_url, - response - )); Err(CheckError::HealthCheckError { url }) } } - Err(err) => { - console.eprintln(&format!( - "{} - Health API at {} is failing: {:?}", - "✗".red(), - colored_url, - err - )); - Err(CheckError::HealthCheckError { url }) - } + Err(_) => Err(CheckError::HealthCheckError { url }), } } diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index 501696df4..f65674ceb 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,47 +1,57 @@ use std::str::FromStr; -use colored::Colorize; use log::debug; use reqwest::Url as ServiceUrl; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; -use crate::console::clients::checker::console::Console; -use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce; use crate::shared::bit_torrent::tracker::http::client::responses::scrape; use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; -pub async fn run(http_trackers: &Vec, console: &Console, check_results: &mut Vec) { - console.println("HTTP trackers ..."); +use super::structs::{CheckerOutput, Status}; + +#[allow(clippy::missing_panics_doc)] +pub async fn run(http_trackers: &Vec, check_results: &mut Vec) -> Vec { + let mut http_checkers: Vec = Vec::new(); for http_tracker in http_trackers { - let colored_tracker_url = http_tracker.to_string().yellow(); + let mut http_checker = CheckerOutput { + url: http_tracker.to_string(), + status: Status { + code: String::new(), + message: String::new(), + }, + }; match check_http_announce(http_tracker).await { Ok(()) => { check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); + http_checker.status.code = "ok".to_string(); } Err(err) => { check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); + http_checker.status.code = "error".to_string(); + http_checker.status.message = "Announce is failing.".to_string(); } } match check_http_scrape(http_tracker).await { Ok(()) => { check_results.push(Ok(())); - console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_tracker_url)); + http_checker.status.code = "ok".to_string(); } Err(err) => { check_results.push(Err(err)); - console.println(&format!("{} - Scrape at {} is failing", "✗".red(), colored_tracker_url)); + http_checker.status.code = "error".to_string(); + http_checker.status.message = "Scrape is failing.".to_string(); } } + http_checkers.push(http_checker); } + http_checkers } async fn check_http_announce(tracker_url: &Url) -> Result<(), CheckError> { diff --git a/src/console/clients/checker/checks/mod.rs b/src/console/clients/checker/checks/mod.rs index 16256595e..f8b03f749 100644 --- a/src/console/clients/checker/checks/mod.rs +++ b/src/console/clients/checker/checks/mod.rs @@ -1,3 +1,4 @@ pub mod health; pub mod http; +pub mod structs; pub mod udp; diff --git a/src/console/clients/checker/checks/structs.rs b/src/console/clients/checker/checks/structs.rs new file mode 100644 index 000000000..d28e20c04 --- /dev/null +++ b/src/console/clients/checker/checks/structs.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Status { + pub code: String, + pub message: String, +} +#[derive(Serialize, Deserialize)] +pub struct CheckerOutput { + pub url: String, + pub status: Status, +} diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 47a2a1a00..e9a777a8d 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -1,26 +1,32 @@ use std::net::SocketAddr; use aquatic_udp_protocol::{Port, TransactionId}; -use colored::Colorize; use hex_literal::hex; use log::debug; use torrust_tracker_primitives::info_hash::InfoHash; -use crate::console::clients::checker::console::Console; -use crate::console::clients::checker::printer::Printer; use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::console::clients::udp::checker; +use crate::console::clients::checker::checks::structs::{CheckerOutput, Status}; + const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; -pub async fn run(udp_trackers: &Vec, console: &Console, check_results: &mut Vec) { - console.println("UDP trackers ..."); +#[allow(clippy::missing_panics_doc)] +pub async fn run(udp_trackers: &Vec, check_results: &mut Vec) -> Vec { + let mut udp_checkers: Vec = Vec::new(); for udp_tracker in udp_trackers { - debug!("UDP tracker: {:?}", udp_tracker); + let mut checker_output = CheckerOutput { + url: udp_tracker.to_string(), + status: Status { + code: String::new(), + message: String::new(), + }, + }; - let colored_tracker_url = udp_tracker.to_string().yellow(); + debug!("UDP tracker: {:?}", udp_tracker); let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); @@ -32,7 +38,8 @@ pub async fn run(udp_trackers: &Vec, console: &Console, check_result check_results.push(Err(CheckError::UdpError { socket_addr: *udp_tracker, })); - console.println(&format!("{} - Can't connect to socket {}", "✗".red(), colored_tracker_url)); + checker_output.status.code = "error".to_string(); + checker_output.status.message = "Can't connect to socket.".to_string(); break; }; @@ -42,11 +49,8 @@ pub async fn run(udp_trackers: &Vec, console: &Console, check_result check_results.push(Err(CheckError::UdpError { socket_addr: *udp_tracker, })); - console.println(&format!( - "{} - Can't make tracker connection request to {}", - "✗".red(), - colored_tracker_url - )); + checker_output.status.code = "error".to_string(); + checker_output.status.message = "Can't make tracker connection request.".to_string(); break; }; @@ -60,13 +64,14 @@ pub async fn run(udp_trackers: &Vec, console: &Console, check_result .is_ok() { check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); + checker_output.status.code = "ok".to_string(); } else { let err = CheckError::UdpError { socket_addr: *udp_tracker, }; check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); + checker_output.status.code = "error".to_string(); + checker_output.status.message = "Announce is failing.".to_string(); } debug!("Send scrape request"); @@ -75,13 +80,16 @@ pub async fn run(udp_trackers: &Vec, console: &Console, check_result if (client.send_scrape_request(connection_id, transaction_id, info_hashes).await).is_ok() { check_results.push(Ok(())); - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url)); + checker_output.status.code = "ok".to_string(); } else { let err = CheckError::UdpError { socket_addr: *udp_tracker, }; check_results.push(Err(err)); - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url)); + checker_output.status.code = "error".to_string(); + checker_output.status.message = "Scrape is failing.".to_string(); } + udp_checkers.push(checker_output); } + udp_checkers } diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index 94eff4a88..fd97342cc 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use reqwest::Url; -use super::checks; +use super::checks::{self}; use super::config::Configuration; use super::console::Console; use crate::console::clients::checker::printer::Printer; @@ -26,16 +26,21 @@ impl Service { /// # Errors /// /// Will return OK is all checks pass or an array with the check errors. + #[allow(clippy::missing_panics_doc)] pub async fn run_checks(&self) -> Vec { self.console.println("Running checks for trackers ..."); let mut check_results = vec![]; - checks::udp::run(&self.config.udp_trackers, &self.console, &mut check_results).await; + let udp_checkers = checks::udp::run(&self.config.udp_trackers, &mut check_results).await; - checks::http::run(&self.config.http_trackers, &self.console, &mut check_results).await; + let http_checkers = checks::http::run(&self.config.http_trackers, &mut check_results).await; - checks::health::run(&self.config.health_checks, &self.console, &mut check_results).await; + let health_checkers = checks::health::run(&self.config.health_checks, &mut check_results).await; + + let json_output = + serde_json::json!({ "udp_trackers": udp_checkers, "http_trackers": http_checkers, "health_checks": health_checkers }); + self.console.println(&serde_json::to_string_pretty(&json_output).unwrap()); check_results } From 7de4fbcf727e0b2484c80c5385dca78aba20446b Mon Sep 17 00:00:00 2001 From: ngthhu Date: Wed, 24 Apr 2024 21:25:30 +0700 Subject: [PATCH 031/217] format fix --- src/console/clients/checker/checks/health.rs | 3 +-- src/console/clients/checker/checks/http.rs | 3 +-- src/console/clients/checker/checks/udp.rs | 3 +-- src/console/clients/checker/service.rs | 2 -- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index bc545510b..47eec4cbd 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -2,9 +2,8 @@ use std::time::Duration; use reqwest::{Client as HttpClient, Url, Url as ServiceUrl}; -use crate::console::clients::checker::service::{CheckError, CheckResult}; - use super::structs::{CheckerOutput, Status}; +use crate::console::clients::checker::service::{CheckError, CheckResult}; #[allow(clippy::missing_panics_doc)] pub async fn run(health_checks: &Vec, check_results: &mut Vec) -> Vec { diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index f65674ceb..e0b14b480 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -5,14 +5,13 @@ use reqwest::Url as ServiceUrl; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; +use super::structs::{CheckerOutput, Status}; use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce; use crate::shared::bit_torrent::tracker::http::client::responses::scrape; use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; -use super::structs::{CheckerOutput, Status}; - #[allow(clippy::missing_panics_doc)] pub async fn run(http_trackers: &Vec, check_results: &mut Vec) -> Vec { let mut http_checkers: Vec = Vec::new(); diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index e9a777a8d..48f72edf9 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -5,11 +5,10 @@ use hex_literal::hex; use log::debug; use torrust_tracker_primitives::info_hash::InfoHash; +use crate::console::clients::checker::checks::structs::{CheckerOutput, Status}; use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::console::clients::udp::checker; -use crate::console::clients::checker::checks::structs::{CheckerOutput, Status}; - const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index fd97342cc..16483e92e 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -28,8 +28,6 @@ impl Service { /// Will return OK is all checks pass or an array with the check errors. #[allow(clippy::missing_panics_doc)] pub async fn run_checks(&self) -> Vec { - self.console.println("Running checks for trackers ..."); - let mut check_results = vec![]; let udp_checkers = checks::udp::run(&self.config.udp_trackers, &mut check_results).await; From b27f002ac6e2fb21142bc21c1eb6580544c1bf2f Mon Sep 17 00:00:00 2001 From: ngthhu Date: Wed, 24 Apr 2024 23:00:27 +0700 Subject: [PATCH 032/217] remove unused dependencies --- Cargo.lock | 11 ----------- Cargo.toml | 1 - 2 files changed, 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f71275517..44f9db17c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,16 +836,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - [[package]] name = "concurrent-queue" version = "2.4.0" @@ -3926,7 +3916,6 @@ dependencies = [ "axum-server", "chrono", "clap", - "colored", "config", "crossbeam-skiplist", "dashmap", diff --git a/Cargo.toml b/Cargo.toml index e201f5ba9..0e37b7ad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } -colored = "2" config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" From effca568032eaffcdb9d3f984000b238962ce438 Mon Sep 17 00:00:00 2001 From: ngthhu Date: Thu, 25 Apr 2024 17:22:01 +0700 Subject: [PATCH 033/217] refactor: [#681] udp return errors instead of panicking --- src/console/clients/udp/checker.rs | 16 +- src/shared/bit_torrent/tracker/udp/client.rs | 215 +++++++++++-------- tests/servers/udp/contract.rs | 71 ++++-- 3 files changed, 193 insertions(+), 109 deletions(-) diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index 12b8d764c..9b2a9011e 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -64,7 +64,7 @@ impl Client { let binding_address = local_bind_to.parse().context("binding local address")?; debug!("Binding to: {local_bind_to}"); - let udp_client = UdpClient::bind(&local_bind_to).await; + let udp_client = UdpClient::bind(&local_bind_to).await?; let bound_to = udp_client.socket.local_addr().context("bound local address")?; debug!("Bound to: {bound_to}"); @@ -88,7 +88,7 @@ impl Client { match &self.udp_tracker_client { Some(client) => { - client.udp_client.connect(&tracker_socket_addr.to_string()).await; + client.udp_client.connect(&tracker_socket_addr.to_string()).await?; self.remote_socket = Some(*tracker_socket_addr); Ok(()) } @@ -116,9 +116,9 @@ impl Client { match &self.udp_tracker_client { Some(client) => { - client.send(connect_request.into()).await; + client.send(connect_request.into()).await?; - let response = client.receive().await; + let response = client.receive().await?; debug!("connection request response:\n{response:#?}"); @@ -163,9 +163,9 @@ impl Client { match &self.udp_tracker_client { Some(client) => { - client.send(announce_request.into()).await; + client.send(announce_request.into()).await?; - let response = client.receive().await; + let response = client.receive().await?; debug!("announce request response:\n{response:#?}"); @@ -200,9 +200,9 @@ impl Client { match &self.udp_tracker_client { Some(client) => { - client.send(scrape_request.into()).await; + client.send(scrape_request.into()).await?; - let response = client.receive().await; + let response = client.receive().await?; debug!("scrape request response:\n{response:#?}"); diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 11c8d8f62..9af9571bc 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -1,8 +1,10 @@ +use core::result::Result::{Err, Ok}; use std::io::Cursor; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; +use anyhow::{anyhow, Context, Result}; use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId}; use log::debug; use tokio::net::UdpSocket; @@ -25,99 +27,120 @@ pub struct UdpClient { } impl UdpClient { - /// # Panics + /// # Errors + /// + /// Will return error if the local address can't be bound. /// - /// Will panic if the local address can't be bound. - pub async fn bind(local_address: &str) -> Self { - let valid_socket_addr = local_address + pub async fn bind(local_address: &str) -> Result { + let socket_addr = local_address .parse::() - .unwrap_or_else(|_| panic!("{local_address} is not a valid socket address")); + .context(format!("{local_address} is not a valid socket address"))?; - let socket = UdpSocket::bind(valid_socket_addr).await.unwrap(); + let socket = UdpSocket::bind(socket_addr).await?; - Self { + let udp_client = Self { socket: Arc::new(socket), timeout: DEFAULT_TIMEOUT, - } + }; + Ok(udp_client) } - /// # Panics + /// # Errors /// - /// Will panic if can't connect to the socket. - pub async fn connect(&self, remote_address: &str) { - let valid_socket_addr = remote_address + /// Will return error if can't connect to the socket. + pub async fn connect(&self, remote_address: &str) -> Result<()> { + let socket_addr = remote_address .parse::() - .unwrap_or_else(|_| panic!("{remote_address} is not a valid socket address")); + .context(format!("{remote_address} is not a valid socket address"))?; - match self.socket.connect(valid_socket_addr).await { - Ok(()) => debug!("Connected successfully"), - Err(e) => panic!("Failed to connect: {e:?}"), + match self.socket.connect(socket_addr).await { + Ok(()) => { + debug!("Connected successfully"); + Ok(()) + } + Err(e) => Err(anyhow!("Failed to connect: {e:?}")), } } - /// # Panics + /// # Errors /// - /// Will panic if: + /// Will return error if: /// /// - Can't write to the socket. /// - Can't send data. - pub async fn send(&self, bytes: &[u8]) -> usize { + pub async fn send(&self, bytes: &[u8]) -> Result { debug!(target: "UDP client", "sending {bytes:?} ..."); match time::timeout(self.timeout, self.socket.writable()).await { - Ok(writable_result) => match writable_result { - Ok(()) => (), - Err(e) => panic!("{}", format!("IO error waiting for the socket to become readable: {e:?}")), - }, - Err(e) => panic!("{}", format!("Timeout waiting for the socket to become readable: {e:?}")), + Ok(writable_result) => { + match writable_result { + Ok(()) => (), + Err(e) => return Err(anyhow!("IO error waiting for the socket to become readable: {e:?}")), + }; + } + Err(e) => return Err(anyhow!("Timeout waiting for the socket to become readable: {e:?}")), }; match time::timeout(self.timeout, self.socket.send(bytes)).await { Ok(send_result) => match send_result { - Ok(size) => size, - Err(e) => panic!("{}", format!("IO error during send: {e:?}")), + Ok(size) => Ok(size), + Err(e) => Err(anyhow!("IO error during send: {e:?}")), }, - Err(e) => panic!("{}", format!("Send operation timed out: {e:?}")), + Err(e) => Err(anyhow!("Send operation timed out: {e:?}")), } } - /// # Panics + /// # Errors /// - /// Will panic if: + /// Will return error if: /// /// - Can't read from the socket. /// - Can't receive data. - pub async fn receive(&self, bytes: &mut [u8]) -> usize { + /// + /// # Panics + /// + pub async fn receive(&self, bytes: &mut [u8]) -> Result { debug!(target: "UDP client", "receiving ..."); match time::timeout(self.timeout, self.socket.readable()).await { - Ok(readable_result) => match readable_result { - Ok(()) => (), - Err(e) => panic!("{}", format!("IO error waiting for the socket to become readable: {e:?}")), - }, - Err(e) => panic!("{}", format!("Timeout waiting for the socket to become readable: {e:?}")), + Ok(readable_result) => { + match readable_result { + Ok(()) => (), + Err(e) => return Err(anyhow!("IO error waiting for the socket to become readable: {e:?}")), + }; + } + Err(e) => return Err(anyhow!("Timeout waiting for the socket to become readable: {e:?}")), }; - let size = match time::timeout(self.timeout, self.socket.recv(bytes)).await { + let size_result = match time::timeout(self.timeout, self.socket.recv(bytes)).await { Ok(recv_result) => match recv_result { - Ok(size) => size, - Err(e) => panic!("{}", format!("IO error during send: {e:?}")), + Ok(size) => Ok(size), + Err(e) => Err(anyhow!("IO error during send: {e:?}")), }, - Err(e) => panic!("{}", format!("Receive operation timed out: {e:?}")), + Err(e) => Err(anyhow!("Receive operation timed out: {e:?}")), }; - debug!(target: "UDP client", "{size} bytes received {bytes:?}"); - - size + if size_result.is_ok() { + let size = size_result.as_ref().unwrap(); + debug!(target: "UDP client", "{size} bytes received {bytes:?}"); + size_result + } else { + size_result + } } } /// Creates a new `UdpClient` connected to a Udp server -pub async fn new_udp_client_connected(remote_address: &str) -> UdpClient { +/// +/// # Errors +/// +/// Will return any errors present in the call stack +/// +pub async fn new_udp_client_connected(remote_address: &str) -> Result { let port = 0; // Let OS choose an unused port. - let client = UdpClient::bind(&source_address(port)).await; - client.connect(remote_address).await; - client + let client = UdpClient::bind(&source_address(port)).await?; + client.connect(remote_address).await?; + Ok(client) } #[allow(clippy::module_name_repetitions)] @@ -127,85 +150,103 @@ pub struct UdpTrackerClient { } impl UdpTrackerClient { - /// # Panics + /// # Errors /// - /// Will panic if can't write request to bytes. - pub async fn send(&self, request: Request) -> usize { + /// Will return error if can't write request to bytes. + pub async fn send(&self, request: Request) -> Result { debug!(target: "UDP tracker client", "send request {request:?}"); // Write request into a buffer let request_buffer = vec![0u8; MAX_PACKET_SIZE]; let mut cursor = Cursor::new(request_buffer); - let request_data = match request.write(&mut cursor) { + let request_data_result = match request.write(&mut cursor) { Ok(()) => { #[allow(clippy::cast_possible_truncation)] let position = cursor.position() as usize; let inner_request_buffer = cursor.get_ref(); // Return slice which contains written request data - &inner_request_buffer[..position] + Ok(&inner_request_buffer[..position]) } - Err(e) => panic!("could not write request to bytes: {e}."), + Err(e) => Err(anyhow!("could not write request to bytes: {e}.")), }; + let request_data = request_data_result?; + self.udp_client.send(request_data).await } - /// # Panics + /// # Errors /// - /// Will panic if can't create response from the received payload (bytes buffer). - pub async fn receive(&self) -> Response { + /// Will return error if can't create response from the received payload (bytes buffer). + pub async fn receive(&self) -> Result { let mut response_buffer = [0u8; MAX_PACKET_SIZE]; - let payload_size = self.udp_client.receive(&mut response_buffer).await; + let payload_size = self.udp_client.receive(&mut response_buffer).await?; debug!(target: "UDP tracker client", "received {payload_size} bytes. Response {response_buffer:?}"); - Response::from_bytes(&response_buffer[..payload_size], true).unwrap() + let response = Response::from_bytes(&response_buffer[..payload_size], true)?; + + Ok(response) } } /// Creates a new `UdpTrackerClient` connected to a Udp Tracker server -pub async fn new_udp_tracker_client_connected(remote_address: &str) -> UdpTrackerClient { - let udp_client = new_udp_client_connected(remote_address).await; - UdpTrackerClient { udp_client } +/// +/// # Errors +/// +/// Will return any errors present in the call stack +/// +pub async fn new_udp_tracker_client_connected(remote_address: &str) -> Result { + let udp_client = new_udp_client_connected(remote_address).await?; + let udp_tracker_client = UdpTrackerClient { udp_client }; + Ok(udp_tracker_client) } /// Helper Function to Check if a UDP Service is Connectable /// -/// # Errors +/// # Panics /// /// It will return an error if unable to connect to the UDP service. /// -/// # Panics +/// # Errors +/// pub async fn check(binding: &SocketAddr) -> Result { debug!("Checking Service (detail): {binding:?}."); - let client = new_udp_tracker_client_connected(binding.to_string().as_str()).await; - - let connect_request = ConnectRequest { - transaction_id: TransactionId(123), - }; - - client.send(connect_request.into()).await; - - let process = move |response| { - if matches!(response, Response::Connect(_connect_response)) { - Ok("Connected".to_string()) - } else { - Err("Did not Connect".to_string()) - } - }; - - let sleep = time::sleep(Duration::from_millis(2000)); - tokio::pin!(sleep); - - tokio::select! { - () = &mut sleep => { - Err("Timed Out".to_string()) - } - response = client.receive() => { - process(response) + match new_udp_tracker_client_connected(binding.to_string().as_str()).await { + Ok(client) => { + let connect_request = ConnectRequest { + transaction_id: TransactionId(123), + }; + + // client.send() return usize, but doesn't use here + match client.send(connect_request.into()).await { + Ok(_) => (), + Err(e) => debug!("Error: {e:?}."), + }; + + let process = move |response| { + if matches!(response, Response::Connect(_connect_response)) { + Ok("Connected".to_string()) + } else { + Err("Did not Connect".to_string()) + } + }; + + let sleep = time::sleep(Duration::from_millis(2000)); + tokio::pin!(sleep); + + tokio::select! { + () = &mut sleep => { + Err("Timed Out".to_string()) + } + response = client.receive() => { + process(response.unwrap()) + } + } } + Err(e) => Err(format!("{e:?}")), } } diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index 91dca4d42..56e400f84 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -24,9 +24,15 @@ fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { let connect_request = ConnectRequest { transaction_id }; - client.send(connect_request.into()).await; + match client.send(connect_request.into()).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; - let response = client.receive().await; + let response = match client.receive().await { + Ok(response) => response, + Err(err) => panic!("{err}"), + }; match response { Response::Connect(connect_response) => connect_response.connection_id, @@ -38,12 +44,22 @@ async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrac async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_request() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_client_connected(&env.bind_address().to_string()).await; + let client = match new_udp_client_connected(&env.bind_address().to_string()).await { + Ok(udp_client) => udp_client, + Err(err) => panic!("{err}"), + }; - client.send(&empty_udp_request()).await; + match client.send(&empty_udp_request()).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; let mut buffer = empty_buffer(); - client.receive(&mut buffer).await; + match client.receive(&mut buffer).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; + let response = Response::from_bytes(&buffer, true).unwrap(); assert!(is_error_response(&response, "bad request")); @@ -63,15 +79,24 @@ mod receiving_a_connection_request { async fn should_return_a_connect_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = match new_udp_tracker_client_connected(&env.bind_address().to_string()).await { + Ok(udp_tracker_client) => udp_tracker_client, + Err(err) => panic!("{err}"), + }; let connect_request = ConnectRequest { transaction_id: TransactionId(123), }; - client.send(connect_request.into()).await; + match client.send(connect_request.into()).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; - let response = client.receive().await; + let response = match client.receive().await { + Ok(response) => response, + Err(err) => panic!("{err}"), + }; assert!(is_connect_response(&response, TransactionId(123))); @@ -97,7 +122,10 @@ mod receiving_an_announce_request { async fn should_return_an_announce_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = match new_udp_tracker_client_connected(&env.bind_address().to_string()).await { + Ok(udp_tracker_client) => udp_tracker_client, + Err(err) => panic!("{err}"), + }; let connection_id = send_connection_request(TransactionId(123), &client).await; @@ -118,9 +146,15 @@ mod receiving_an_announce_request { port: Port(client.udp_client.socket.local_addr().unwrap().port()), }; - client.send(announce_request.into()).await; + match client.send(announce_request.into()).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; - let response = client.receive().await; + let response = match client.receive().await { + Ok(response) => response, + Err(err) => panic!("{err}"), + }; println!("test response {response:?}"); @@ -143,7 +177,10 @@ mod receiving_an_scrape_request { async fn should_return_a_scrape_response() { let env = Started::new(&configuration::ephemeral().into()).await; - let client = new_udp_tracker_client_connected(&env.bind_address().to_string()).await; + let client = match new_udp_tracker_client_connected(&env.bind_address().to_string()).await { + Ok(udp_tracker_client) => udp_tracker_client, + Err(err) => panic!("{err}"), + }; let connection_id = send_connection_request(TransactionId(123), &client).await; @@ -159,9 +196,15 @@ mod receiving_an_scrape_request { info_hashes, }; - client.send(scrape_request.into()).await; + match client.send(scrape_request.into()).await { + Ok(_) => (), + Err(err) => panic!("{err}"), + }; - let response = client.receive().await; + let response = match client.receive().await { + Ok(response) => response, + Err(err) => panic!("{err}"), + }; assert!(is_scrape_response(&response)); From 895efe9c4154a6883008e9cf26592f3fed38602d Mon Sep 17 00:00:00 2001 From: ngthhu Date: Thu, 2 May 2024 16:02:42 +0700 Subject: [PATCH 034/217] refactor: [#680] http return errors instead of panicking --- src/console/clients/checker/checks/http.rs | 22 +++++- src/console/clients/http/app.rs | 10 +-- .../bit_torrent/tracker/http/client/mod.rs | 72 ++++++++++--------- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index e0b14b480..e526b5e57 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -61,9 +61,19 @@ async fn check_http_announce(tracker_url: &Url) -> Result<(), CheckError> { // We should change the client to catch that error and return a `CheckError`. // Otherwise the checking process will stop. The idea is to process all checks // and return a final report. - let response = Client::new(tracker_url.clone()) + let Ok(client) = Client::new(tracker_url.clone()) else { + return Err(CheckError::HttpError { + url: (tracker_url.to_owned()), + }); + }; + let Ok(response) = client .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await; + .await + else { + return Err(CheckError::HttpError { + url: (tracker_url.to_owned()), + }); + }; if let Ok(body) = response.bytes().await { if let Ok(_announce_response) = serde_bencode::from_bytes::(&body) { @@ -89,7 +99,13 @@ async fn check_http_scrape(url: &Url) -> Result<(), CheckError> { // We should change the client to catch that error and return a `CheckError`. // Otherwise the checking process will stop. The idea is to process all checks // and return a final report. - let response = Client::new(url.clone()).scrape(&query).await; + + let Ok(client) = Client::new(url.clone()) else { + return Err(CheckError::HttpError { url: (url.to_owned()) }); + }; + let Ok(response) = client.scrape(&query).await else { + return Err(CheckError::HttpError { url: (url.to_owned()) }); + }; if let Ok(body) = response.bytes().await { if let Ok(_scrape_response) = scrape::Response::try_from_bencoded(&body) { diff --git a/src/console/clients/http/app.rs b/src/console/clients/http/app.rs index 511fb6628..8fc9db0c3 100644 --- a/src/console/clients/http/app.rs +++ b/src/console/clients/http/app.rs @@ -64,11 +64,11 @@ async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Res let info_hash = InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); - let response = Client::new(base_url) + let response = Client::new(base_url)? .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await; + .await?; - let body = response.bytes().await.unwrap(); + let body = response.bytes().await?; let announce_response: Announce = serde_bencode::from_bytes(&body) .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body)); @@ -85,9 +85,9 @@ async fn scrape_command(tracker_url: &str, info_hashes: &[String]) -> anyhow::Re let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; - let response = Client::new(base_url).scrape(&query).await; + let response = Client::new(base_url)?.scrape(&query).await?; - let body = response.bytes().await.unwrap(); + let body = response.bytes().await?; let scrape_response = scrape::Response::try_from_bencoded(&body) .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body)); diff --git a/src/shared/bit_torrent/tracker/http/client/mod.rs b/src/shared/bit_torrent/tracker/http/client/mod.rs index a75b0fec3..f5b1b3310 100644 --- a/src/shared/bit_torrent/tracker/http/client/mod.rs +++ b/src/shared/bit_torrent/tracker/http/client/mod.rs @@ -3,6 +3,7 @@ pub mod responses; use std::net::IpAddr; +use anyhow::{anyhow, Result}; use requests::announce::{self, Query}; use requests::scrape; use reqwest::{Client as ReqwestClient, Response, Url}; @@ -25,78 +26,83 @@ pub struct Client { /// base url path query /// ``` impl Client { - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn new(base_url: Url) -> Self { - Self { + pub fn new(base_url: Url) -> Result { + let reqwest = reqwest::Client::builder().build()?; + Ok(Self { base_url, - reqwest: reqwest::Client::builder().build().unwrap(), + reqwest, key: None, - } + }) } /// Creates the new client binding it to an specific local address. /// - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn bind(base_url: Url, local_address: IpAddr) -> Self { - Self { + pub fn bind(base_url: Url, local_address: IpAddr) -> Result { + let reqwest = reqwest::Client::builder().local_address(local_address).build()?; + Ok(Self { base_url, - reqwest: reqwest::Client::builder().local_address(local_address).build().unwrap(), + reqwest, key: None, - } + }) } - /// # Panics + /// # Errors /// /// This method fails if the client builder fails. - #[must_use] - pub fn authenticated(base_url: Url, key: Key) -> Self { - Self { + pub fn authenticated(base_url: Url, key: Key) -> Result { + let reqwest = reqwest::Client::builder().build()?; + Ok(Self { base_url, - reqwest: reqwest::Client::builder().build().unwrap(), + reqwest, key: Some(key), - } + }) } - pub async fn announce(&self, query: &announce::Query) -> Response { + /// # Errors + pub async fn announce(&self, query: &announce::Query) -> Result { self.get(&self.build_announce_path_and_query(query)).await } - pub async fn scrape(&self, query: &scrape::Query) -> Response { + /// # Errors + pub async fn scrape(&self, query: &scrape::Query) -> Result { self.get(&self.build_scrape_path_and_query(query)).await } - pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Response { + /// # Errors + pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Result { self.get_with_header(&self.build_announce_path_and_query(query), key, value) .await } - pub async fn health_check(&self) -> Response { + /// # Errors + pub async fn health_check(&self) -> Result { self.get(&self.build_path("health_check")).await } - /// # Panics + /// # Errors /// /// This method fails if there was an error while sending request. - pub async fn get(&self, path: &str) -> Response { - self.reqwest.get(self.build_url(path)).send().await.unwrap() + pub async fn get(&self, path: &str) -> Result { + match self.reqwest.get(self.build_url(path)).send().await { + Ok(response) => Ok(response), + Err(err) => Err(anyhow!("{err}")), + } } - /// # Panics + /// # Errors /// /// This method fails if there was an error while sending request. - pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Response { - self.reqwest - .get(self.build_url(path)) - .header(key, value) - .send() - .await - .unwrap() + pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Result { + match self.reqwest.get(self.build_url(path)).header(key, value).send().await { + Ok(response) => Ok(response), + Err(err) => Err(anyhow!("{err}")), + } } fn build_announce_path_and_query(&self, query: &announce::Query) -> String { From 75518570ae086ada07bbce6d077475a67461b944 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 6 May 2024 08:03:56 +0100 Subject: [PATCH 035/217] chore(deps): update dependencies --- Cargo.lock | 279 +++++++++++++++++++++++++++-------------------------- Cargo.toml | 4 +- 2 files changed, 144 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44f9db17c..143ba1aac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,47 +93,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -196,16 +197,16 @@ checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", "event-listener 5.3.0", - "event-listener-strategy 0.5.1", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "brotli", "flate2", @@ -225,7 +226,7 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-lite 2.3.0", "slab", ] @@ -278,8 +279,8 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.6.0", - "rustix 0.38.32", + "polling 3.7.0", + "rustix 0.38.34", "slab", "tracing", "windows-sys 0.52.0", @@ -334,9 +335,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" @@ -357,9 +358,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -509,9 +510,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bigdecimal" @@ -588,25 +589,23 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel 2.2.1", "async-lock 3.3.0", "async-task", - "fastrand 2.0.2", "futures-io", "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -614,9 +613,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -628,9 +627,9 @@ dependencies = [ [[package]] name = "brotli" -version = "4.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" +checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -639,9 +638,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "3.0.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" +checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -701,12 +700,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -832,15 +832,15 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -1096,7 +1096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1242,9 +1242,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener 5.3.0", "pin-project-lite", @@ -1273,9 +1273,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fern" @@ -1288,9 +1288,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-sys", @@ -1470,7 +1470,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -1618,9 +1618,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", "allocator-api2", @@ -1632,7 +1632,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1761,7 +1761,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "tower", "tower-service", @@ -1825,7 +1825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -1875,6 +1875,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -1901,9 +1907,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -2024,9 +2030,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" @@ -2092,9 +2098,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2219,7 +2225,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "socket2 0.5.6", + "socket2 0.5.7", "twox-hash", "url", ] @@ -2376,9 +2382,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -2476,9 +2482,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -2486,15 +2492,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -2521,9 +2527,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -2532,9 +2538,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73541b156d32197eecda1a4014d7f868fd2bcb3c550d5386087cfba442bf69c" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -2542,9 +2548,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35eeed0a3fab112f75165fdc026b3913f4183133f19b49be773ac9ea966e8bd" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", @@ -2555,9 +2561,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.9" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adbf29bb9776f28caece835398781ab24435585fe0d4dc1374a61db5accedca" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -2641,7 +2647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", ] @@ -2697,15 +2703,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.32", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -2919,11 +2925,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -2957,9 +2963,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "relative-path" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rend" @@ -2972,11 +2978,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -3183,9 +3189,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -3196,9 +3202,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -3212,15 +3218,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" @@ -3301,11 +3307,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -3314,9 +3320,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -3330,9 +3336,9 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] @@ -3358,9 +3364,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -3436,11 +3442,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3454,9 +3460,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", @@ -3494,9 +3500,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3540,9 +3546,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3679,8 +3685,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", - "rustix 0.38.32", + "fastrand 2.1.0", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -3701,18 +3707,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -3797,7 +3803,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] @@ -3835,16 +3841,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -3856,7 +3861,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.11", + "toml_edit 0.22.12", ] [[package]] @@ -3892,15 +3897,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.11" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb686a972ccef8537b39eead3968b0e8616cb5040dbb9bba93007c8e07c9215f" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.8", ] [[package]] @@ -4220,9 +4225,9 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" [[package]] name = "vcpkg" @@ -4361,11 +4366,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4533,9 +4538,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -4570,18 +4575,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 0e37b7ad0..486c41230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "3.0.0-alpha.12-develop" [dependencies] anyhow = "1" -aquatic_udp_protocol = "0" +aquatic_udp_protocol = "0.8" async-trait = "0" axum = { version = "0", features = ["macros"] } axum-client-ip = "0" @@ -57,7 +57,7 @@ r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" reqwest = { version = "0", features = ["json"] } -ringbuf = "0" +ringbuf = "0.3.3" serde = { version = "1", features = ["derive"] } serde_bencode = "0" serde_bytes = "0" From 801d91363a7570efef75d4aede39b961564963d1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 10:05:48 +0100 Subject: [PATCH 036/217] chore(deps): bump aquatic_udp_protocol from 0.8.0 to 0.9.0 --- Cargo.lock | 65 +++++++- Cargo.toml | 3 +- cSpell.json | 3 +- src/console/clients/checker/checks/udp.rs | 4 +- src/console/clients/udp/app.rs | 6 +- src/console/clients/udp/checker.rs | 19 +-- src/console/clients/udp/responses.rs | 38 ++--- src/servers/udp/connection_cookie.rs | 8 +- src/servers/udp/handlers.rs | 152 ++++++++++--------- src/servers/udp/peer_builder.rs | 17 ++- src/servers/udp/request.rs | 2 +- src/servers/udp/server.rs | 2 +- src/shared/bit_torrent/tracker/udp/client.rs | 7 +- tests/servers/udp/contract.rs | 35 ++--- 14 files changed, 225 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 143ba1aac..b7f592666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,14 +146,30 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +[[package]] +name = "aquatic_peer_id" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0732a73df221dcb25713849c6ebaf57b85355f669716652a7466f688cc06f25" +dependencies = [ + "compact_str", + "hex", + "quickcheck", + "regex", + "serde", + "zerocopy", +] + [[package]] name = "aquatic_udp_protocol" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2919b480121f7d20d247524da62bad1b6b7928bc3f50898f624b5c592727341" +checksum = "0af90e5162f5fcbde33524128f08dc52a779f32512d5f8692eadd4b55c89389e" dependencies = [ + "aquatic_peer_id", "byteorder", "either", + "zerocopy", ] [[package]] @@ -698,6 +714,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.96" @@ -836,6 +861,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1176,6 +1214,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2826,6 +2874,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + [[package]] name = "quote" version = "1.0.36" @@ -3961,6 +4020,7 @@ dependencies = [ "tracing", "url", "uuid", + "zerocopy", ] [[package]] @@ -4579,6 +4639,7 @@ version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" dependencies = [ + "byteorder", "zerocopy-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 486c41230..63735450e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "3.0.0-alpha.12-develop" [dependencies] anyhow = "1" -aquatic_udp_protocol = "0.8" +aquatic_udp_protocol = "0" async-trait = "0" axum = { version = "0", features = ["macros"] } axum-client-ip = "0" @@ -76,6 +76,7 @@ trace = "0" tracing = "0" url = "2" uuid = { version = "1", features = ["v4"] } +zerocopy = "0.7.33" [package.metadata.cargo-machete] ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] diff --git a/cSpell.json b/cSpell.json index 24ef6b0a0..2473e9c33 100644 --- a/cSpell.json +++ b/cSpell.json @@ -170,7 +170,8 @@ "Xtorrent", "Xunlei", "xxxxxxxxxxxxxxxxxxxxd", - "yyyyyyyyyyyyyyyyyyyyd" + "yyyyyyyyyyyyyyyyyyyyd", + "zerocopy" ], "enableFiletypes": [ "dockerfile", diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 48f72edf9..6458190d4 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -27,7 +27,7 @@ pub async fn run(udp_trackers: &Vec, check_results: &mut Vec, check_results: &mut Vec anyhow::Result { - let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); + let transaction_id = TransactionId::new(RANDOM_TRANSACTION_ID); let mut client = checker::Client::default(); @@ -151,12 +151,12 @@ async fn handle_announce(tracker_socket_addr: &SocketAddr, info_hash: &TorrustIn let connection_id = client.send_connection_request(transaction_id).await?; client - .send_announce_request(connection_id, transaction_id, *info_hash, Port(bound_to.port())) + .send_announce_request(connection_id, transaction_id, *info_hash, Port(bound_to.port().into())) .await } async fn handle_scrape(tracker_socket_addr: &SocketAddr, info_hashes: &[TorrustInfoHash]) -> anyhow::Result { - let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); + let transaction_id = TransactionId::new(RANDOM_TRANSACTION_ID); let mut client = checker::Client::default(); diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index 9b2a9011e..d51492041 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -3,8 +3,8 @@ use std::net::{Ipv4Addr, SocketAddr}; use anyhow::Context; use aquatic_udp_protocol::common::InfoHash; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, - ScrapeRequest, TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, + PeerId, PeerKey, Port, Response, ScrapeRequest, TransactionId, }; use log::debug; use thiserror::Error; @@ -148,16 +148,17 @@ impl Client { let announce_request = AnnounceRequest { connection_id, + action_placeholder: AnnounceActionPlaceholder::default(), transaction_id, info_hash: InfoHash(info_hash.bytes()), peer_id: PeerId(*b"-qB00000000000000001"), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4Addr::new(0, 0, 0, 0).into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers(1i32.into()), port: client_port, }; diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 2fbc38f5f..8ea1a978b 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,7 +1,7 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use aquatic_udp_protocol::{AnnounceResponse, ScrapeResponse}; +use aquatic_udp_protocol::{AnnounceResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; #[derive(Serialize)] @@ -13,33 +13,33 @@ pub struct AnnounceResponseDto { peers: Vec, } -impl From> for AnnounceResponseDto { - fn from(announce: AnnounceResponse) -> Self { +impl From> for AnnounceResponseDto { + fn from(announce: AnnounceResponse) -> Self { Self { - transaction_id: announce.transaction_id.0, - announce_interval: announce.announce_interval.0, - leechers: announce.leechers.0, - seeders: announce.seeders.0, + transaction_id: announce.fixed.transaction_id.0.into(), + announce_interval: announce.fixed.announce_interval.0.into(), + leechers: announce.fixed.leechers.0.into(), + seeders: announce.fixed.seeders.0.into(), peers: announce .peers .iter() - .map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)) + .map(|peer| format!("{}:{}", Ipv4Addr::from(peer.ip_address), peer.port.0)) .collect::>(), } } } -impl From> for AnnounceResponseDto { - fn from(announce: AnnounceResponse) -> Self { +impl From> for AnnounceResponseDto { + fn from(announce: AnnounceResponse) -> Self { Self { - transaction_id: announce.transaction_id.0, - announce_interval: announce.announce_interval.0, - leechers: announce.leechers.0, - seeders: announce.seeders.0, + transaction_id: announce.fixed.transaction_id.0.into(), + announce_interval: announce.fixed.announce_interval.0.into(), + leechers: announce.fixed.leechers.0.into(), + seeders: announce.fixed.seeders.0.into(), peers: announce .peers .iter() - .map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)) + .map(|peer| format!("{}:{}", Ipv6Addr::from(peer.ip_address), peer.port.0)) .collect::>(), } } @@ -54,14 +54,14 @@ pub struct ScrapeResponseDto { impl From for ScrapeResponseDto { fn from(scrape: ScrapeResponse) -> Self { Self { - transaction_id: scrape.transaction_id.0, + transaction_id: scrape.transaction_id.0.into(), torrent_stats: scrape .torrent_stats .iter() .map(|torrent_scrape_statistics| TorrentStats { - seeders: torrent_scrape_statistics.seeders.0, - completed: torrent_scrape_statistics.completed.0, - leechers: torrent_scrape_statistics.leechers.0, + seeders: torrent_scrape_statistics.seeders.0.into(), + completed: torrent_scrape_statistics.completed.0.into(), + leechers: torrent_scrape_statistics.leechers.0.into(), }) .collect::>(), } diff --git a/src/servers/udp/connection_cookie.rs b/src/servers/udp/connection_cookie.rs index 49ea6261b..af3a28702 100644 --- a/src/servers/udp/connection_cookie.rs +++ b/src/servers/udp/connection_cookie.rs @@ -71,6 +71,8 @@ use std::panic::Location; use aquatic_udp_protocol::ConnectionId; use torrust_tracker_clock::time_extent::{Extent, TimeExtent}; +use zerocopy::network_endian::I64; +use zerocopy::AsBytes; use super::error::Error; @@ -83,13 +85,15 @@ pub const COOKIE_LIFETIME: TimeExtent = TimeExtent::from_sec(2, &60); /// Converts a connection ID into a connection cookie. #[must_use] pub fn from_connection_id(connection_id: &ConnectionId) -> Cookie { - connection_id.0.to_le_bytes() + let mut cookie = [0u8; 8]; + connection_id.write_to(&mut cookie); + cookie } /// Converts a connection cookie into a connection ID. #[must_use] pub fn into_connection_id(connection_cookie: &Cookie) -> ConnectionId { - ConnectionId(i64::from_le_bytes(*connection_cookie)) + ConnectionId(I64::new(i64::from_be_bytes(*connection_cookie))) } /// Generates a new connection cookie. diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 122e666a8..876f4c9fe 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -1,19 +1,21 @@ //! Handlers for the UDP server. use std::fmt; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, SocketAddr}; use std::panic::Location; use std::sync::Arc; use std::time::Instant; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, - NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, + AnnounceInterval, AnnounceRequest, AnnounceResponse, AnnounceResponseFixedData, ConnectRequest, ConnectResponse, + ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, + ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; use log::debug; use tokio::net::UdpSocket; use torrust_tracker_located_error::DynError; use torrust_tracker_primitives::info_hash::InfoHash; use uuid::Uuid; +use zerocopy::network_endian::I32; use super::connection_cookie::{check, from_connection_id, into_connection_id, make}; use super::UdpRequest; @@ -41,7 +43,7 @@ pub(crate) async fn handle_packet(udp_request: UdpRequest, tracker: &Arc { - ip_address: ip, - port: Port(peer.peer_addr.port()), + Some(ResponsePeer:: { + ip_address: ip.into(), + port: Port(peer.peer_addr.port().into()), }) } else { None @@ -204,18 +208,20 @@ pub async fn handle_announce( Ok(Response::from(announce_response)) } else { let announce_response = AnnounceResponse { - transaction_id: wrapped_announce_request.announce_request.transaction_id, - announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32), - leechers: NumberOfPeers(i64::from(response.stats.incomplete) as i32), - seeders: NumberOfPeers(i64::from(response.stats.complete) as i32), + fixed: AnnounceResponseFixedData { + transaction_id: wrapped_announce_request.announce_request.transaction_id, + announce_interval: AnnounceInterval(I32::new(i64::from(tracker.get_announce_policy().interval) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(response.stats.incomplete) as i32)), + seeders: NumberOfPeers(I32::new(i64::from(response.stats.complete) as i32)), + }, peers: response .peers .iter() .filter_map(|peer| { if let IpAddr::V6(ip) = peer.peer_addr.ip() { - Some(ResponsePeer:: { - ip_address: ip, - port: Port(peer.peer_addr.port()), + Some(ResponsePeer:: { + ip_address: ip.into(), + port: Port(peer.peer_addr.port().into()), }) } else { None @@ -259,9 +265,9 @@ pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tra #[allow(clippy::cast_possible_truncation)] let scrape_entry = { TorrentScrapeStatistics { - seeders: NumberOfPeers(i64::from(swarm_metadata.complete) as i32), - completed: NumberOfDownloads(i64::from(swarm_metadata.downloaded) as i32), - leechers: NumberOfPeers(i64::from(swarm_metadata.incomplete) as i32), + seeders: NumberOfPeers(I32::new(i64::from(swarm_metadata.complete) as i32)), + completed: NumberOfDownloads(I32::new(i64::from(swarm_metadata.downloaded) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(swarm_metadata.incomplete) as i32)), } }; @@ -445,14 +451,14 @@ mod tests { fn sample_connect_request() -> ConnectRequest { ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), } } #[tokio::test] async fn a_connect_response_should_contain_the_same_transaction_id_as_the_connect_request() { let request = ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), }; let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) @@ -471,7 +477,7 @@ mod tests { #[tokio::test] async fn a_connect_response_should_contain_a_new_connection_id() { let request = ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), }; let response = handle_connect(sample_ipv4_remote_addr(), &request, &public_tracker()) @@ -529,10 +535,11 @@ mod tests { mod announce_request { use std::net::Ipv4Addr; + use std::num::NonZeroU16; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId as AquaticPeerId, PeerKey, Port, - TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectionId, NumberOfBytes, NumberOfPeers, + PeerId as AquaticPeerId, PeerKey, Port, TransactionId, }; use crate::servers::udp::connection_cookie::{into_connection_id, make}; @@ -550,17 +557,18 @@ mod tests { let default_request = AnnounceRequest { connection_id: into_connection_id(&make(&sample_ipv4_remote_addr())), - transaction_id: TransactionId(0i32), + action_placeholder: AnnounceActionPlaceholder::default(), + transaction_id: TransactionId(0i32.into()), info_hash: info_hash_aquatic, peer_id: AquaticPeerId([255u8; 20]), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(client_ip), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port: Port(client_port), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: client_ip.into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers::new(1i32), + port: Port::new(NonZeroU16::new(client_port).expect("a non-zero client port")), }; AnnounceRequestBuilder { request: default_request, @@ -583,12 +591,12 @@ mod tests { } pub fn with_ip_address(mut self, ip_address: Ipv4Addr) -> Self { - self.request.ip_address = Some(ip_address); + self.request.ip_address = ip_address.into(); self } pub fn with_port(mut self, port: u16) -> Self { - self.request.port = Port(port); + self.request.port = Port(port.into()); self } @@ -600,23 +608,23 @@ mod tests { mod using_ipv4 { use std::future; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, NumberOfPeers, PeerId as AquaticPeerId, - Response, ResponsePeer, + AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, Ipv4AddrBytes, Ipv6AddrBytes, NumberOfPeers, + PeerId as AquaticPeerId, Response, ResponsePeer, }; use mockall::predicate::eq; use torrust_tracker_primitives::peer; use crate::core::{self, statistics}; use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; use crate::servers::udp::handlers::tests::{ public_tracker, sample_ipv4_socket_address, tracker_configuration, TorrentPeerBuilder, }; + use crate::servers::udp::handlers::{handle_announce, AnnounceResponseFixedData}; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { @@ -659,14 +667,16 @@ mod tests { let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(120i32), - leechers: NumberOfPeers(0i32), - seeders: NumberOfPeers(1i32), + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval(120i32.into()), + leechers: NumberOfPeers(0i32.into()), + seeders: NumberOfPeers(1i32.into()), + }, peers: empty_peer_vector }) ); @@ -739,7 +749,7 @@ mod tests { let response = announce_a_new_peer_using_ipv4(tracker.clone()).await; // The response should not contain the peer using IPV6 - let peers: Option>> = match response { + let peers: Option>> = match response { Response::AnnounceIpv6(announce_response) => Some(announce_response.peers), _ => None, }; @@ -820,23 +830,23 @@ mod tests { mod using_ipv6 { use std::future; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, NumberOfPeers, PeerId as AquaticPeerId, - Response, ResponsePeer, + AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, Ipv4AddrBytes, Ipv6AddrBytes, NumberOfPeers, + PeerId as AquaticPeerId, Response, ResponsePeer, }; use mockall::predicate::eq; use torrust_tracker_primitives::peer; use crate::core::{self, statistics}; use crate::servers::udp::connection_cookie::{into_connection_id, make}; - use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::announce_request::AnnounceRequestBuilder; use crate::servers::udp::handlers::tests::{ public_tracker, sample_ipv6_remote_addr, tracker_configuration, TorrentPeerBuilder, }; + use crate::servers::udp::handlers::{handle_announce, AnnounceResponseFixedData}; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { @@ -883,14 +893,16 @@ mod tests { let response = handle_announce(remote_addr, &request, &public_tracker()).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(120i32), - leechers: NumberOfPeers(0i32), - seeders: NumberOfPeers(1i32), + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval(120i32.into()), + leechers: NumberOfPeers(0i32.into()), + seeders: NumberOfPeers(1i32.into()), + }, peers: empty_peer_vector }) ); @@ -966,7 +978,7 @@ mod tests { let response = announce_a_new_peer_using_ipv6(tracker.clone()).await; // The response should not contain the peer using IPV4 - let peers: Option>> = match response { + let peers: Option>> = match response { Response::AnnounceIpv4(announce_response) => Some(announce_response.peers), _ => None, }; @@ -1074,9 +1086,9 @@ mod tests { fn zeroed_torrent_statistics() -> TorrentScrapeStatistics { TorrentScrapeStatistics { - seeders: NumberOfPeers(0), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(0.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), } } @@ -1089,7 +1101,7 @@ mod tests { let request = ScrapeRequest { connection_id: into_connection_id(&make(&remote_addr)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), info_hashes, }; @@ -1123,7 +1135,7 @@ mod tests { ScrapeRequest { connection_id: into_connection_id(&make(remote_addr)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId::new(0i32), info_hashes, } } @@ -1159,9 +1171,9 @@ mod tests { let torrent_stats = match_scrape_response(add_a_sample_seeder_and_scrape(tracker.clone()).await); let expected_torrent_stats = vec![TorrentScrapeStatistics { - seeders: NumberOfPeers(1), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(1.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), }]; assert_eq!(torrent_stats.unwrap().torrent_stats, expected_torrent_stats); @@ -1232,9 +1244,9 @@ mod tests { let torrent_stats = match_scrape_response(handle_scrape(remote_addr, &request, &tracker).await.unwrap()).unwrap(); let expected_torrent_stats = vec![TorrentScrapeStatistics { - seeders: NumberOfPeers(1), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(1.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), }]; assert_eq!(torrent_stats.torrent_stats, expected_torrent_stats); @@ -1265,7 +1277,7 @@ mod tests { ScrapeRequest { connection_id: into_connection_id(&make(remote_addr)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), info_hashes, } } diff --git a/src/servers/udp/peer_builder.rs b/src/servers/udp/peer_builder.rs index f7eb935a0..104f42a73 100644 --- a/src/servers/udp/peer_builder.rs +++ b/src/servers/udp/peer_builder.rs @@ -18,13 +18,20 @@ use crate::CurrentClock; /// request. #[must_use] pub fn from_request(announce_wrapper: &AnnounceWrapper, peer_ip: &IpAddr) -> peer::Peer { + let announce_event = match aquatic_udp_protocol::AnnounceEvent::from(announce_wrapper.announce_request.event) { + aquatic_udp_protocol::AnnounceEvent::Started => AnnounceEvent::Started, + aquatic_udp_protocol::AnnounceEvent::Stopped => AnnounceEvent::Stopped, + aquatic_udp_protocol::AnnounceEvent::Completed => AnnounceEvent::Completed, + aquatic_udp_protocol::AnnounceEvent::None => AnnounceEvent::None, + }; + peer::Peer { peer_id: peer::Id(announce_wrapper.announce_request.peer_id.0), - peer_addr: SocketAddr::new(*peer_ip, announce_wrapper.announce_request.port.0), + peer_addr: SocketAddr::new(*peer_ip, announce_wrapper.announce_request.port.0.into()), updated: CurrentClock::now(), - uploaded: NumberOfBytes(announce_wrapper.announce_request.bytes_uploaded.0), - downloaded: NumberOfBytes(announce_wrapper.announce_request.bytes_downloaded.0), - left: NumberOfBytes(announce_wrapper.announce_request.bytes_left.0), - event: AnnounceEvent::from_i32(announce_wrapper.announce_request.event.to_i32()), + uploaded: NumberOfBytes(announce_wrapper.announce_request.bytes_uploaded.0.into()), + downloaded: NumberOfBytes(announce_wrapper.announce_request.bytes_downloaded.0.into()), + left: NumberOfBytes(announce_wrapper.announce_request.bytes_left.0.into()), + event: announce_event, } } diff --git a/src/servers/udp/request.rs b/src/servers/udp/request.rs index e172e03b1..f95fec07a 100644 --- a/src/servers/udp/request.rs +++ b/src/servers/udp/request.rs @@ -21,7 +21,7 @@ impl AnnounceWrapper { #[must_use] pub fn new(announce_request: &AnnounceRequest) -> Self { AnnounceWrapper { - announce_request: announce_request.clone(), + announce_request: *announce_request, info_hash: InfoHash(announce_request.info_hash.0), } } diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 7086b6ab7..dc1bccde3 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -346,7 +346,7 @@ impl Udp { let buffer = vec![0u8; MAX_PACKET_SIZE]; let mut cursor = Cursor::new(buffer); - match response.write(&mut cursor) { + match response.write_bytes(&mut cursor) { Ok(()) => { #[allow(clippy::cast_possible_truncation)] let position = cursor.position() as usize; diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 9af9571bc..81209efb6 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -9,6 +9,7 @@ use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId}; use log::debug; use tokio::net::UdpSocket; use tokio::time; +use zerocopy::network_endian::I32; use crate::shared::bit_torrent::tracker::udp::{source_address, MAX_PACKET_SIZE}; @@ -160,7 +161,7 @@ impl UdpTrackerClient { let request_buffer = vec![0u8; MAX_PACKET_SIZE]; let mut cursor = Cursor::new(request_buffer); - let request_data_result = match request.write(&mut cursor) { + let request_data_result = match request.write_bytes(&mut cursor) { Ok(()) => { #[allow(clippy::cast_possible_truncation)] let position = cursor.position() as usize; @@ -186,7 +187,7 @@ impl UdpTrackerClient { debug!(target: "UDP tracker client", "received {payload_size} bytes. Response {response_buffer:?}"); - let response = Response::from_bytes(&response_buffer[..payload_size], true)?; + let response = Response::parse_bytes(&response_buffer[..payload_size], true)?; Ok(response) } @@ -218,7 +219,7 @@ pub async fn check(binding: &SocketAddr) -> Result { match new_udp_tracker_client_connected(binding.to_string().as_str()).await { Ok(client) => { let connect_request = ConnectRequest { - transaction_id: TransactionId(123), + transaction_id: TransactionId(I32::new(123)), }; // client.send() return usize, but doesn't use here diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index 56e400f84..7abd6092c 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -60,7 +60,7 @@ async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_req Err(err) => panic!("{err}"), }; - let response = Response::from_bytes(&buffer, true).unwrap(); + let response = Response::parse_bytes(&buffer, true).unwrap(); assert!(is_error_response(&response, "bad request")); @@ -85,7 +85,7 @@ mod receiving_a_connection_request { }; let connect_request = ConnectRequest { - transaction_id: TransactionId(123), + transaction_id: TransactionId::new(123), }; match client.send(connect_request.into()).await { @@ -98,7 +98,7 @@ mod receiving_a_connection_request { Err(err) => panic!("{err}"), }; - assert!(is_connect_response(&response, TransactionId(123))); + assert!(is_connect_response(&response, TransactionId::new(123))); env.stop().await; } @@ -108,8 +108,8 @@ mod receiving_an_announce_request { use std::net::Ipv4Addr; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, - TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, + PeerKey, Port, TransactionId, }; use torrust_tracker::shared::bit_torrent::tracker::udp::client::new_udp_tracker_client_connected; use torrust_tracker_test_helpers::configuration; @@ -127,23 +127,24 @@ mod receiving_an_announce_request { Err(err) => panic!("{err}"), }; - let connection_id = send_connection_request(TransactionId(123), &client).await; + let connection_id = send_connection_request(TransactionId::new(123), &client).await; // Send announce request let announce_request = AnnounceRequest { connection_id: ConnectionId(connection_id.0), - transaction_id: TransactionId(123i32), + action_placeholder: AnnounceActionPlaceholder::default(), + transaction_id: TransactionId::new(123i32), info_hash: InfoHash([0u8; 20]), peer_id: PeerId([255u8; 20]), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port: Port(client.udp_client.socket.local_addr().unwrap().port()), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4Addr::new(0, 0, 0, 0).into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers(1i32.into()), + port: Port(client.udp_client.socket.local_addr().unwrap().port().into()), }; match client.send(announce_request.into()).await { @@ -182,7 +183,7 @@ mod receiving_an_scrape_request { Err(err) => panic!("{err}"), }; - let connection_id = send_connection_request(TransactionId(123), &client).await; + let connection_id = send_connection_request(TransactionId::new(123), &client).await; // Send scrape request @@ -192,7 +193,7 @@ mod receiving_an_scrape_request { let scrape_request = ScrapeRequest { connection_id: ConnectionId(connection_id.0), - transaction_id: TransactionId(123i32), + transaction_id: TransactionId::new(123i32), info_hashes, }; From be51d2fa98ed83921be6828c685e1ca4b8a9382e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 11:16:40 +0100 Subject: [PATCH 037/217] chore(deps): bump ringbuf from 0.3.3 to 0.4.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/servers/udp/server.rs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b7f592666..f43e85be1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3094,9 +3094,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +checksum = "2542bc32f4c763f52a2eb375cb0b76c5aa5771f569af74299e84dca51d988a2f" dependencies = [ "crossbeam-utils", ] diff --git a/Cargo.toml b/Cargo.toml index 63735450e..cbfdc7697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" reqwest = { version = "0", features = ["json"] } -ringbuf = "0.3.3" +ringbuf = "0" serde = { version = "1", features = ["derive"] } serde_bencode = "0" serde_bytes = "0" diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index dc1bccde3..70cf8f01d 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -24,7 +24,8 @@ use std::sync::Arc; use aquatic_udp_protocol::Response; use derive_more::Constructor; use log::{debug, error, info, trace}; -use ringbuf::{Rb, StaticRb}; +use ringbuf::traits::{Consumer, Observer, RingBuffer}; +use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::sync::oneshot; use tokio::task::{AbortHandle, JoinHandle}; From 62d4a209b048f120f5969fcd3fb7fb100b87f0a2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 11:44:40 +0100 Subject: [PATCH 038/217] chore(deps): update dependencies ```output cargo update Updating crates.io index Locking 14 packages to latest compatible versions Updating anyhow v1.0.82 -> v1.0.83 Updating cc v1.0.96 -> v1.0.97 Updating getrandom v0.2.14 -> v0.2.15 Updating num-bigint v0.4.4 -> v0.4.5 Updating proc-macro2 v1.0.81 -> v1.0.82 Updating rustls-pki-types v1.5.0 -> v1.6.0 Updating rustversion v1.0.15 -> v1.0.16 Updating ryu v1.0.17 -> v1.0.18 Updating semver v1.0.22 -> v1.0.23 Updating syn v2.0.60 -> v2.0.61 Updating thiserror v1.0.59 -> v1.0.60 Updating thiserror-impl v1.0.59 -> v1.0.60 Updating zerocopy v0.7.33 -> v0.7.34 Updating zerocopy-derive v0.7.33 -> v0.7.34 ``` --- Cargo.lock | 113 ++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f43e85be1..d4282c158 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "aquatic_peer_id" @@ -363,7 +363,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -477,7 +477,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -564,7 +564,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -637,7 +637,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "syn_derive", ] @@ -725,9 +725,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" dependencies = [ "jobserver", "libc", @@ -837,7 +837,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1113,7 +1113,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1124,7 +1124,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1171,7 +1171,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1416,7 +1416,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1428,7 +1428,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1440,7 +1440,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1533,7 +1533,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1584,9 +1584,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -2240,7 +2240,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2291,7 +2291,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "termcolor", "thiserror", ] @@ -2404,11 +2404,10 @@ checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2491,7 +2490,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2604,7 +2603,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2673,7 +2672,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2847,9 +2846,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -3167,7 +3166,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.60", + "syn 2.0.61", "unicode-ident", ] @@ -3283,9 +3282,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "51f344d206c5e1b010eec27349b815a4805f70a778895959d70b74b9b529b30a" [[package]] name = "rustls-webpki" @@ -3299,15 +3298,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3389,9 +3388,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" @@ -3429,7 +3428,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3475,7 +3474,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3526,7 +3525,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3660,9 +3659,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -3678,7 +3677,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3766,22 +3765,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3875,7 +3874,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4182,7 +4181,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4353,7 +4352,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -4387,7 +4386,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4635,9 +4634,9 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "byteorder", "zerocopy-derive", @@ -4645,13 +4644,13 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] From e3143f779c40e590cfbdcfd34ee3d972f94ca1b3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 12:25:48 +0100 Subject: [PATCH 039/217] feat: log aborted UDP requests This will add a warning to the lofs when a UDP request is aborted. --- src/servers/udp/server.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 70cf8f01d..fc2d02a59 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -291,7 +291,14 @@ impl Udp { if !h.is_finished() { // the task is still running, lets yield and give it a chance to flush. tokio::task::yield_now().await; + h.abort(); + + let server_socket_addr = socket.local_addr().expect("Could not get local_addr for socket."); + + tracing::span!( + target: "UDP TRACKER", + tracing::Level::WARN, "request-aborted", server_socket_addr = %server_socket_addr); } } } From 3dee03ea0d6b911e58a51cf5fded5d4a0efc9f6d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 17:48:09 +0100 Subject: [PATCH 040/217] docs: udpate installation docs --- README.md | 4 ++++ src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ea5078b19..8431c00e4 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ The following services are provided by the default configuration: ## Documentation +You can read the [latest documentation][docs] from . + +Some specific sections: + - [Management API (Version 1)][API] - [Tracker (HTTP/TLS)][HTTP] - [Tracker (UDP)][UDP] diff --git a/src/lib.rs b/src/lib.rs index 064f50eb6..22bc133e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,29 +109,47 @@ //! With the default configuration you will need to create the `storage` directory: //! //! ```text -//! storage/ -//! ├── database -//! │   └── data.db -//! └── tls -//! ├── localhost.crt -//! └── localhost.key +//! ./storage/ +//! └── tracker +//! ├── etc +//! ├── lib +//! │   ├── database +//! │   │   └── sqlite3.db +//! │   └── tls +//! └── log //! ``` //! //! The default configuration expects a directory `./storage/tracker/lib/database` to be writable by the tracker process. //! -//! By default the tracker uses `SQLite` and the database file name `data.db`. +//! By default the tracker uses `SQLite` and the database file name `sqlite3.db`. //! //! You only need the `tls` directory in case you are setting up SSL for the HTTP tracker or the tracker API. //! Visit [`HTTP`](crate::servers::http) or [`API`](crate::servers::apis) if you want to know how you can use HTTPS. //! //! ## Install from sources //! +//! First, you need to create a folder to clone the repository. +//! +//! ```text +//! cd /tmp +//! mkdir torrust +//! ``` +//! //! ```text //! git clone https://github.com/torrust/torrust-tracker.git \ //! && cd torrust-tracker \ //! && cargo build --release \ +//! && mkdir -p ./storage/tracker/etc \ //! && mkdir -p ./storage/tracker/lib/database \ -//! && mkdir -p ./storage/tracker/lib/tls +//! && mkdir -p ./storage/tracker/lib/tls \ +//! && mkdir -p ./storage/tracker/log +//! ``` +//! +//! To run the tracker we will have to use the command "cargo run" this will +//! compile and after being compiled it will start running the tracker. +//! +//! ```text +//! cargo run //! ``` //! //! ## Run with docker @@ -141,9 +159,10 @@ //! //! # Configuration //! -//! In order to run the tracker you need to provide the configuration. If you run the tracker without providing the configuration, -//! the tracker will generate the default configuration the first time you run it. It will generate a `tracker.toml` file with -//! in the root directory. +//! In order to run the tracker you need to provide the configuration. If you +//! run the tracker without providing the configuration, the tracker will +//! generate the default configuration the first time you run it. It will +//! generate a `tracker.toml` file with in the root directory. //! //! The default configuration is: //! @@ -187,26 +206,37 @@ //! bind_address = "127.0.0.1:1313" //!``` //! -//! The default configuration includes one disabled UDP server, one disabled HTTP server and the enabled API. +//! The default configuration includes one disabled UDP server, one disabled +//! HTTP server and the enabled API. //! -//! For more information about each service and options you can visit the documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). +//! For more information about each service and options you can visit the +//! documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). //! -//! Alternatively to the `tracker.toml` file you can use one environment variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: +//! Alternatively to the `tracker.toml` file you can use one environment +//! variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: //! //! ```text -//! TORRUST_TRACKER_CONFIG=$(cat tracker.toml) -//! cargo run +//! TORRUST_TRACKER_CONFIG=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker //! ``` //! -//! In the previous example you are just setting the env var with the contents of the `tracker.toml` file. +//! In the previous example you are just setting the env var with the contents +//! of the `tracker.toml` file. +//! +//! The env var contains the same data as the `tracker.toml`. It's particularly +//! useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! +//! > NOTICE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. //! -//! The env var contains the same data as the `tracker.toml`. It's particularly useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! By default, if you don’t specify any `tracker.toml` file, the application +//! will use `./share/default/config/tracker.development.sqlite3.toml`. //! -//! > NOTE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. +//! > IMPORTANT: Every time you change the configuration you need to restart the +//! service. //! //! # Usage //! -//! Running the tracker with the default configuration and enabling the UDP and HTTP trackers will expose the services on these URLs: +//! Running the tracker with the default configuration and enabling the UDP and +//! HTTP trackers will expose the services on these URLs: //! //! - REST API: //! - UDP tracker: From cddc4dee7c8de35b543b8aeefb741f8882ccb27e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 18:22:01 +0100 Subject: [PATCH 041/217] chore(deps): bump rustc-demangle v0.1.23 -> v0.1.24 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4282c158..bd0e36c3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3212,9 +3212,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" From 6f02aeb7e97c1d990df916514b0737f8b5e67f26 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 12:15:41 +0100 Subject: [PATCH 042/217] docs: fix profiling docs - Remove unnecessary file - Fix filename typo --- ... => flamegraph_generated_without_sudo.svg} | 0 docs/profiling.md | 2 +- flamegraph_generated_withput_sudo.svg | 491 ------------------ 3 files changed, 1 insertion(+), 492 deletions(-) rename docs/media/{flamegraph_generated_withput_sudo.svg => flamegraph_generated_without_sudo.svg} (100%) delete mode 100644 flamegraph_generated_withput_sudo.svg diff --git a/docs/media/flamegraph_generated_withput_sudo.svg b/docs/media/flamegraph_generated_without_sudo.svg similarity index 100% rename from docs/media/flamegraph_generated_withput_sudo.svg rename to docs/media/flamegraph_generated_without_sudo.svg diff --git a/docs/profiling.md b/docs/profiling.md index 26e5b786e..406560f3c 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -79,7 +79,7 @@ Loading configuration file: `./share/default/config/tracker.udp.benchmarking.tom And some bars in the graph will have the `unknown` label. -![flamegraph generated without sudo](./media/flamegraph_generated_withput_sudo.svg) +![flamegraph generated without sudo](./media/flamegraph_generated_without_sudo.svg) ## Using valgrind and kcachegrind diff --git a/flamegraph_generated_withput_sudo.svg b/flamegraph_generated_withput_sudo.svg deleted file mode 100644 index 84c00ffe3..000000000 --- a/flamegraph_generated_withput_sudo.svg +++ /dev/null @@ -1,491 +0,0 @@ -Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file From 9e01f7fa750c023507522a43f2ec70fc5fca64a6 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Tue, 7 May 2024 20:01:15 +0200 Subject: [PATCH 043/217] dev: fix udp ring-buffer not looping My previous version would be limited to a single thread, as `push_overwrite` would keep on returning the last element when the ring-buffer was full. Now the ring-buffer is pre-filled and is looped over with a mutating iterator. New handles are progressively swapped-in when the old entries are finished. Note: I think that this implementation can be replaced with a standard vector with the same effect. --- src/servers/udp/server.rs | 77 +++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index fc2d02a59..f7092f377 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -24,7 +24,7 @@ use std::sync::Arc; use aquatic_udp_protocol::Response; use derive_more::Constructor; use log::{debug, error, info, trace}; -use ringbuf::traits::{Consumer, Observer, RingBuffer}; +use ringbuf::traits::{Consumer, Observer, Producer}; use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::sync::oneshot; @@ -202,11 +202,23 @@ impl Launcher { } } -#[derive(Default)] struct ActiveRequests { rb: StaticRb, // the number of requests we handle at the same time. } +impl ActiveRequests { + /// Creates a new [`ActiveRequests`] filled with finished tasks. + async fn new() -> Self { + let mut rb = StaticRb::default(); + + let () = while rb.try_push(tokio::task::spawn_blocking(|| ()).abort_handle()).is_ok() {}; + + task::yield_now().await; + + Self { rb } + } +} + impl std::fmt::Debug for ActiveRequests { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let (left, right) = &self.rb.as_slices(); @@ -280,15 +292,22 @@ impl Udp { let tracker = tracker.clone(); let socket = socket.clone(); - let reqs = &mut ActiveRequests::default(); + let reqs = &mut ActiveRequests::new().await; - // Main Waiting Loop, awaits on async [`receive_request`]. loop { - if let Some(h) = reqs.rb.push_overwrite( - Self::spawn_request_processor(Self::receive_request(socket.clone()).await, tracker.clone(), socket.clone()) - .abort_handle(), - ) { - if !h.is_finished() { + task::yield_now().await; + for h in reqs.rb.iter_mut() { + if h.is_finished() { + std::mem::swap( + h, + &mut Self::spawn_request_processor( + Self::receive_request(socket.clone()).await, + tracker.clone(), + socket.clone(), + ) + .abort_handle(), + ); + } else { // the task is still running, lets yield and give it a chance to flush. tokio::task::yield_now().await; @@ -299,6 +318,9 @@ impl Udp { tracing::span!( target: "UDP TRACKER", tracing::Level::WARN, "request-aborted", server_socket_addr = %server_socket_addr); + + // force-break a single thread, then loop again. + break; } } } @@ -396,13 +418,46 @@ mod tests { use std::sync::Arc; use std::time::Duration; - use tokio::time::sleep; + use ringbuf::traits::{Consumer, Observer, RingBuffer}; use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + use super::ActiveRequests; use crate::bootstrap::app::initialize_with_configuration; use crate::servers::registar::Registar; use crate::servers::udp::server::{Launcher, UdpServer}; + #[tokio::test] + async fn it_should_return_to_the_start_of_the_ring_buffer() { + let mut a_req = ActiveRequests::new().await; + + tokio::time::sleep(Duration::from_millis(10)).await; + + let mut count: usize = 0; + let cap: usize = a_req.rb.capacity().into(); + + // Add a single pending task to check that the ring-buffer is looping correctly. + a_req + .rb + .push_overwrite(tokio::task::spawn(std::future::pending::<()>()).abort_handle()); + + count += 1; + + for _ in 0..2 { + for h in a_req.rb.iter() { + let first = count % cap; + println!("{count},{first},{}", h.is_finished()); + + if first == 0 { + assert!(!h.is_finished()); + } else { + assert!(h.is_finished()); + } + + count += 1; + } + } + } + #[tokio::test] async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); @@ -423,7 +478,7 @@ mod tests { .expect("it should start the server"); let stopped = started.stop().await.expect("it should stop the server"); - sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_secs(1)).await; assert_eq!(stopped.state.launcher.bind_to, bind_to); } From 7da52b1c796b17ebf8208185115eb0f566e61b03 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:24:31 +0100 Subject: [PATCH 044/217] chore(deps): add dependency figment It will replace the custom code for configuration inyection. --- Cargo.lock | 37 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 38 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bd0e36c3a..5972c250a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,15 @@ dependencies = [ "syn 2.0.61", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -696,6 +705,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + [[package]] name = "byteorder" version = "1.5.0" @@ -1334,6 +1349,18 @@ dependencies = [ "log", ] +[[package]] +name = "figment" +version = "0.10.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +dependencies = [ + "atomic", + "serde", + "uncased", + "version_check", +] + [[package]] name = "flate2" version = "1.0.30" @@ -3984,6 +4011,7 @@ dependencies = [ "dashmap", "derive_more", "fern", + "figment", "futures", "hex-literal", "hyper", @@ -4222,6 +4250,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index cbfdc7697..2be3455b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" fern = "0" +figment = "0.10.18" futures = "0" hex-literal = "0" hyper = "1" From f0e07217a52dddfcfd0170370db1177f7b31abe1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:56:44 +0100 Subject: [PATCH 045/217] test: remove broken example in rustdoc --- packages/configuration/src/lib.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index ca873f3cd..660a90701 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -229,6 +229,8 @@ //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` +pub mod v1; + use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; @@ -263,15 +265,6 @@ pub struct Info { impl Info { /// Build Configuration Info /// - /// # Examples - /// - /// ``` - /// use torrust_tracker_configuration::Info; - /// - /// let result = Info::new(env_var_config, env_var_path_config, default_path_config, env_var_api_admin_token); - /// assert_eq!(result, ); - /// ``` - /// /// # Errors /// /// Will return `Err` if unable to obtain a configuration. From 157807ca3144ef69de344f0570e9571c4f0e9492 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:57:54 +0100 Subject: [PATCH 046/217] chore(deps): enable figment features: env, toml, test --- Cargo.lock | 53 +++++++++++++++++++++++++++++++ packages/configuration/Cargo.toml | 1 + 2 files changed, 54 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5972c250a..600914da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,7 +1356,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" dependencies = [ "atomic", + "parking_lot", + "pear", "serde", + "tempfile", + "toml", "uncased", "version_check", ] @@ -1904,6 +1908,12 @@ dependencies = [ "serde", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "instant" version = "0.1.12" @@ -2583,6 +2593,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.60", +] + [[package]] name = "pem" version = "2.0.1" @@ -2880,6 +2913,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "version_check", + "yansi", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -4065,6 +4111,7 @@ version = "3.0.0-alpha.12-develop" dependencies = [ "config", "derive_more", + "figment", "serde", "serde_with", "thiserror", @@ -4669,6 +4716,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 102177816..e5335d416 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -17,6 +17,7 @@ version.workspace = true [dependencies] config = "0" derive_more = "0" +figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } serde_with = "3" thiserror = "1" From 636e779242e15965c8bbdefbb1142c3356dfa4b6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 14:00:20 +0100 Subject: [PATCH 047/217] refactor: create new configuration v1 mod with figment - Clone config strcuctures into a new mod `v1`. - Introduce versioning for configuration API. - Split config sections into submodules. TODO: - Still using root mod types in production. - Not using figment to build config in production. --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 600914da7..1b4c2a4e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2613,7 +2613,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2921,7 +2921,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "version_check", "yansi", ] From e7d344c5f8af51cfff7af4abf71db3a08f039096 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 14:00:20 +0100 Subject: [PATCH 048/217] refactor: create new configuration v1 mod with figment - Clone config strcuctures into a new mod `v1`. - Introduce versioning for configuration API. - Split config sections into submodules. TODO: - Still using root mod types in production. - Not using figment to build config in production. --- .../configuration/src/v1/health_check_api.rs | 13 + packages/configuration/src/v1/http_tracker.rs | 23 + packages/configuration/src/v1/mod.rs | 433 ++++++++++++++++++ packages/configuration/src/v1/tracker_api.rs | 32 ++ packages/configuration/src/v1/udp_tracker.rs | 12 + 5 files changed, 513 insertions(+) create mode 100644 packages/configuration/src/v1/health_check_api.rs create mode 100644 packages/configuration/src/v1/http_tracker.rs create mode 100644 packages/configuration/src/v1/mod.rs create mode 100644 packages/configuration/src/v1/tracker_api.rs create mode 100644 packages/configuration/src/v1/udp_tracker.rs diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs new file mode 100644 index 000000000..f7b15249c --- /dev/null +++ b/packages/configuration/src/v1/health_check_api.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Configuration for the Health Check API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HealthCheckApi { + /// The address the API will bind to. + /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs new file mode 100644 index 000000000..4c88feb9c --- /dev/null +++ b/packages/configuration/src/v1/http_tracker.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +/// Configuration for each HTTP tracker. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpTracker { + /// Weather the HTTP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP tracker will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs new file mode 100644 index 000000000..815d74e40 --- /dev/null +++ b/packages/configuration/src/v1/mod.rs @@ -0,0 +1,433 @@ +//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! This module contains the configuration data structures for the +//! Torrust Tracker, which is a `BitTorrent` tracker server. +//! +//! The configuration is loaded from a [TOML](https://toml.io/en/) file +//! `tracker.toml` in the project root folder or from an environment variable +//! with the same content as the file. +//! +//! Configuration can not only be loaded from a file, but also from an +//! environment variable `TORRUST_TRACKER_CONFIG`. This is useful when running +//! the tracker in a Docker container or environments where you do not have a +//! persistent storage or you cannot inject a configuration file. Refer to +//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more +//! information about how to pass configuration to the tracker. +//! +//! When you run the tracker without providing the configuration via a file or +//! env var, the default configuration is used. +//! +//! # Table of contents +//! +//! - [Sections](#sections) +//! - [Port binding](#port-binding) +//! - [TSL support](#tsl-support) +//! - [Generating self-signed certificates](#generating-self-signed-certificates) +//! - [Default configuration](#default-configuration) +//! +//! ## Sections +//! +//! Each section in the toml structure is mapped to a data structure. For +//! example, the `[http_api]` section (configuration for the tracker HTTP API) +//! is mapped to the [`HttpApi`] structure. +//! +//! > **NOTICE**: some sections are arrays of structures. For example, the +//! > `[[udp_trackers]]` section is an array of [`UdpTracker`] since +//! > you can have multiple running UDP trackers bound to different ports. +//! +//! Please refer to the documentation of each structure for more information +//! about each section. +//! +//! - [`Core configuration`](crate::v1::Configuration) +//! - [`HTTP API configuration`](crate::v1::tracker_api::HttpApi) +//! - [`HTTP Tracker configuration`](crate::v1::http_tracker::HttpTracker) +//! - [`UDP Tracker configuration`](crate::v1::udp_tracker::UdpTracker) +//! - [`Health Check API configuration`](crate::v1::health_check_api::HealthCheckApi) +//! +//! ## Port binding +//! +//! For the API, HTTP and UDP trackers you can bind to a random port by using +//! port `0`. For example, if you want to bind to a random port on all +//! interfaces, use `0.0.0.0:0`. The OS will choose a random free port. +//! +//! ## TSL support +//! +//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to +//! `true` and setting the paths to the certificate and key files. +//! +//! Typically, you will have a `storage` directory like the following: +//! +//! ```text +//! storage/ +//! ├── config.toml +//! └── tracker +//! ├── etc +//! │ └── tracker.toml +//! ├── lib +//! │ ├── database +//! │ │ ├── sqlite3.db +//! │ │ └── sqlite.db +//! │ └── tls +//! │ ├── localhost.crt +//! │ └── localhost.key +//! └── log +//! ``` +//! +//! where the application stores all the persistent data. +//! +//! Alternatively, you could setup a reverse proxy like Nginx or Apache to +//! handle the SSL/TLS part and forward the requests to the tracker. If you do +//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! to `true` in the configuration file. It's out of scope for this +//! documentation to explain in detail how to setup a reverse proxy, but the +//! configuration file should be something like this: +//! +//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! server { +//! listen 80; +//! server_name tracker.torrust.com; +//! +//! return 301 https://$host$request_uri; +//! } +//! +//! server { +//! listen 443; +//! server_name tracker.torrust.com; +//! +//! ssl_certificate CERT_PATH +//! ssl_certificate_key CERT_KEY_PATH; +//! +//! location / { +//! proxy_set_header X-Forwarded-For $remote_addr; +//! proxy_pass http://127.0.0.1:6969; +//! } +//! } +//! ``` +//! +//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! RewriteEngine on +//! RewriteCond %{HTTPS} off +//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] +//! +//! +//! +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! Order allow,deny +//! Allow from all +//! +//! +//! ProxyPreserveHost On +//! ProxyRequests Off +//! AllowEncodedSlashes NoDecode +//! +//! ProxyPass / http://localhost:3000/ +//! ProxyPassReverse / http://localhost:3000/ +//! ProxyPassReverse / http://tracker.torrust.com/ +//! +//! RequestHeader set X-Forwarded-Proto "https" +//! RequestHeader set X-Forwarded-Port "443" +//! +//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log +//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined +//! +//! SSLCertificateFile CERT_PATH +//! SSLCertificateKeyFile CERT_KEY_PATH +//! +//! +//! ``` +//! +//! ## Generating self-signed certificates +//! +//! For testing purposes, you can use self-signed certificates. +//! +//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) +//! for more information. +//! +//! Running the following command will generate a certificate (`localhost.crt`) +//! and key (`localhost.key`) file in your current directory: +//! +//! ```s +//! openssl req -x509 -out localhost.crt -keyout localhost.key \ +//! -newkey rsa:2048 -nodes -sha256 \ +//! -subj '/CN=localhost' -extensions EXT -config <( \ +//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +//! ``` +//! +//! You can then use the generated files in the configuration file: +//! +//! ```s +//! [[http_trackers]] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! +//! [http_api] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! ``` +//! +//! ## Default configuration +//! +//! The default configuration is: +//! +//! ```toml +//! log_level = "info" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/tracker/lib/database/sqlite3.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! on_reverse_proxy = false +//! external_ip = "0.0.0.0" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = false +//! max_peer_timeout = 900 +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = true +//! +//! [[udp_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:6969" +//! +//! [[http_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:7070" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! [health_check_api] +//! bind_address = "127.0.0.1:1313" +//!``` +pub mod health_check_api; +pub mod http_tracker; +pub mod tracker_api; +pub mod udp_tracker; + +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; + +use self::health_check_api::HealthCheckApi; +use self::http_tracker::HttpTracker; +use self::tracker_api::HttpApi; +use self::udp_tracker::UdpTracker; +use crate::AnnouncePolicy; + +/// Core configuration for the tracker. +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct Configuration { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + pub log_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. + pub mode: TrackerMode, + + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + pub db_path: String, + + /// See [`AnnouncePolicy::interval`] + pub announce_interval: u32, + + /// See [`AnnouncePolicy::interval_min`] + pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. + pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. + pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + pub remove_peerless_torrents: bool, + + // Server jobs configuration + /// The list of UDP trackers the tracker is running. Each UDP tracker + /// represents a UDP server that the tracker is running and it has its own + /// configuration. + pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker + /// represents a HTTP server that the tracker is running and it has its own + /// configuration. + pub http_trackers: Vec, + /// The HTTP API configuration. + pub http_api: HttpApi, + /// The Health Check API configuration. + pub health_check_api: HealthCheckApi, +} + +impl Default for Configuration { + fn default() -> Self { + let announce_policy = AnnouncePolicy::default(); + + let mut configuration = Configuration { + log_level: Option::from(String::from("info")), + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, + db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + announce_interval: announce_policy.interval, + min_announce_interval: announce_policy.interval_min, + max_peer_timeout: 900, + on_reverse_proxy: false, + external_ip: Some(String::from("0.0.0.0")), + tracker_usage_statistics: true, + persistent_torrent_completed_stat: false, + inactive_peer_cleanup_interval: 600, + remove_peerless_torrents: true, + udp_trackers: Vec::new(), + http_trackers: Vec::new(), + http_api: HttpApi { + enabled: true, + bind_address: String::from("127.0.0.1:1212"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + }, + health_check_api: HealthCheckApi { + bind_address: String::from("127.0.0.1:1313"), + }, + }; + configuration.udp_trackers.push(UdpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + }); + configuration.http_trackers.push(HttpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + }); + configuration + } +} + +#[cfg(test)] +mod tests { + use figment::providers::{Format, Toml}; + use figment::Figment; + + use crate::v1::Configuration; + + #[test] + fn configuration_should_be_loaded_from_a_toml_config_file() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "Config.toml", + r#" + log_level = "info" + mode = "public" + db_driver = "Sqlite3" + db_path = "./storage/tracker/lib/database/sqlite3.db" + announce_interval = 120 + min_announce_interval = 120 + on_reverse_proxy = false + external_ip = "0.0.0.0" + tracker_usage_statistics = true + persistent_torrent_completed_stat = false + max_peer_timeout = 900 + inactive_peer_cleanup_interval = 600 + remove_peerless_torrents = true + + [[udp_trackers]] + enabled = false + bind_address = "0.0.0.0:6969" + + [[http_trackers]] + enabled = false + bind_address = "0.0.0.0:7070" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api] + enabled = true + bind_address = "127.0.0.1:1212" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api.access_tokens] + admin = "MyAccessToken" + + [health_check_api] + bind_address = "127.0.0.1:1313" + "#, + )?; + + let figment = Figment::new().merge(Toml::file("Config.toml")); + + let config: Configuration = figment.extract()?; + + assert_eq!(config, Configuration::default()); + + Ok(()) + }); + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs new file mode 100644 index 000000000..6cda9b437 --- /dev/null +++ b/packages/configuration/src/v1/tracker_api.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +pub type AccessTokens = HashMap; + +/// Configuration for the HTTP API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpApi { + /// Weather the HTTP API is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP API will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, + /// Access tokens for the HTTP API. The key is a label identifying the + /// token and the value is the token itself. The token is used to + /// authenticate the user. All tokens are valid for all endpoints and have + /// the all permissions. + pub access_tokens: AccessTokens, +} diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs new file mode 100644 index 000000000..b304054c3 --- /dev/null +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct UdpTracker { + /// Weather the UDP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +} From 002fb306087919c874d0d3296d0306a083eb62f6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 12:57:14 +0100 Subject: [PATCH 049/217] refactor: reexport config versioned config types This is part of the migration to Figment in the configuration. This expose new versioned types (version 1). However, those types still used the old Config crate. Replacement by Figment has not been done yet. --- packages/configuration/src/lib.rs | 536 +------------------ packages/configuration/src/v1/mod.rs | 109 +++- packages/configuration/src/v1/tracker_api.rs | 6 + 3 files changed, 123 insertions(+), 528 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 660a90701..666500189 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -3,251 +3,29 @@ //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. //! -//! The configuration is loaded from a [TOML](https://toml.io/en/) file -//! `tracker.toml` in the project root folder or from an environment variable -//! with the same content as the file. -//! -//! When you run the tracker without a configuration file, a new one will be -//! created with the default values, but the tracker immediately exits. You can -//! then edit the configuration file and run the tracker again. -//! -//! Configuration can not only be loaded from a file, but also from environment -//! variable `TORRUST_TRACKER_CONFIG`. This is useful when running the tracker -//! in a Docker container or environments where you do not have a persistent -//! storage or you cannot inject a configuration file. Refer to -//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more -//! information about how to pass configuration to the tracker. -//! -//! # Table of contents -//! -//! - [Sections](#sections) -//! - [Port binding](#port-binding) -//! - [TSL support](#tsl-support) -//! - [Generating self-signed certificates](#generating-self-signed-certificates) -//! - [Default configuration](#default-configuration) -//! -//! ## Sections -//! -//! Each section in the toml structure is mapped to a data structure. For -//! example, the `[http_api]` section (configuration for the tracker HTTP API) -//! is mapped to the [`HttpApi`] structure. -//! -//! > **NOTICE**: some sections are arrays of structures. For example, the -//! > `[[udp_trackers]]` section is an array of [`UdpTracker`] since -//! > you can have multiple running UDP trackers bound to different ports. -//! -//! Please refer to the documentation of each structure for more information -//! about each section. -//! -//! - [`Core configuration`](crate::Configuration) -//! - [`HTTP API configuration`](crate::HttpApi) -//! - [`HTTP Tracker configuration`](crate::HttpTracker) -//! - [`UDP Tracker configuration`](crate::UdpTracker) -//! -//! ## Port binding -//! -//! For the API, HTTP and UDP trackers you can bind to a random port by using -//! port `0`. For example, if you want to bind to a random port on all -//! interfaces, use `0.0.0.0:0`. The OS will choose a random port but the -//! tracker will not print the port it is listening to when it starts. It just -//! says `Starting Torrust HTTP tracker server on: http://0.0.0.0:0`. It shows -//! the port used in the configuration file, and not the port the -//! tracker is actually listening to. This is a planned feature, see issue -//! [186](https://github.com/torrust/torrust-tracker/issues/186) for more -//! information. -//! -//! ## TSL support -//! -//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to -//! `true` and setting the paths to the certificate and key files. -//! -//! Typically, you will have a directory structure like this: -//! -//! ```text -//! storage/ -//! ├── database -//! │ └── data.db -//! └── tls -//! ├── localhost.crt -//! └── localhost.key -//! ``` -//! -//! where you can store all the persistent data. -//! -//! Alternatively, you could setup a reverse proxy like Nginx or Apache to -//! handle the SSL/TLS part and forward the requests to the tracker. If you do -//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) -//! to `true` in the configuration file. It's out of scope for this -//! documentation to explain in detail how to setup a reverse proxy, but the -//! configuration file should be something like this: -//! -//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): -//! -//! ```text -//! # HTTPS only (with SSL - force redirect to HTTPS) -//! -//! server { -//! listen 80; -//! server_name tracker.torrust.com; -//! -//! return 301 https://$host$request_uri; -//! } -//! -//! server { -//! listen 443; -//! server_name tracker.torrust.com; -//! -//! ssl_certificate CERT_PATH -//! ssl_certificate_key CERT_KEY_PATH; -//! -//! location / { -//! proxy_set_header X-Forwarded-For $remote_addr; -//! proxy_pass http://127.0.0.1:6969; -//! } -//! } -//! ``` -//! -//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): -//! -//! ```text -//! # HTTPS only (with SSL - force redirect to HTTPS) -//! -//! -//! ServerAdmin webmaster@tracker.torrust.com -//! ServerName tracker.torrust.com -//! -//! -//! RewriteEngine on -//! RewriteCond %{HTTPS} off -//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] -//! -//! -//! -//! -//! -//! ServerAdmin webmaster@tracker.torrust.com -//! ServerName tracker.torrust.com -//! -//! -//! Order allow,deny -//! Allow from all -//! -//! -//! ProxyPreserveHost On -//! ProxyRequests Off -//! AllowEncodedSlashes NoDecode -//! -//! ProxyPass / http://localhost:3000/ -//! ProxyPassReverse / http://localhost:3000/ -//! ProxyPassReverse / http://tracker.torrust.com/ -//! -//! RequestHeader set X-Forwarded-Proto "https" -//! RequestHeader set X-Forwarded-Port "443" -//! -//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log -//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined -//! -//! SSLCertificateFile CERT_PATH -//! SSLCertificateKeyFile CERT_KEY_PATH -//! -//! -//! ``` -//! -//! ## Generating self-signed certificates -//! -//! For testing purposes, you can use self-signed certificates. -//! -//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) -//! for more information. -//! -//! Running the following command will generate a certificate (`localhost.crt`) -//! and key (`localhost.key`) file in your current directory: -//! -//! ```s -//! openssl req -x509 -out localhost.crt -keyout localhost.key \ -//! -newkey rsa:2048 -nodes -sha256 \ -//! -subj '/CN=localhost' -extensions EXT -config <( \ -//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") -//! ``` -//! -//! You can then use the generated files in the configuration file: -//! -//! ```s -//! [[http_trackers]] -//! enabled = true -//! ... -//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" -//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -//! -//! [http_api] -//! enabled = true -//! ... -//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" -//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -//! ``` -//! -//! ## Default configuration -//! -//! The default configuration is: -//! -//! ```toml -//! announce_interval = 120 -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" -//! external_ip = "0.0.0.0" -//! inactive_peer_cleanup_interval = 600 -//! log_level = "info" -//! max_peer_timeout = 900 -//! min_announce_interval = 120 -//! mode = "public" -//! on_reverse_proxy = false -//! persistent_torrent_completed_stat = false -//! remove_peerless_torrents = true -//! tracker_usage_statistics = true -//! -//! [[udp_trackers]] -//! bind_address = "0.0.0.0:6969" -//! enabled = false -//! -//! [[http_trackers]] -//! bind_address = "0.0.0.0:7070" -//! enabled = false -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api] -//! bind_address = "127.0.0.1:1212" -//! enabled = true -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api.access_tokens] -//! admin = "MyAccessToken" -//! -//! [health_check_api] -//! bind_address = "127.0.0.1:1313" -//!``` +//! The current version for configuration is [`v1`](crate::v1). pub mod v1; use std::collections::HashMap; -use std::net::IpAddr; -use std::str::FromStr; use std::sync::Arc; use std::{env, fs}; -use config::{Config, ConfigError, File, FileFormat}; +use config::ConfigError; use derive_more::Constructor; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; use torrust_tracker_located_error::{DynError, Located, LocatedError}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; +pub type Configuration = v1::Configuration; +pub type UdpTracker = v1::udp_tracker::UdpTracker; +pub type HttpTracker = v1::http_tracker::HttpTracker; +pub type HttpApi = v1::tracker_api::HttpApi; +pub type HealthCheckApi = v1::health_check_api::HealthCheckApi; + +pub type AccessTokens = HashMap; + #[derive(Copy, Clone, Debug, PartialEq, Constructor)] pub struct TrackerPolicy { pub remove_peerless_torrents: bool, @@ -307,84 +85,6 @@ impl Info { } } -/// Configuration for each UDP tracker. -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct UdpTracker { - /// Weather the UDP tracker is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, -} - -/// Configuration for each HTTP tracker. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HttpTracker { - /// Weather the HTTP tracker is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, - /// Weather the HTTP tracker will use SSL or not. - pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, -} - -pub type AccessTokens = HashMap; - -/// Configuration for the HTTP API. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HttpApi { - /// Weather the HTTP API is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, - /// Weather the HTTP API will use SSL or not. - pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, - /// Access tokens for the HTTP API. The key is a label identifying the - /// token and the value is the token itself. The token is used to - /// authenticate the user. All tokens are valid for all endpoints and have - /// the all permissions. - pub access_tokens: AccessTokens, -} - -impl HttpApi { - fn override_admin_token(&mut self, api_admin_token: &str) { - self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); - } -} - -/// Configuration for the Health Check API. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HealthCheckApi { - /// The address the API will bind to. - /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, -} - /// Announce policy #[derive(PartialEq, Eq, Debug, Clone, Copy, Constructor)] pub struct AnnouncePolicy { @@ -424,81 +124,6 @@ impl Default for AnnouncePolicy { } } -/// Core configuration for the tracker. -#[allow(clippy::struct_excessive_bools)] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] -pub struct Configuration { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, - /// Tracker mode. See [`TrackerMode`] for more information. - pub mode: TrackerMode, - - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - pub db_driver: DatabaseDriver, - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - pub db_path: String, - - /// See [`AnnouncePolicy::interval`] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - pub on_reverse_proxy: bool, - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. - /// If enabled, the tracker will collect statistics like the number of - /// connections handled, the number of announce requests handled, etc. - /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more - /// information about the collected metrics. - pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive - /// peers from the torrent peer list. - pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - pub remove_peerless_torrents: bool, - - // Server jobs configuration - /// The list of UDP trackers the tracker is running. Each UDP tracker - /// represents a UDP server that the tracker is running and it has its own - /// configuration. - pub udp_trackers: Vec, - /// The list of HTTP trackers the tracker is running. Each HTTP tracker - /// represents a HTTP server that the tracker is running and it has its own - /// configuration. - pub http_trackers: Vec, - /// The HTTP API configuration. - pub http_api: HttpApi, - /// The Health Check API configuration. - pub health_check_api: HealthCheckApi, -} - /// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { @@ -532,147 +157,6 @@ impl From for Error { } } -impl Default for Configuration { - fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - - let mut configuration = Configuration { - log_level: Option::from(String::from("info")), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(String::from("0.0.0.0")), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, - udp_trackers: Vec::new(), - http_trackers: Vec::new(), - http_api: HttpApi { - enabled: true, - bind_address: String::from("127.0.0.1:1212"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), - }, - health_check_api: HealthCheckApi { - bind_address: String::from("127.0.0.1:1313"), - }, - }; - configuration.udp_trackers.push(UdpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:6969"), - }); - configuration.http_trackers.push(HttpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:7070"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - }); - configuration - } -} - -impl Configuration { - fn override_api_admin_token(&mut self, api_admin_token: &str) { - self.http_api.override_admin_token(api_admin_token); - } - - /// Returns the tracker public IP address id defined in the configuration, - /// and `None` otherwise. - #[must_use] - pub fn get_ext_ip(&self) -> Option { - match &self.external_ip { - None => None, - Some(external_ip) => match IpAddr::from_str(external_ip) { - Ok(external_ip) => Some(external_ip), - Err(_) => None, - }, - } - } - - /// Loads the configuration from the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `path` does not exist or has a bad configuration. - pub fn load_from_file(path: &str) -> Result { - let config_builder = Config::builder(); - - #[allow(unused_assignments)] - let mut config = Config::default(); - - config = config_builder.add_source(File::with_name(path)).build()?; - - let torrust_config: Configuration = config.try_deserialize()?; - - Ok(torrust_config) - } - - /// Saves the default configuration at the given path. - /// - /// # Errors - /// - /// Will return `Err` if `path` is not a valid path or the configuration - /// file cannot be created. - pub fn create_default_configuration_file(path: &str) -> Result { - let config = Configuration::default(); - config.save_to_file(path)?; - Ok(config) - } - - /// Loads the configuration from the `Info` struct. The whole - /// configuration in toml format is included in the `info.tracker_toml` string. - /// - /// Optionally will override the admin api token. - /// - /// # Errors - /// - /// Will return `Err` if the environment variable does not exist or has a bad configuration. - pub fn load(info: &Info) -> Result { - let config_builder = Config::builder() - .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) - .build()?; - let mut config: Configuration = config_builder.try_deserialize()?; - - if let Some(ref token) = info.api_admin_token { - config.override_api_admin_token(token); - }; - - Ok(config) - } - - /// Saves the configuration to the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `filename` does not exist or the user does not have - /// permission to read it. Will also return `Err` if the configuration is - /// not valid or cannot be encoded to TOML. - /// - /// # Panics - /// - /// Will panic if the configuration cannot be written into the file. - pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - fs::write(path, self.to_toml()).expect("Could not write to file!"); - Ok(()) - } - - /// Encodes the configuration to TOML. - fn to_toml(&self) -> String { - toml::to_string(self).expect("Could not encode TOML value") - } -} - #[cfg(test)] mod tests { use crate::Configuration; diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 815d74e40..486d2c300 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -1,4 +1,5 @@ -//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! Version `1` for [Torrust Tracker](https://docs.rs/torrust-tracker) +//! configuration data structures. //! //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. @@ -234,6 +235,11 @@ pub mod http_tracker; pub mod tracker_api; pub mod udp_tracker; +use std::fs; +use std::net::IpAddr; +use std::str::FromStr; + +use config::{Config, File, FileFormat}; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -241,7 +247,7 @@ use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::AnnouncePolicy; +use crate::{AnnouncePolicy, Error, Info}; /// Core configuration for the tracker. #[allow(clippy::struct_excessive_bools)] @@ -368,6 +374,105 @@ impl Default for Configuration { } } +impl Configuration { + fn override_api_admin_token(&mut self, api_admin_token: &str) { + self.http_api.override_admin_token(api_admin_token); + } + + /// Returns the tracker public IP address id defined in the configuration, + /// and `None` otherwise. + #[must_use] + pub fn get_ext_ip(&self) -> Option { + match &self.external_ip { + None => None, + Some(external_ip) => match IpAddr::from_str(external_ip) { + Ok(external_ip) => Some(external_ip), + Err(_) => None, + }, + } + } + + /// Loads the configuration from the configuration file. + /// + /// # Errors + /// + /// Will return `Err` if `path` does not exist or has a bad configuration. + pub fn load_from_file(path: &str) -> Result { + // todo: use Figment + + let config_builder = Config::builder(); + + #[allow(unused_assignments)] + let mut config = Config::default(); + + config = config_builder.add_source(File::with_name(path)).build()?; + + let torrust_config: Configuration = config.try_deserialize()?; + + Ok(torrust_config) + } + + /// Saves the default configuration at the given path. + /// + /// # Errors + /// + /// Will return `Err` if `path` is not a valid path or the configuration + /// file cannot be created. + pub fn create_default_configuration_file(path: &str) -> Result { + // todo: use Figment + + let config = Configuration::default(); + config.save_to_file(path)?; + Ok(config) + } + + /// Loads the configuration from the `Info` struct. The whole + /// configuration in toml format is included in the `info.tracker_toml` string. + /// + /// Optionally will override the admin api token. + /// + /// # Errors + /// + /// Will return `Err` if the environment variable does not exist or has a bad configuration. + pub fn load(info: &Info) -> Result { + // todo: use Figment + + let config_builder = Config::builder() + .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) + .build()?; + let mut config: Configuration = config_builder.try_deserialize()?; + + if let Some(ref token) = info.api_admin_token { + config.override_api_admin_token(token); + }; + + Ok(config) + } + + /// Saves the configuration to the configuration file. + /// + /// # Errors + /// + /// Will return `Err` if `filename` does not exist or the user does not have + /// permission to read it. Will also return `Err` if the configuration is + /// not valid or cannot be encoded to TOML. + /// + /// # Panics + /// + /// Will panic if the configuration cannot be written into the file. + pub fn save_to_file(&self, path: &str) -> Result<(), Error> { + // todo: use Figment + + fs::write(path, self.to_toml()).expect("Could not write to file!"); + Ok(()) + } + + /// Encodes the configuration to TOML. + fn to_toml(&self) -> String { + toml::to_string(self).expect("Could not encode TOML value") + } +} + #[cfg(test)] mod tests { use figment::providers::{Format, Toml}; diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 6cda9b437..51f11a14d 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -30,3 +30,9 @@ pub struct HttpApi { /// the all permissions. pub access_tokens: AccessTokens, } + +impl HttpApi { + pub fn override_admin_token(&mut self, api_admin_token: &str) { + self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); + } +} From 265d89d1e8cfd318919e793bbfeceb5b3556cbcb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 13:54:07 +0100 Subject: [PATCH 050/217] refactor: replace Config by Figment in Configuration implementation This replaces the crate `config` with `figment` in the Configuration implementation. --- packages/configuration/src/lib.rs | 13 +++++++------ packages/configuration/src/v1/mod.rs | 28 ++++++++-------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 666500189..78b62442c 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -10,10 +10,9 @@ use std::collections::HashMap; use std::sync::Arc; use std::{env, fs}; -use config::ConfigError; use derive_more::Constructor; use thiserror::Error; -use torrust_tracker_located_error::{DynError, Located, LocatedError}; +use torrust_tracker_located_error::{DynError, LocatedError}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; @@ -142,17 +141,19 @@ pub enum Error { /// Unable to load the configuration from the configuration file. #[error("Failed processing the configuration: {source}")] - ConfigError { source: LocatedError<'static, ConfigError> }, + ConfigError { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, #[error("The error for errors that can never happen.")] Infallible, } -impl From for Error { +impl From for Error { #[track_caller] - fn from(err: ConfigError) -> Self { + fn from(err: figment::Error) -> Self { Self::ConfigError { - source: Located(err).into(), + source: (Arc::new(err) as DynError).into(), } } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 486d2c300..6c044c462 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,8 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use config::{Config, File, FileFormat}; +use figment::providers::{Format, Toml}; +use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -398,18 +399,11 @@ impl Configuration { /// /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { - // todo: use Figment + let figment = Figment::new().merge(Toml::file(path)); - let config_builder = Config::builder(); + let config: Configuration = figment.extract()?; - #[allow(unused_assignments)] - let mut config = Config::default(); - - config = config_builder.add_source(File::with_name(path)).build()?; - - let torrust_config: Configuration = config.try_deserialize()?; - - Ok(torrust_config) + Ok(config) } /// Saves the default configuration at the given path. @@ -419,8 +413,6 @@ impl Configuration { /// Will return `Err` if `path` is not a valid path or the configuration /// file cannot be created. pub fn create_default_configuration_file(path: &str) -> Result { - // todo: use Figment - let config = Configuration::default(); config.save_to_file(path)?; Ok(config) @@ -435,12 +427,9 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - // todo: use Figment + let figment = Figment::new().merge(Toml::string(&info.tracker_toml)); - let config_builder = Config::builder() - .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) - .build()?; - let mut config: Configuration = config_builder.try_deserialize()?; + let mut config: Configuration = figment.extract()?; if let Some(ref token) = info.api_admin_token { config.override_api_admin_token(token); @@ -461,14 +450,13 @@ impl Configuration { /// /// Will panic if the configuration cannot be written into the file. pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - // todo: use Figment - fs::write(path, self.to_toml()).expect("Could not write to file!"); Ok(()) } /// Encodes the configuration to TOML. fn to_toml(&self) -> String { + // code-review: do we need to use Figment also to serialize into toml? toml::to_string(self).expect("Could not encode TOML value") } } From 5bd94940d7230948fed44c6d0b6cae0c1da9810e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 15:36:27 +0100 Subject: [PATCH 051/217] chore: remove unused config dependenciy It was replaced by `figment`. --- Cargo.lock | 195 +----------------------------- Cargo.toml | 7 +- packages/configuration/Cargo.toml | 1 - 3 files changed, 4 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b4c2a4e7..0bbf0205a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -587,9 +587,6 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -dependencies = [ - "serde", -] [[package]] name = "bitvec" @@ -898,61 +895,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1171,7 +1119,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1199,15 +1147,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "downcast" version = "0.11.0" @@ -2008,17 +1947,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2151,12 +2079,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2548,16 +2470,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list", - "hashbrown 0.13.2", -] - [[package]] name = "parking" version = "2.2.0" @@ -2587,12 +2499,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "pear" version = "0.2.9" @@ -2632,51 +2538,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.61", -] - -[[package]] -name = "pest_meta" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.11.2" @@ -3202,18 +3063,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.5.0", - "serde", - "serde_derive", -] - [[package]] name = "rstest" version = "0.19.0" @@ -3257,16 +3106,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rust_decimal" version = "1.35.0" @@ -3887,15 +3726,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -4052,7 +3882,6 @@ dependencies = [ "axum-server", "chrono", "clap", - "config", "crossbeam-skiplist", "dashmap", "derive_more", @@ -4109,7 +3938,6 @@ dependencies = [ name = "torrust-tracker-configuration" version = "3.0.0-alpha.12-develop" dependencies = [ - "config", "derive_more", "figment", "serde", @@ -4291,12 +4119,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "uncased" version = "0.9.10" @@ -4327,12 +4149,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - [[package]] name = "untrusted" version = "0.9.0" @@ -4707,15 +4523,6 @@ dependencies = [ "tap", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2be3455b8..d7aa9a31c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } -config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" @@ -80,7 +79,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] +ignored = ["crossbeam-skiplist", "dashmap", "figment", "parking_lot", "serde_bytes"] [dev-dependencies] local-ip-address = "0" @@ -94,7 +93,7 @@ members = [ "packages/located-error", "packages/primitives", "packages/test-helpers", - "packages/torrent-repository" + "packages/torrent-repository", ] [profile.dev] @@ -108,5 +107,5 @@ lto = "fat" opt-level = 3 [profile.release-debug] -inherits = "release" debug = true +inherits = "release" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index e5335d416..a033dcea1 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,7 +15,6 @@ rust-version.workspace = true version.workspace = true [dependencies] -config = "0" derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } From 146b77d86f86b62fc50014586ab19a1848edbc1b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 16:38:09 +0100 Subject: [PATCH 052/217] feat: enable overwrite Configuration values using env vars Enable Figment ability to overwrite all config options with env vars. We are currently overwriting only this value: ```toml [http_api.access_tokens] admin = "MyAccessToken" ``` With the env var `TORRUST_TRACKER_API_ADMIN_TOKEN`. The name we gave to the env var does nto follow Figment convention which is `TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN`. We have to keep both options until we remove the old one in the rest of the code. --- packages/configuration/src/v1/mod.rs | 138 ++++++++++++++++++--------- 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 6c044c462..562eb569e 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,7 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use figment::providers::{Format, Toml}; +use figment::providers::{Env, Format, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -400,6 +400,16 @@ impl Configuration { /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { let figment = Figment::new().merge(Toml::file(path)); + //.merge(Env::prefixed("TORRUST_TRACKER_")); + + // code-review: merging values from env vars makes the test + // "configuration_should_be_loaded_from_a_toml_config_file" fail. + // + // It's because this line in a new test: + // + // jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + // + // It seems env vars are shared between tests. let config: Configuration = figment.extract()?; @@ -427,7 +437,9 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::new().merge(Toml::string(&info.tracker_toml)); + let figment = Figment::new() + .merge(Toml::string(&info.tracker_toml)) + .merge(Env::prefixed("TORRUST_TRACKER_")); let mut config: Configuration = figment.extract()?; @@ -463,58 +475,67 @@ impl Configuration { #[cfg(test)] mod tests { - use figment::providers::{Format, Toml}; + use figment::providers::{Env, Format, Toml}; use figment::Figment; use crate::v1::Configuration; + #[cfg(test)] + fn default_config_toml() -> String { + let config = r#"log_level = "info" + mode = "public" + db_driver = "Sqlite3" + db_path = "./storage/tracker/lib/database/sqlite3.db" + announce_interval = 120 + min_announce_interval = 120 + on_reverse_proxy = false + external_ip = "0.0.0.0" + tracker_usage_statistics = true + persistent_torrent_completed_stat = false + max_peer_timeout = 900 + inactive_peer_cleanup_interval = 600 + remove_peerless_torrents = true + + [[udp_trackers]] + enabled = false + bind_address = "0.0.0.0:6969" + + [[http_trackers]] + enabled = false + bind_address = "0.0.0.0:7070" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api] + enabled = true + bind_address = "127.0.0.1:1212" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api.access_tokens] + admin = "MyAccessToken" + + [health_check_api] + bind_address = "127.0.0.1:1313" + "# + .lines() + .map(str::trim_start) + .collect::>() + .join("\n"); + config + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|jail| { - jail.create_file( - "Config.toml", - r#" - log_level = "info" - mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" - announce_interval = 120 - min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" - tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 - inactive_peer_cleanup_interval = 600 - remove_peerless_torrents = true - - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api.access_tokens] - admin = "MyAccessToken" - - [health_check_api] - bind_address = "127.0.0.1:1313" - "#, - )?; - - let figment = Figment::new().merge(Toml::file("Config.toml")); + jail.create_file("Config.toml", &default_config_toml())?; + + // todo: replace with Configuration method + let figment = Figment::new() + .merge(Toml::file("Config.toml")) + .merge(Env::prefixed("TORRUST_TRACKER_")); let config: Configuration = figment.extract()?; @@ -523,4 +544,27 @@ mod tests { Ok(()) }); } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { + figment::Jail::expect_with(|jail| { + jail.create_file("Config.toml", &default_config_toml())?; + + jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + + // todo: replace with Configuration method + let figment = Figment::new() + .merge(Toml::file("Config.toml")) + .merge(Env::prefixed("TORRUST_TRACKER_")); + + let config: Configuration = figment.extract()?; + + assert_eq!( + config.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } } From 632c8baad3ad47934872a40f1f6da5c721325e75 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 17:06:06 +0100 Subject: [PATCH 053/217] refactor: move Configuration unit test to inner mods --- packages/configuration/src/lib.rs | 133 ------------------- packages/configuration/src/v1/mod.rs | 91 +++++++------ packages/configuration/src/v1/tracker_api.rs | 29 ++++ 3 files changed, 80 insertions(+), 173 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 78b62442c..20912990a 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -157,136 +157,3 @@ impl From for Error { } } } - -#[cfg(test)] -mod tests { - use crate::Configuration; - - #[cfg(test)] - fn default_config_toml() -> String { - let config = r#"log_level = "info" - mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" - announce_interval = 120 - min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" - tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 - inactive_peer_cleanup_interval = 600 - remove_peerless_torrents = true - - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api.access_tokens] - admin = "MyAccessToken" - - [health_check_api] - bind_address = "127.0.0.1:1313" - "# - .lines() - .map(str::trim_start) - .collect::>() - .join("\n"); - config - } - - #[test] - fn configuration_should_have_default_values() { - let configuration = Configuration::default(); - - let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); - - assert_eq!(toml, default_config_toml()); - } - - #[test] - fn configuration_should_contain_the_external_ip() { - let configuration = Configuration::default(); - - assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); - } - - #[test] - fn configuration_should_be_saved_in_a_toml_config_file() { - use std::{env, fs}; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::save_to_file - let config_file_path = temp_file; - let path = config_file_path.to_string_lossy().to_string(); - - let default_configuration = Configuration::default(); - - default_configuration - .save_to_file(&path) - .expect("Could not save configuration to file"); - - let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); - - assert_eq!(contents, default_config_toml()); - } - - #[cfg(test)] - fn create_temp_config_file_with_default_config() -> String { - use std::env; - use std::fs::File; - use std::io::Write; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::load_from_file - let config_file_path = temp_file.clone(); - let path = config_file_path.to_string_lossy().to_string(); - - // Write file contents - let mut file = File::create(temp_file).unwrap(); - writeln!(&mut file, "{}", default_config_toml()).unwrap(); - - path - } - - #[test] - fn configuration_should_be_loaded_from_a_toml_config_file() { - let config_file_path = create_temp_config_file_with_default_config(); - - let configuration = Configuration::load_from_file(&config_file_path).expect("Could not load configuration from file"); - - assert_eq!(configuration, Configuration::default()); - } - - #[test] - fn http_api_configuration_should_check_if_it_contains_a_token() { - let configuration = Configuration::default(); - - assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken")); - assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken")); - } -} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 562eb569e..4413ed7c4 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -345,17 +345,7 @@ impl Default for Configuration { remove_peerless_torrents: true, udp_trackers: Vec::new(), http_trackers: Vec::new(), - http_api: HttpApi { - enabled: true, - bind_address: String::from("127.0.0.1:1212"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), - }, + http_api: HttpApi::default(), health_check_api: HealthCheckApi { bind_address: String::from("127.0.0.1:1313"), }, @@ -399,17 +389,9 @@ impl Configuration { /// /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { - let figment = Figment::new().merge(Toml::file(path)); - //.merge(Env::prefixed("TORRUST_TRACKER_")); - - // code-review: merging values from env vars makes the test - // "configuration_should_be_loaded_from_a_toml_config_file" fail. - // - // It's because this line in a new test: - // - // jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - // - // It seems env vars are shared between tests. + let figment = Figment::new() + .merge(Toml::file(path)) + .merge(Env::prefixed("TORRUST_TRACKER_")); let config: Configuration = figment.extract()?; @@ -475,8 +457,6 @@ impl Configuration { #[cfg(test)] mod tests { - use figment::providers::{Env, Format, Toml}; - use figment::Figment; use crate::v1::Configuration; @@ -527,19 +507,55 @@ mod tests { config } + #[test] + fn configuration_should_have_default_values() { + let configuration = Configuration::default(); + + let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); + + assert_eq!(toml, default_config_toml()); + } + + #[test] + fn configuration_should_contain_the_external_ip() { + let configuration = Configuration::default(); + + assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); + } + + #[test] + fn configuration_should_be_saved_in_a_toml_config_file() { + use std::{env, fs}; + + use uuid::Uuid; + + // Build temp config file path + let temp_directory = env::temp_dir(); + let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); + + // Convert to argument type for Configuration::save_to_file + let config_file_path = temp_file; + let path = config_file_path.to_string_lossy().to_string(); + + let default_configuration = Configuration::default(); + + default_configuration + .save_to_file(&path) + .expect("Could not save configuration to file"); + + let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); + + assert_eq!(contents, default_config_toml()); + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|jail| { - jail.create_file("Config.toml", &default_config_toml())?; - - // todo: replace with Configuration method - let figment = Figment::new() - .merge(Toml::file("Config.toml")) - .merge(Env::prefixed("TORRUST_TRACKER_")); + jail.create_file("tracker.toml", &default_config_toml())?; - let config: Configuration = figment.extract()?; + let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); - assert_eq!(config, Configuration::default()); + assert_eq!(configuration, Configuration::default()); Ok(()) }); @@ -548,19 +564,14 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.create_file("Config.toml", &default_config_toml())?; + jail.create_file("tracker.toml", &default_config_toml())?; jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - // todo: replace with Configuration method - let figment = Figment::new() - .merge(Toml::file("Config.toml")) - .merge(Env::prefixed("TORRUST_TRACKER_")); - - let config: Configuration = figment.extract()?; + let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); assert_eq!( - config.http_api.access_tokens.get("admin"), + configuration.http_api.access_tokens.get("admin"), Some("NewToken".to_owned()).as_ref() ); diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 51f11a14d..8749478c8 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -31,8 +31,37 @@ pub struct HttpApi { pub access_tokens: AccessTokens, } +impl Default for HttpApi { + fn default() -> Self { + Self { + enabled: true, + bind_address: String::from("127.0.0.1:1212"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + } + } +} + impl HttpApi { pub fn override_admin_token(&mut self, api_admin_token: &str) { self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); } } + +#[cfg(test)] +mod tests { + use crate::v1::tracker_api::HttpApi; + + #[test] + fn http_api_configuration_should_check_if_it_contains_a_token() { + let configuration = HttpApi::default(); + + assert!(configuration.access_tokens.values().any(|t| t == "MyAccessToken")); + assert!(!configuration.access_tokens.values().any(|t| t == "NonExistingToken")); + } +} From b3a1442ee808f2b50296e455880a0d767b56f599 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 18:01:23 +0100 Subject: [PATCH 054/217] refactor!: remove unused method in Configuration --- packages/configuration/src/v1/mod.rs | 34 +++++++++++----------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 4413ed7c4..dbb6eb7c0 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -383,21 +383,6 @@ impl Configuration { } } - /// Loads the configuration from the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `path` does not exist or has a bad configuration. - pub fn load_from_file(path: &str) -> Result { - let figment = Figment::new() - .merge(Toml::file(path)) - .merge(Env::prefixed("TORRUST_TRACKER_")); - - let config: Configuration = figment.extract()?; - - Ok(config) - } - /// Saves the default configuration at the given path. /// /// # Errors @@ -459,6 +444,7 @@ impl Configuration { mod tests { use crate::v1::Configuration; + use crate::Info; #[cfg(test)] fn default_config_toml() -> String { @@ -550,10 +536,13 @@ mod tests { #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { - figment::Jail::expect_with(|jail| { - jail.create_file("tracker.toml", &default_config_toml())?; + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; - let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!(configuration, Configuration::default()); @@ -564,11 +553,14 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.create_file("tracker.toml", &default_config_toml())?; - jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!( configuration.http_api.access_tokens.get("admin"), From caae725578a253d9c73b85c9d4b8cb97f0ad66b3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 08:15:35 +0100 Subject: [PATCH 055/217] feat: use double underscore to split config env var names For example, the env var `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` would be the config option: ``` [http_api.access_tokens] admin = "MyAccessToken" ``` It uses `__` double underscore becuase dots are not allowed in Bash names. See: https://www.gnu.org/software/bash/manual/bash.html#Definitions ``` name A word consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. Names are used as shell variable and function names. Also referred to as an identifier. ``` --- packages/configuration/src/v1/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index dbb6eb7c0..88b9565bd 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -406,7 +406,7 @@ impl Configuration { pub fn load(info: &Info) -> Result { let figment = Figment::new() .merge(Toml::string(&info.tracker_toml)) - .merge(Env::prefixed("TORRUST_TRACKER_")); + .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); let mut config: Configuration = figment.extract()?; @@ -553,7 +553,7 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); let info = Info { tracker_toml: default_config_toml(), From 69d793986605ff058a44a5ad4cba86f9ab50d360 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:00:31 +0100 Subject: [PATCH 056/217] refactor: implement Default for Configuration sections --- .../configuration/src/v1/health_check_api.rs | 8 ++++++++ packages/configuration/src/v1/http_tracker.rs | 12 ++++++++++++ packages/configuration/src/v1/mod.rs | 17 +++-------------- packages/configuration/src/v1/udp_tracker.rs | 8 ++++++++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index f7b15249c..1c2cd073a 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -11,3 +11,11 @@ pub struct HealthCheckApi { /// system to choose a random port, use port `0`. pub bind_address: String, } + +impl Default for HealthCheckApi { + fn default() -> Self { + Self { + bind_address: String::from("127.0.0.1:1313"), + } + } +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index 4c88feb9c..c2d5928e2 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -21,3 +21,15 @@ pub struct HttpTracker { #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, } + +impl Default for HttpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + } + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 88b9565bd..07d8a7194 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -346,21 +346,10 @@ impl Default for Configuration { udp_trackers: Vec::new(), http_trackers: Vec::new(), http_api: HttpApi::default(), - health_check_api: HealthCheckApi { - bind_address: String::from("127.0.0.1:1313"), - }, + health_check_api: HealthCheckApi::default(), }; - configuration.udp_trackers.push(UdpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:6969"), - }); - configuration.http_trackers.push(HttpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:7070"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - }); + configuration.udp_trackers.push(UdpTracker::default()); + configuration.http_trackers.push(HttpTracker::default()); configuration } } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index b304054c3..254272bdd 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -10,3 +10,11 @@ pub struct UdpTracker { /// system to choose a random port, use port `0`. pub bind_address: String, } +impl Default for UdpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + } + } +} From b0c2f9f435f9a6c8b033fd70f1f0a69049709cd9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:07:36 +0100 Subject: [PATCH 057/217] docs: update env var name in toml config template files --- share/default/config/tracker.container.mysql.toml | 8 ++++---- share/default/config/tracker.container.sqlite3.toml | 8 ++++---- share/default/config/tracker.development.sqlite3.toml | 4 ++++ share/default/config/tracker.e2e.container.sqlite3.toml | 8 ++++---- share/default/config/tracker.udp.benchmarking.toml | 4 ++++ 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index e7714c229..f2db06228 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 4ec055c56..4a3ba03b6 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index 9304a2d51..62e5b478e 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 86ffb3ffd..3738704b5 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 70298e9dc..1e951d8fc 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] From 43942ce2b0fca425558e715d7d556ce9307b0ef5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:26:46 +0100 Subject: [PATCH 058/217] tests: add test for configuration with deprecated env var name Until it's updated in this repor and the Index repo you can overwrite the admin token using the new env var name and the old one (deprecated): - New: `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` - Old (deprecated): `TORRUST_TRACKER_API_ADMIN_TOKEN` THe new one uses exactly the strcuture and atribute names in the toml file, using `__` as the separator for levels. We can remove the test when we remove the deprecated name. --- packages/configuration/src/v1/mod.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 07d8a7194..8d5c123fe 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -540,7 +540,7 @@ mod tests { } #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_env_var() { figment::Jail::expect_with(|jail| { jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); @@ -559,4 +559,23 @@ mod tests { Ok(()) }); } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: Some("NewToken".to_owned()), + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!( + configuration.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } } From 0252f308183e8ea53988907e7553eda8952ebdc7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:47:22 +0100 Subject: [PATCH 059/217] feat: allow users not to provide config option with default values Now, you are able to run the tracler like this: ``` TORRUST_TRACKER_CONFIG="" cargo run ``` Default values will be used for the missing values in the provided configuration. In that case, none of the values have been provided, so it will use default values for all options. --- packages/configuration/src/v1/mod.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8d5c123fe..25aa587b3 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,7 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use figment::providers::{Env, Format, Toml}; +use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -393,7 +393,7 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::new() + let figment = Figment::from(Serialized::defaults(Configuration::default())) .merge(Toml::string(&info.tracker_toml)) .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); @@ -523,6 +523,24 @@ mod tests { assert_eq!(contents, default_config_toml()); } + #[test] + fn configuration_should_use_the_default_values_when_an_empty_configuration_is_provided_by_the_user() { + figment::Jail::expect_with(|_jail| { + let empty_configuration = String::new(); + + let info = Info { + tracker_toml: empty_configuration, + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration, Configuration::default()); + + Ok(()) + }); + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|_jail| { From 384e9f820ea685d6f4d2cc9639b83df478b6b359 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 17:19:30 +0100 Subject: [PATCH 060/217] refactor: [#852] eenrich field types in HealthCheckApi config struct --- packages/configuration/src/v1/health_check_api.rs | 6 ++++-- packages/test-helpers/src/configuration.rs | 4 ++-- src/bootstrap/jobs/health_check_api.rs | 5 +---- tests/servers/health_check_api/environment.rs | 5 +---- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index 1c2cd073a..b8bfd2c1b 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -1,3 +1,5 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -9,13 +11,13 @@ pub struct HealthCheckApi { /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, } impl Default for HealthCheckApi { fn default() -> Self { Self { - bind_address: String::from("127.0.0.1:1313"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 49cfdd390..c1ee95197 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -1,6 +1,6 @@ //! Tracker configuration factories for testing. use std::env; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use torrust_tracker_configuration::Configuration; use torrust_tracker_primitives::TrackerMode; @@ -39,7 +39,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; - config.health_check_api.bind_address = format!("127.0.0.1:{}", &health_check_api_port); + config.health_check_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), health_check_api_port); // Ephemeral socket address for UDP tracker let udp_port = 0u16; diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index eec4d81a8..fdedaa3e9 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -35,10 +35,7 @@ use crate::servers::signals::Halted; /// /// It would panic if unable to send the `ApiServerJobStarted` notice. pub async fn start_job(config: &HealthCheckApi, register: ServiceRegistry) -> JoinHandle<()> { - let bind_addr = config - .bind_address - .parse::() - .expect("it should have a valid health check bind address"); + let bind_addr = config.bind_address; let (tx_start, rx_start) = oneshot::channel::(); let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs index 0856985d5..c200beaeb 100644 --- a/tests/servers/health_check_api/environment.rs +++ b/tests/servers/health_check_api/environment.rs @@ -33,10 +33,7 @@ pub struct Environment { impl Environment { pub fn new(config: &Arc, registar: Registar) -> Self { - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let bind_to = config.bind_address; Self { registar, From 1475eadd25e7fbf208523f72f49188d3f28173c2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 11:12:53 +0100 Subject: [PATCH 061/217] refactor: [#852] eenrich field types in UdpTracker config struct --- packages/configuration/src/v1/udp_tracker.rs | 6 ++++-- packages/test-helpers/src/configuration.rs | 10 +++++----- src/bootstrap/jobs/udp_tracker.rs | 5 +---- src/servers/udp/server.rs | 9 +++------ tests/servers/udp/environment.rs | 5 +---- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index 254272bdd..1f772164e 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -1,3 +1,5 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -8,13 +10,13 @@ pub struct UdpTracker { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, } impl Default for UdpTracker { fn default() -> Self { Self { enabled: false, - bind_address: String::from("0.0.0.0:6969"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c1ee95197..c28cf553e 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -1,6 +1,6 @@ //! Tracker configuration factories for testing. use std::env; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use torrust_tracker_configuration::Configuration; use torrust_tracker_primitives::TrackerMode; @@ -44,7 +44,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for UDP tracker let udp_port = 0u16; config.udp_trackers[0].enabled = true; - config.udp_trackers[0].bind_address = format!("127.0.0.1:{}", &udp_port); + config.udp_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port); // Ephemeral socket address for HTTP tracker let http_port = 0u16; @@ -136,10 +136,10 @@ pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { pub fn ephemeral_ipv6() -> Configuration { let mut cfg = ephemeral(); - let ipv6 = format!("[::]:{}", 0); + let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6); - cfg.http_trackers[0].bind_address.clone_from(&ipv6); + cfg.http_api.bind_address.clone_from(&ipv6.to_string()); + cfg.http_trackers[0].bind_address.clone_from(&ipv6.to_string()); cfg.udp_trackers[0].bind_address = ipv6; cfg diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index e9e4bc642..bb1cdb492 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -27,10 +27,7 @@ use crate::servers::udp::server::{Launcher, UdpServer}; /// It will panic if the task did not finish successfully. #[must_use] pub async fn start_job(config: &UdpTracker, tracker: Arc, form: ServiceRegistrationForm) -> JoinHandle<()> { - let bind_to = config - .bind_address - .parse::() - .expect("it should have a valid udp tracker bind address"); + let bind_to = config.bind_address; let server = UdpServer::new(Launcher::new(bind_to)) .start(tracker, form) diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index f7092f377..b02b9802d 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -463,19 +463,16 @@ mod tests { let cfg = Arc::new(ephemeral_mode_public()); let tracker = initialize_with_configuration(&cfg); let config = &cfg.udp_trackers[0]; - - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - + let bind_to = config.bind_address; let register = &Registar::default(); let stopped = UdpServer::new(Launcher::new(bind_to)); + let started = stopped .start(tracker, register.give_form()) .await .expect("it should start the server"); + let stopped = started.stop().await.expect("it should stop the server"); tokio::time::sleep(Duration::from_secs(1)).await; diff --git a/tests/servers/udp/environment.rs b/tests/servers/udp/environment.rs index 6ced1dbb7..c1fecbdd3 100644 --- a/tests/servers/udp/environment.rs +++ b/tests/servers/udp/environment.rs @@ -31,10 +31,7 @@ impl Environment { let config = Arc::new(configuration.udp_trackers[0].clone()); - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let bind_to = config.bind_address; let server = UdpServer::new(Launcher::new(bind_to)); From fc191f7b6f8eef730665f16739e016ed1b4b5820 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 11:26:04 +0100 Subject: [PATCH 062/217] refactor: [#852] enrich field types in HttpTracker config struct If the next major config version the `TslConfig` shoud always contain valid file paths and the whole field should be optional in the parent strcut `HttpTracker`: ```rust pub struct HttpTracker { pub enabled: bool, pub bind_address: SocketAddr, pub ssl_enabled: bool, #[serde(flatten)] pub tsl_config: Optional, } pub struct TslConfig { pub ssl_cert_path: PathBuf, pub ssl_key_path: PathBuf, } ``` That mean, the user could provide it or not, but if it's provided file paths can't be empty. --- packages/configuration/src/lib.rs | 13 +++++++++++ packages/configuration/src/v1/http_tracker.rs | 22 +++++++++---------- packages/test-helpers/src/configuration.rs | 4 ++-- src/bootstrap/jobs/http_tracker.rs | 15 +++++++------ src/servers/http/server.rs | 17 +++++++------- tests/servers/http/environment.rs | 13 ++++++----- 6 files changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 20912990a..f1e316f10 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -11,6 +11,8 @@ use std::sync::Arc; use std::{env, fs}; use derive_more::Constructor; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; @@ -157,3 +159,14 @@ impl From for Error { } } } + +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct TslConfig { + /// Path to the SSL certificate file. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index c2d5928e2..57b2d83a1 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -1,5 +1,9 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; + +use crate::TslConfig; /// Configuration for each HTTP tracker. #[serde_as] @@ -11,25 +15,21 @@ pub struct HttpTracker { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, /// Weather the HTTP tracker will use SSL or not. pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + /// TSL config. Only used if `ssl_enabled` is true. + #[serde(flatten)] + pub tsl_config: TslConfig, } impl Default for HttpTracker { fn default() -> Self { Self { enabled: false, - bind_address: String::from("0.0.0.0:7070"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070), ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, + tsl_config: TslConfig::default(), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c28cf553e..e6f53f85b 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -49,7 +49,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for HTTP tracker let http_port = 0u16; config.http_trackers[0].enabled = true; - config.http_trackers[0].bind_address = format!("127.0.0.1:{}", &http_port); + config.http_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port); // Ephemeral sqlite database let temp_directory = env::temp_dir(); @@ -139,7 +139,7 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); cfg.http_api.bind_address.clone_from(&ipv6.to_string()); - cfg.http_trackers[0].bind_address.clone_from(&ipv6.to_string()); + cfg.http_trackers[0].bind_address.clone_from(&ipv6); cfg.udp_trackers[0].bind_address = ipv6; cfg diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 0a0638b78..88d643149 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -40,14 +40,15 @@ pub async fn start_job( version: Version, ) -> Option> { if config.enabled { - let socket = config - .bind_address - .parse::() - .expect("it should have a valid http tracker bind address"); + let socket = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let tls = make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + ) + .await + .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); match version { Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index decc734c5..5791708ec 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -234,14 +234,15 @@ mod tests { let tracker = initialize_with_configuration(&cfg); let config = &cfg.http_trackers[0]; - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("tls config failed")); + let bind_to = config.bind_address; + + let tls = make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + ) + .await + .map(|tls| tls.expect("tls config failed")); let register = &Registar::default(); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index f00da293e..e3aa6641e 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -31,13 +31,14 @@ impl Environment { let config = Arc::new(configuration.http_trackers[0].clone()); - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let bind_to = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) - .map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + )) + .map(|tls| tls.expect("tls config failed")); let server = HttpServer::new(Launcher::new(bind_to, tls)); From a2e718bb1d56b5977f53bed0e10b677db4b4e63e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:00:24 +0100 Subject: [PATCH 063/217] chore(deps): add dependency camino We are using `String` to represent a filepath. We are refactoring to enrich types in configuration. Filepath should be represented with `PathBuf` but it allows non UTF-8 chars, so it can't be serialized. Since we need to serialize config options (toml, json) is valid UTF-8 strings, we need a type that represents a valid fileptah but also is a valid UTF-8 strings. That makes impossible to use non-UTF8 fielpath. It seems that's a restriction accepted for a lot of projects including Rust `cargo`. --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + cSpell.json | 1 + packages/configuration/Cargo.toml | 1 + 4 files changed, 14 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0bbf0205a..e54601bcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,6 +720,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + [[package]] name = "cast" version = "0.3.0" @@ -3880,6 +3889,7 @@ dependencies = [ "axum-client-ip", "axum-extra", "axum-server", + "camino", "chrono", "clap", "crossbeam-skiplist", @@ -3938,6 +3948,7 @@ dependencies = [ name = "torrust-tracker-configuration" version = "3.0.0-alpha.12-develop" dependencies = [ + "camino", "derive_more", "figment", "serde", diff --git a/Cargo.toml b/Cargo.toml index d7aa9a31c..60652b160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ axum = { version = "0", features = ["macros"] } axum-client-ip = "0" axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } +camino = { version = "1.1.6", features = ["serde"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" diff --git a/cSpell.json b/cSpell.json index 2473e9c33..bd6c9d489 100644 --- a/cSpell.json +++ b/cSpell.json @@ -25,6 +25,7 @@ "Buildx", "byteorder", "callgrind", + "camino", "canonicalize", "canonicalized", "certbot", diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index a033dcea1..bac2132d5 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,6 +15,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +camino = { version = "1.1.6", features = ["serde"] } derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } From 3997cfa256a944b30b55f2d94ae5dbcf0670a696 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:06:32 +0100 Subject: [PATCH 064/217] refactor: [#852] eenrich field types in TslConfig config struct --- packages/configuration/src/lib.rs | 5 +++-- src/bootstrap/jobs/http_tracker.rs | 4 ++-- src/bootstrap/jobs/mod.rs | 31 +++++++++++++++++++++++++++++- src/servers/http/server.rs | 4 ++-- tests/servers/http/environment.rs | 4 ++-- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index f1e316f10..239803f47 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::{env, fs}; +use camino::Utf8PathBuf; use derive_more::Constructor; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, NoneAsEmptyString}; @@ -165,8 +166,8 @@ impl From for Error { pub struct TslConfig { /// Path to the SSL certificate file. #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, + pub ssl_cert_path: Option, /// Path to the SSL key file. #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + pub ssl_key_path: Option, } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 88d643149..d9e8bdabe 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -18,7 +18,7 @@ use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; -use super::make_rust_tls; +use super::make_rust_tls_from_path_buf; use crate::core; use crate::servers::http::server::{HttpServer, Launcher}; use crate::servers::http::Version; @@ -42,7 +42,7 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls( + let tls = make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 2c12eb40e..dd855f7c6 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -28,7 +28,35 @@ pub async fn make_rust_tls(enabled: bool, cert: &Option, key: &Option, + key: &Option, +) -> Option> { + if !enabled { + info!("TLS not enabled"); + return None; + } + + if let (Some(cert), Some(key)) = (cert, key) { + info!("Using https: cert path: {cert}."); + info!("Using https: key path: {key}."); Some( RustlsConfig::from_pem_file(cert, key) @@ -77,6 +105,7 @@ use std::panic::Location; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; +use camino::Utf8PathBuf; use log::info; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 5791708ec..a6f96634f 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -224,7 +224,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; use crate::bootstrap::app::initialize_with_configuration; - use crate::bootstrap::jobs::make_rust_tls; + use crate::bootstrap::jobs::make_rust_tls_from_path_buf; use crate::servers::http::server::{HttpServer, Launcher}; use crate::servers::registar::Registar; @@ -236,7 +236,7 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls( + let tls = make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index e3aa6641e..e662cea7c 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use futures::executor::block_on; use torrust_tracker::bootstrap::app::initialize_with_configuration; -use torrust_tracker::bootstrap::jobs::make_rust_tls; +use torrust_tracker::bootstrap::jobs::make_rust_tls_from_path_buf; use torrust_tracker::core::Tracker; use torrust_tracker::servers::http::server::{HttpServer, Launcher, Running, Stopped}; use torrust_tracker::servers::registar::Registar; @@ -33,7 +33,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls( + let tls = block_on(make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, From ceb30747cec6372b5244f797ad032c7db8c03d6b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:49:57 +0100 Subject: [PATCH 065/217] refactor: [#852] enrich field types in HttpApi config struct --- packages/configuration/src/v1/tracker_api.rs | 23 ++++--- packages/test-helpers/src/configuration.rs | 4 +- src/bootstrap/jobs/http_tracker.rs | 12 ++-- src/bootstrap/jobs/mod.rs | 68 ++++++++------------ src/bootstrap/jobs/tracker_apis.rs | 7 +- src/servers/apis/server.rs | 7 +- src/servers/http/server.rs | 12 ++-- tests/servers/api/environment.rs | 8 +-- tests/servers/http/environment.rs | 9 +-- 9 files changed, 56 insertions(+), 94 deletions(-) diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 8749478c8..5089c496a 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; + +use crate::TslConfig; pub type AccessTokens = HashMap; @@ -15,19 +18,16 @@ pub struct HttpApi { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, /// Weather the HTTP API will use SSL or not. pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + /// TSL config. Only used if `ssl_enabled` is true. + #[serde(flatten)] + pub tsl_config: TslConfig, /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have - /// the all permissions. + /// all permissions. pub access_tokens: AccessTokens, } @@ -35,10 +35,9 @@ impl Default for HttpApi { fn default() -> Self { Self { enabled: true, - bind_address: String::from("127.0.0.1:1212"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212), ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, + tsl_config: TslConfig::default(), access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] .iter() .cloned() diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index e6f53f85b..08f570dc6 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -35,7 +35,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for API let api_port = 0u16; config.http_api.enabled = true; - config.http_api.bind_address = format!("127.0.0.1:{}", &api_port); + config.http_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port); // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; @@ -138,7 +138,7 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6.to_string()); + cfg.http_api.bind_address.clone_from(&ipv6); cfg.http_trackers[0].bind_address.clone_from(&ipv6); cfg.udp_trackers[0].bind_address = ipv6; diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index d9e8bdabe..d8a976b98 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -18,7 +18,7 @@ use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; -use super::make_rust_tls_from_path_buf; +use super::make_rust_tls; use crate::core; use crate::servers::http::server::{HttpServer, Launcher}; use crate::servers::http::Version; @@ -42,13 +42,9 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - ) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + .await + .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); match version { Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index dd855f7c6..d288989b5 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -20,41 +20,13 @@ pub struct Started { pub address: std::net::SocketAddr, } -pub async fn make_rust_tls(enabled: bool, cert: &Option, key: &Option) -> Option> { +pub async fn make_rust_tls(enabled: bool, tsl_config: &TslConfig) -> Option> { if !enabled { info!("TLS not enabled"); return None; } - if let (Some(cert), Some(key)) = (cert, key) { - info!("Using https: cert path: {cert}."); - info!("Using https: key path: {key}."); - - Some( - RustlsConfig::from_pem_file(cert, key) - .await - .map_err(|err| Error::BadTlsConfig { - source: (Arc::new(err) as DynError).into(), - }), - ) - } else { - Some(Err(Error::MissingTlsConfig { - location: Location::caller(), - })) - } -} - -pub async fn make_rust_tls_from_path_buf( - enabled: bool, - cert: &Option, - key: &Option, -) -> Option> { - if !enabled { - info!("TLS not enabled"); - return None; - } - - if let (Some(cert), Some(key)) = (cert, key) { + if let (Some(cert), Some(key)) = (tsl_config.ssl_cert_path.clone(), tsl_config.ssl_key_path.clone()) { info!("Using https: cert path: {cert}."); info!("Using https: key path: {key}."); @@ -75,15 +47,23 @@ pub async fn make_rust_tls_from_path_buf( #[cfg(test)] mod tests { + use camino::Utf8PathBuf; + use torrust_tracker_configuration::TslConfig; + use super::make_rust_tls; #[tokio::test] async fn it_should_error_on_bad_tls_config() { - let (bad_cert_path, bad_key_path) = (Some("bad cert path".to_string()), Some("bad key path".to_string())); - let err = make_rust_tls(true, &bad_cert_path, &bad_key_path) - .await - .expect("tls_was_enabled") - .expect_err("bad_cert_and_key_files"); + let err = make_rust_tls( + true, + &TslConfig { + ssl_cert_path: Some(Utf8PathBuf::from("bad cert path")), + ssl_key_path: Some(Utf8PathBuf::from("bad key path")), + }, + ) + .await + .expect("tls_was_enabled") + .expect_err("bad_cert_and_key_files"); assert!(err .to_string() @@ -91,11 +71,17 @@ mod tests { } #[tokio::test] - async fn it_should_error_on_missing_tls_config() { - let err = make_rust_tls(true, &None, &None) - .await - .expect("tls_was_enabled") - .expect_err("missing_config"); + async fn it_should_error_on_missing_cert_or_key_paths() { + let err = make_rust_tls( + true, + &TslConfig { + ssl_cert_path: None, + ssl_key_path: None, + }, + ) + .await + .expect("tls_was_enabled") + .expect_err("missing_config"); assert_eq!(err.to_string(), "tls config missing"); } @@ -105,9 +91,9 @@ use std::panic::Location; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use camino::Utf8PathBuf; use log::info; use thiserror::Error; +use torrust_tracker_configuration::TslConfig; use torrust_tracker_located_error::{DynError, LocatedError}; /// Error returned by the Bootstrap Process. diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index ffd7c7407..120c960ef 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -61,12 +61,9 @@ pub async fn start_job( version: Version, ) -> Option> { if config.enabled { - let bind_to = config - .bind_address - .parse::() - .expect("it should have a valid tracker api bind address"); + let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) .await .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index e72890557..9317d6ec0 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -275,12 +275,9 @@ mod tests { let tracker = initialize_with_configuration(&cfg); - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) .await .map(|tls| tls.expect("tls config failed")); diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index a6f96634f..7c8148f22 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -224,7 +224,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; use crate::bootstrap::app::initialize_with_configuration; - use crate::bootstrap::jobs::make_rust_tls_from_path_buf; + use crate::bootstrap::jobs::make_rust_tls; use crate::servers::http::server::{HttpServer, Launcher}; use crate::servers::registar::Registar; @@ -236,13 +236,9 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - ) - .await - .map(|tls| tls.expect("tls config failed")); + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + .await + .map(|tls| tls.expect("tls config failed")); let register = &Registar::default(); diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index dec4ccff2..cacde8af9 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -33,13 +33,9 @@ impl Environment { let config = Arc::new(configuration.http_api.clone()); - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let bind_to = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) - .map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); let server = ApiServer::new(Launcher::new(bind_to, tls)); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index e662cea7c..61837c40f 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use futures::executor::block_on; use torrust_tracker::bootstrap::app::initialize_with_configuration; -use torrust_tracker::bootstrap::jobs::make_rust_tls_from_path_buf; +use torrust_tracker::bootstrap::jobs::make_rust_tls; use torrust_tracker::core::Tracker; use torrust_tracker::servers::http::server::{HttpServer, Launcher, Running, Stopped}; use torrust_tracker::servers::registar::Registar; @@ -33,12 +33,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - )) - .map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); let server = HttpServer::new(Launcher::new(bind_to, tls)); From 7519ecc70052555dcaf9b59f533327d190649cd6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 13:47:47 +0100 Subject: [PATCH 066/217] refactor: [#852] enrich field types in Configuration struct --- packages/configuration/src/lib.rs | 17 +++++++++++++++ packages/configuration/src/v1/mod.rs | 25 +++++++++------------- packages/test-helpers/src/configuration.rs | 6 +++--- src/bootstrap/logging.rs | 14 ++++++++---- src/servers/apis/mod.rs | 4 ++-- src/servers/http/v1/services/announce.rs | 5 +++-- src/servers/udp/handlers.rs | 2 +- 7 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 239803f47..85867816c 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -171,3 +171,20 @@ pub struct TslConfig { #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, } + +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + /// A level lower than all log levels. + Off, + /// Corresponds to the `Error` log level. + Error, + /// Corresponds to the `Warn` log level. + Warn, + /// Corresponds to the `Info` log level. + Info, + /// Corresponds to the `Debug` log level. + Debug, + /// Corresponds to the `Trace` log level. + Trace, +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 25aa587b3..643235c03 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -236,8 +236,7 @@ pub mod tracker_api; pub mod udp_tracker; use std::fs; -use std::net::IpAddr; -use std::str::FromStr; +use std::net::{IpAddr, Ipv4Addr}; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; @@ -248,7 +247,7 @@ use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::{AnnouncePolicy, Error, Info}; +use crate::{AnnouncePolicy, Error, Info, LogLevel}; /// Core configuration for the tracker. #[allow(clippy::struct_excessive_bools)] @@ -256,7 +255,7 @@ use crate::{AnnouncePolicy, Error, Info}; pub struct Configuration { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, + pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. pub mode: TrackerMode, @@ -284,7 +283,7 @@ pub struct Configuration { /// is using a loopback IP address, the tracker assumes that the peer is /// in the same network as the tracker and will use the tracker's IP /// address instead. - pub external_ip: Option, + pub external_ip: Option, /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -330,7 +329,7 @@ impl Default for Configuration { let announce_policy = AnnouncePolicy::default(); let mut configuration = Configuration { - log_level: Option::from(String::from("info")), + log_level: Some(LogLevel::Info), mode: TrackerMode::Public, db_driver: DatabaseDriver::Sqlite3, db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), @@ -338,7 +337,7 @@ impl Default for Configuration { min_announce_interval: announce_policy.interval_min, max_peer_timeout: 900, on_reverse_proxy: false, - external_ip: Some(String::from("0.0.0.0")), + external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), tracker_usage_statistics: true, persistent_torrent_completed_stat: false, inactive_peer_cleanup_interval: 600, @@ -363,13 +362,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - match &self.external_ip { - None => None, - Some(external_ip) => match IpAddr::from_str(external_ip) { - Ok(external_ip) => Some(external_ip), - Err(_) => None, - }, - } + self.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -432,6 +425,8 @@ impl Configuration { #[cfg(test)] mod tests { + use std::net::{IpAddr, Ipv4Addr}; + use crate::v1::Configuration; use crate::Info; @@ -495,7 +490,7 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); + assert_eq!(configuration.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); } #[test] diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 08f570dc6..0c7cc533a 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -2,7 +2,7 @@ use std::env; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use torrust_tracker_configuration::Configuration; +use torrust_tracker_configuration::{Configuration, LogLevel}; use torrust_tracker_primitives::TrackerMode; use crate::random; @@ -28,7 +28,7 @@ pub fn ephemeral() -> Configuration { // For example: a test for the UDP tracker should disable the API and HTTP tracker. let mut config = Configuration { - log_level: Some("off".to_owned()), // Change to `debug` for tests debugging + log_level: Some(LogLevel::Off), // Change to `debug` for tests debugging ..Default::default() }; @@ -125,7 +125,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.external_ip = Some(ip.to_string()); + cfg.external_ip = Some(ip); cfg } diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 97e26919d..b71079b57 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -10,11 +10,10 @@ //! - `Trace` //! //! Refer to the [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) to know how to change log settings. -use std::str::FromStr; use std::sync::Once; use log::{info, LevelFilter}; -use torrust_tracker_configuration::Configuration; +use torrust_tracker_configuration::{Configuration, LogLevel}; static INIT: Once = Once::new(); @@ -31,10 +30,17 @@ pub fn setup(cfg: &Configuration) { }); } -fn config_level_or_default(log_level: &Option) -> LevelFilter { +fn config_level_or_default(log_level: &Option) -> LevelFilter { match log_level { None => log::LevelFilter::Info, - Some(level) => LevelFilter::from_str(level).unwrap(), + Some(level) => match level { + LogLevel::Off => LevelFilter::Off, + LogLevel::Error => LevelFilter::Error, + LogLevel::Warn => LevelFilter::Warn, + LogLevel::Info => LevelFilter::Info, + LogLevel::Debug => LevelFilter::Debug, + LogLevel::Trace => LevelFilter::Trace, + }, } } diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 2d4b3abe1..ef37026fe 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -130,8 +130,8 @@ //! > **NOTICE**: You can generate a self-signed certificate for localhost using //! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). //! That's particularly useful for testing purposes. Once you have the certificate -//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::ssl_cert_path) -//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::ssl_key_path) +//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) +//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) //! options in the configuration file with the paths to the certificate //! (`localhost.crt`) and key (`localhost.key`) files. //! diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index b37081045..5a0ae40e4 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -145,8 +145,9 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.external_ip = - Some(IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)).to_string()); + configuration.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, + ))); Tracker::new(&configuration, Some(stats_event_sender), statistics::Repo::new()).unwrap() } diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 876f4c9fe..d8ca4680c 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -426,7 +426,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.external_ip = Some(external_ip.to_owned()); + self.configuration.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From b545b33c17b3f71591a98dba9aeab84294cd1a62 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 15:59:58 +0100 Subject: [PATCH 067/217] refactor: [#852] extract Core configuration type --- packages/configuration/src/lib.rs | 4 +- packages/configuration/src/v1/core.rs | 88 +++++++++++++++ packages/configuration/src/v1/mod.rs | 101 +++--------------- packages/test-helpers/src/configuration.rs | 23 ++-- src/app.rs | 6 +- src/bootstrap/jobs/torrent_cleanup.rs | 4 +- src/bootstrap/logging.rs | 2 +- src/core/mod.rs | 16 +-- src/core/services/mod.rs | 2 +- .../http/v1/extractors/client_ip_sources.rs | 2 +- src/servers/http/v1/services/announce.rs | 2 +- src/servers/udp/handlers.rs | 2 +- 12 files changed, 134 insertions(+), 118 deletions(-) create mode 100644 packages/configuration/src/v1/core.rs diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 85867816c..fdc021480 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -3,7 +3,7 @@ //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. //! -//! The current version for configuration is [`v1`](crate::v1). +//! The current version for configuration is [`v1`]. pub mod v1; use std::collections::HashMap; @@ -172,7 +172,7 @@ pub struct TslConfig { pub ssl_key_path: Option, } -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] #[serde(rename_all = "lowercase")] pub enum LogLevel { /// A level lower than all log levels. diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs new file mode 100644 index 000000000..ed9074194 --- /dev/null +++ b/packages/configuration/src/v1/core.rs @@ -0,0 +1,88 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; + +use crate::{AnnouncePolicy, LogLevel}; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Core { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + pub log_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. + pub mode: TrackerMode, + + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + pub db_path: String, + + /// See [`AnnouncePolicy::interval`] + pub announce_interval: u32, + + /// See [`AnnouncePolicy::interval_min`] + pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. + pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. + pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + pub remove_peerless_torrents: bool, +} + +impl Default for Core { + fn default() -> Self { + let announce_policy = AnnouncePolicy::default(); + + Self { + log_level: Some(LogLevel::Info), + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, + db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + announce_interval: announce_policy.interval, + min_announce_interval: announce_policy.interval_min, + max_peer_timeout: 900, + on_reverse_proxy: false, + external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), + tracker_usage_statistics: true, + persistent_torrent_completed_stat: false, + inactive_peer_cleanup_interval: 600, + remove_peerless_torrents: true, + } + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 643235c03..d19fedc87 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -78,7 +78,7 @@ //! //! Alternatively, you could setup a reverse proxy like Nginx or Apache to //! handle the SSL/TLS part and forward the requests to the tracker. If you do -//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! that, you should set [`on_reverse_proxy`](crate::v1::core::Core::on_reverse_proxy) //! to `true` in the configuration file. It's out of scope for this //! documentation to explain in detail how to setup a reverse proxy, but the //! configuration file should be something like this: @@ -230,86 +230,32 @@ //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` +pub mod core; pub mod health_check_api; pub mod http_tracker; pub mod tracker_api; pub mod udp_tracker; use std::fs; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +use self::core::Core; use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::{AnnouncePolicy, Error, Info, LogLevel}; +use crate::{Error, Info}; /// Core configuration for the tracker. -#[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, - /// Tracker mode. See [`TrackerMode`] for more information. - pub mode: TrackerMode, - - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - pub db_driver: DatabaseDriver, - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - pub db_path: String, - - /// See [`AnnouncePolicy::interval`] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - pub on_reverse_proxy: bool, - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. - /// If enabled, the tracker will collect statistics like the number of - /// connections handled, the number of announce requests handled, etc. - /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more - /// information about the collected metrics. - pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive - /// peers from the torrent peer list. - pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - pub remove_peerless_torrents: bool, - - // Server jobs configuration + /// Core configuration. + #[serde(flatten)] + pub core: Core, /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. @@ -326,30 +272,13 @@ pub struct Configuration { impl Default for Configuration { fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - - let mut configuration = Configuration { - log_level: Some(LogLevel::Info), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, - udp_trackers: Vec::new(), - http_trackers: Vec::new(), + Self { + core: Core::default(), + udp_trackers: vec![UdpTracker::default()], + http_trackers: vec![HttpTracker::default()], http_api: HttpApi::default(), health_check_api: HealthCheckApi::default(), - }; - configuration.udp_trackers.push(UdpTracker::default()); - configuration.http_trackers.push(HttpTracker::default()); - configuration + } } } @@ -362,7 +291,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - self.external_ip.as_ref().map(|external_ip| *external_ip) + self.core.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -490,7 +419,7 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); + assert_eq!(configuration.core.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); } #[test] diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 0c7cc533a..86ed57b9e 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -27,10 +27,9 @@ pub fn ephemeral() -> Configuration { // todo: disable services that are not needed. // For example: a test for the UDP tracker should disable the API and HTTP tracker. - let mut config = Configuration { - log_level: Some(LogLevel::Off), // Change to `debug` for tests debugging - ..Default::default() - }; + let mut config = Configuration::default(); + + config.core.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; @@ -55,7 +54,7 @@ pub fn ephemeral() -> Configuration { let temp_directory = env::temp_dir(); let random_db_id = random::string(16); let temp_file = temp_directory.join(format!("data_{random_db_id}.db")); - temp_file.to_str().unwrap().clone_into(&mut config.db_path); + temp_file.to_str().unwrap().clone_into(&mut config.core.db_path); config } @@ -65,7 +64,7 @@ pub fn ephemeral() -> Configuration { pub fn ephemeral_with_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.on_reverse_proxy = true; + cfg.core.on_reverse_proxy = true; cfg } @@ -75,7 +74,7 @@ pub fn ephemeral_with_reverse_proxy() -> Configuration { pub fn ephemeral_without_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.on_reverse_proxy = false; + cfg.core.on_reverse_proxy = false; cfg } @@ -85,7 +84,7 @@ pub fn ephemeral_without_reverse_proxy() -> Configuration { pub fn ephemeral_mode_public() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Public; + cfg.core.mode = TrackerMode::Public; cfg } @@ -95,7 +94,7 @@ pub fn ephemeral_mode_public() -> Configuration { pub fn ephemeral_mode_private() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Private; + cfg.core.mode = TrackerMode::Private; cfg } @@ -105,7 +104,7 @@ pub fn ephemeral_mode_private() -> Configuration { pub fn ephemeral_mode_whitelisted() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Listed; + cfg.core.mode = TrackerMode::Listed; cfg } @@ -115,7 +114,7 @@ pub fn ephemeral_mode_whitelisted() -> Configuration { pub fn ephemeral_mode_private_whitelisted() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::PrivateListed; + cfg.core.mode = TrackerMode::PrivateListed; cfg } @@ -125,7 +124,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.external_ip = Some(ip); + cfg.core.external_ip = Some(ip); cfg } diff --git a/src/app.rs b/src/app.rs index 8bdc281a6..fcb01a696 100644 --- a/src/app.rs +++ b/src/app.rs @@ -67,7 +67,7 @@ pub async fn start(config: &Configuration, tracker: Arc) -> Vec) -> Vec 0 { - jobs.push(torrent_cleanup::start_job(config, &tracker)); + if config.core.inactive_peer_cleanup_interval > 0 { + jobs.push(torrent_cleanup::start_job(&config.core, &tracker)); } // Start Health Check API diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs index 300813430..bd3b2e332 100644 --- a/src/bootstrap/jobs/torrent_cleanup.rs +++ b/src/bootstrap/jobs/torrent_cleanup.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use chrono::Utc; use log::info; use tokio::task::JoinHandle; -use torrust_tracker_configuration::Configuration; +use torrust_tracker_configuration::v1::core::Core; use crate::core; @@ -25,7 +25,7 @@ use crate::core; /// /// Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about that option. #[must_use] -pub fn start_job(config: &Configuration, tracker: &Arc) -> JoinHandle<()> { +pub fn start_job(config: &Core, tracker: &Arc) -> JoinHandle<()> { let weak_tracker = std::sync::Arc::downgrade(tracker); let interval = config.inactive_peer_cleanup_interval; diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index b71079b57..5c7e93811 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -19,7 +19,7 @@ static INIT: Once = Once::new(); /// It redirects the log info to the standard output with the log level defined in the configuration pub fn setup(cfg: &Configuration) { - let level = config_level_or_default(&cfg.log_level); + let level = config_level_or_default(&cfg.core.log_level); if level == log::LevelFilter::Off { return; diff --git a/src/core/mod.rs b/src/core/mod.rs index 83813a863..dbaf27e22 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -544,13 +544,13 @@ impl Tracker { stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); + let database = Arc::new(databases::driver::build(&config.core.db_driver, &config.core.db_path)?); - let mode = config.mode; + let mode = config.core.mode; Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), + announce_policy: AnnouncePolicy::new(config.core.announce_interval, config.core.min_announce_interval), mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), @@ -560,11 +560,11 @@ impl Tracker { database, external_ip: config.get_ext_ip(), policy: TrackerPolicy::new( - config.remove_peerless_torrents, - config.max_peer_timeout, - config.persistent_torrent_completed_stat, + config.core.remove_peerless_torrents, + config.core.max_peer_timeout, + config.core.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.on_reverse_proxy, + on_reverse_proxy: config.core.on_reverse_proxy, }) } @@ -1033,7 +1033,7 @@ mod tests { pub fn tracker_persisting_torrents_in_database() -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.persistent_torrent_completed_stat = true; + configuration.core.persistent_torrent_completed_stat = true; tracker_factory(&configuration) } diff --git a/src/core/services/mod.rs b/src/core/services/mod.rs index 76c6a36f6..dec143568 100644 --- a/src/core/services/mod.rs +++ b/src/core/services/mod.rs @@ -21,7 +21,7 @@ use crate::core::Tracker; #[must_use] pub fn tracker_factory(config: &Configuration) -> Tracker { // Initialize statistics - let (stats_event_sender, stats_repository) = statistics::setup::factory(config.tracker_usage_statistics); + let (stats_event_sender, stats_repository) = statistics::setup::factory(config.core.tracker_usage_statistics); // Initialize Torrust tracker match Tracker::new(&Arc::new(config), stats_event_sender, stats_repository) { diff --git a/src/servers/http/v1/extractors/client_ip_sources.rs b/src/servers/http/v1/extractors/client_ip_sources.rs index 18eff26b3..1c6cdc636 100644 --- a/src/servers/http/v1/extractors/client_ip_sources.rs +++ b/src/servers/http/v1/extractors/client_ip_sources.rs @@ -16,7 +16,7 @@ //! the tracker will use the `X-Forwarded-For` header to get the client IP //! address. //! -//! See [`torrust_tracker_configuration::Configuration::on_reverse_proxy`]. +//! See [`torrust_tracker_configuration::Configuration::core.on_reverse_proxy`]. //! //! The tracker can also be configured to run without a reverse proxy. In this //! case, the tracker will use the IP address from the connection info. diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 5a0ae40e4..9529f954c 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -145,7 +145,7 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + configuration.core.external_ip = Some(IpAddr::V6(Ipv6Addr::new( 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index d8ca4680c..d6d7a1065 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -426,7 +426,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); + self.configuration.core.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From ae77ebc50f2286e4c23b3f539759ebe4d5a908d1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 17:05:48 +0100 Subject: [PATCH 068/217] refactor: tracker core service only needs the core config --- src/core/mod.rs | 21 +++++------ src/core/services/mod.rs | 2 +- src/servers/http/v1/services/announce.rs | 22 +++++++++--- src/servers/http/v1/services/scrape.rs | 40 ++++++++++++++++----- src/servers/udp/handlers.rs | 44 ++++++++++++++++++++---- 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/core/mod.rs b/src/core/mod.rs index dbaf27e22..18a6028f7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -445,7 +445,8 @@ use derive_more::Constructor; use log::debug; use tokio::sync::mpsc::error::SendError; use torrust_tracker_clock::clock::Time; -use torrust_tracker_configuration::{AnnouncePolicy, Configuration, TrackerPolicy, TORRENT_PEERS_LIMIT}; +use torrust_tracker_configuration::v1::core::Core; +use torrust_tracker_configuration::{AnnouncePolicy, TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; @@ -540,17 +541,17 @@ impl Tracker { /// /// Will return a `databases::error::Error` if unable to connect to database. The `Tracker` is responsible for the persistence. pub fn new( - config: &Configuration, + config: &Core, stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.core.db_driver, &config.core.db_path)?); + let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); - let mode = config.core.mode; + let mode = config.mode; Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.core.announce_interval, config.core.min_announce_interval), + announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), @@ -558,13 +559,13 @@ impl Tracker { stats_event_sender, stats_repository, database, - external_ip: config.get_ext_ip(), + external_ip: config.external_ip, policy: TrackerPolicy::new( - config.core.remove_peerless_torrents, - config.core.max_peer_timeout, - config.core.persistent_torrent_completed_stat, + config.remove_peerless_torrents, + config.max_peer_timeout, + config.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.core.on_reverse_proxy, + on_reverse_proxy: config.on_reverse_proxy, }) } diff --git a/src/core/services/mod.rs b/src/core/services/mod.rs index dec143568..166f40df4 100644 --- a/src/core/services/mod.rs +++ b/src/core/services/mod.rs @@ -24,7 +24,7 @@ pub fn tracker_factory(config: &Configuration) -> Tracker { let (stats_event_sender, stats_repository) = statistics::setup::factory(config.core.tracker_usage_statistics); // Initialize Torrust tracker - match Tracker::new(&Arc::new(config), stats_event_sender, stats_repository) { + match Tracker::new(&Arc::new(config).core, stats_event_sender, stats_repository) { Ok(tracker) => tracker, Err(error) => { panic!("{}", error) diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 9529f954c..e3bef3973 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -135,8 +135,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let mut peer = sample_peer_using_ipv4(); @@ -149,7 +155,7 @@ mod tests { 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); - Tracker::new(&configuration, Some(stats_event_sender), statistics::Repo::new()).unwrap() + Tracker::new(&configuration.core, Some(stats_event_sender), statistics::Repo::new()).unwrap() } fn peer_with_the_ipv4_loopback_ip() -> peer::Peer { @@ -194,8 +200,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let mut peer = sample_peer_using_ipv6(); diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index 18b57f479..a6a40186f 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -146,8 +146,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -164,8 +170,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); @@ -217,8 +229,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -235,8 +253,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index d6d7a1065..fee00a0bd 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -506,7 +506,12 @@ mod tests { let client_socket_address = sample_ipv4_socket_address(); let torrent_tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_connect(client_socket_address, &sample_connect_request(), &torrent_tracker) .await @@ -524,7 +529,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let torrent_tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_connect(sample_ipv6_remote_addr(), &sample_connect_request(), &torrent_tracker) .await @@ -768,7 +778,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_announce( @@ -997,7 +1012,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); let remote_addr = sample_ipv6_remote_addr(); @@ -1027,7 +1047,7 @@ mod tests { let configuration = Arc::new(TrackerConfigurationBuilder::default().with_external_ip("::126.0.0.1").into()); let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); let tracker = - Arc::new(core::Tracker::new(&configuration, Some(stats_event_sender), stats_repository).unwrap()); + Arc::new(core::Tracker::new(&configuration.core, Some(stats_event_sender), stats_repository).unwrap()); let loopback_ipv4 = Ipv4Addr::new(127, 0, 0, 1); let loopback_ipv6 = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1); @@ -1305,7 +1325,12 @@ mod tests { let remote_addr = sample_ipv4_remote_addr(); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) @@ -1337,7 +1362,12 @@ mod tests { let remote_addr = sample_ipv6_remote_addr(); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_scrape(remote_addr, &sample_scrape_request(&remote_addr), &tracker) From 445bd5361607d8dcf78fcab48450a8d10c6753b3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 13 May 2024 14:10:09 +0100 Subject: [PATCH 069/217] feat: define only non-defaults in toml config templates After implementing the configuration with Figment, it's now possible to omit values if they have a default value. Therefore we don't need to add all options in templates. We only need to add values that are overwriting deffault values. --- packages/configuration/src/lib.rs | 28 ++++++- packages/configuration/src/v1/core.rs | 83 ++++++++++++++++--- .../configuration/src/v1/health_check_api.rs | 9 +- packages/configuration/src/v1/http_tracker.rs | 24 +++++- packages/configuration/src/v1/tracker_api.rs | 35 ++++++-- packages/configuration/src/v1/udp_tracker.rs | 16 +++- .../config/tracker.container.mysql.toml | 31 ------- .../config/tracker.container.sqlite3.toml | 32 ------- .../config/tracker.development.sqlite3.toml | 36 -------- .../config/tracker.e2e.container.sqlite3.toml | 28 ------- .../config/tracker.udp.benchmarking.toml | 32 ------- 11 files changed, 169 insertions(+), 185 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index fdc021480..c393623df 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -120,12 +120,22 @@ pub struct AnnouncePolicy { impl Default for AnnouncePolicy { fn default() -> Self { Self { - interval: 120, - interval_min: 120, + interval: Self::default_interval(), + interval_min: Self::default_interval_min(), } } } +impl AnnouncePolicy { + fn default_interval() -> u32 { + 120 + } + + fn default_interval_min() -> u32 { + 120 + } +} + /// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { @@ -166,12 +176,26 @@ impl From for Error { pub struct TslConfig { /// Path to the SSL certificate file. #[serde_as(as = "NoneAsEmptyString")] + #[serde(default = "TslConfig::default_ssl_cert_path")] pub ssl_cert_path: Option, /// Path to the SSL key file. #[serde_as(as = "NoneAsEmptyString")] + #[serde(default = "TslConfig::default_ssl_key_path")] pub ssl_key_path: Option, } +impl TslConfig { + #[allow(clippy::unnecessary_wraps)] + fn default_ssl_cert_path() -> Option { + Some(Utf8PathBuf::new()) + } + + #[allow(clippy::unnecessary_wraps)] + fn default_ssl_key_path() -> Option { + Some(Utf8PathBuf::new()) + } +} + #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] #[serde(rename_all = "lowercase")] pub enum LogLevel { diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index ed9074194..5d00c67ab 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -10,58 +10,71 @@ use crate::{AnnouncePolicy, LogLevel}; pub struct Core { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. + #[serde(default = "Core::default_log_level")] pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. + #[serde(default = "Core::default_mode")] pub mode: TrackerMode, // Database configuration /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + #[serde(default = "Core::default_db_driver")] pub db_driver: DatabaseDriver, /// Database connection string. The format depends on the database driver. /// For `Sqlite3`, the format is `path/to/database.db`, for example: /// `./storage/tracker/lib/database/sqlite3.db`. /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for /// example: `root:password@localhost:3306/torrust`. + #[serde(default = "Core::default_db_path")] pub db_path: String, /// See [`AnnouncePolicy::interval`] + #[serde(default = "AnnouncePolicy::default_interval")] pub announce_interval: u32, /// See [`AnnouncePolicy::interval_min`] + #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, /// Weather the tracker is behind a reverse proxy or not. /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header /// sent from the proxy will be used to get the client's IP address. + #[serde(default = "Core::default_on_reverse_proxy")] pub on_reverse_proxy: bool, /// The external IP address of the tracker. If the client is using a /// loopback IP address, this IP address will be used instead. If the peer /// is using a loopback IP address, the tracker assumes that the peer is /// in the same network as the tracker and will use the tracker's IP /// address instead. + #[serde(default = "Core::default_external_ip")] pub external_ip: Option, /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more /// information about the collected metrics. + #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, /// If enabled the tracker will persist the number of completed downloads. /// That's how many times a torrent has been downloaded completely. + #[serde(default = "Core::default_persistent_torrent_completed_stat")] pub persistent_torrent_completed_stat: bool, // Cleanup job configuration /// Maximum time in seconds that a peer can be inactive before being /// considered an inactive peer. If a peer is inactive for more than this /// time, it will be removed from the torrent peer list. + #[serde(default = "Core::default_max_peer_timeout")] pub max_peer_timeout: u32, /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. + #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, /// If enabled, the tracker will remove torrents that have no peers. /// The clean up torrent job runs every `inactive_peer_cleanup_interval` /// seconds and it removes inactive peers. Eventually, the peer list of a /// torrent could be empty and the torrent will be removed if this option is /// enabled. + #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, } @@ -70,19 +83,67 @@ impl Default for Core { let announce_policy = AnnouncePolicy::default(); Self { - log_level: Some(LogLevel::Info), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + log_level: Self::default_log_level(), + mode: Self::default_mode(), + db_driver: Self::default_db_driver(), + db_path: Self::default_db_path(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, + max_peer_timeout: Self::default_max_peer_timeout(), + on_reverse_proxy: Self::default_on_reverse_proxy(), + external_ip: Self::default_external_ip(), + tracker_usage_statistics: Self::default_tracker_usage_statistics(), + persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), + inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), + remove_peerless_torrents: Self::default_remove_peerless_torrents(), } } } + +impl Core { + #[allow(clippy::unnecessary_wraps)] + fn default_log_level() -> Option { + Some(LogLevel::Info) + } + + fn default_mode() -> TrackerMode { + TrackerMode::Public + } + + fn default_db_driver() -> DatabaseDriver { + DatabaseDriver::Sqlite3 + } + + fn default_db_path() -> String { + String::from("./storage/tracker/lib/database/sqlite3.db") + } + + fn default_on_reverse_proxy() -> bool { + false + } + + #[allow(clippy::unnecessary_wraps)] + fn default_external_ip() -> Option { + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + } + + fn default_tracker_usage_statistics() -> bool { + true + } + + fn default_persistent_torrent_completed_stat() -> bool { + false + } + + fn default_max_peer_timeout() -> u32 { + 900 + } + + fn default_inactive_peer_cleanup_interval() -> u64 { + 600 + } + + fn default_remove_peerless_torrents() -> bool { + true + } +} diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index b8bfd2c1b..61178fa80 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -11,13 +11,20 @@ pub struct HealthCheckApi { /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HealthCheckApi::default_bind_address")] pub bind_address: SocketAddr, } impl Default for HealthCheckApi { fn default() -> Self { Self { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313), + bind_address: Self::default_bind_address(), } } } + +impl HealthCheckApi { + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313) + } +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index 57b2d83a1..b1fe1437b 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -10,26 +10,44 @@ use crate::TslConfig; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { /// Weather the HTTP tracker is enabled or not. + #[serde(default = "HttpTracker::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HttpTracker::default_bind_address")] pub bind_address: SocketAddr, /// Weather the HTTP tracker will use SSL or not. + #[serde(default = "HttpTracker::default_ssl_enabled")] pub ssl_enabled: bool, /// TSL config. Only used if `ssl_enabled` is true. #[serde(flatten)] + #[serde(default = "TslConfig::default")] pub tsl_config: TslConfig, } impl Default for HttpTracker { fn default() -> Self { Self { - enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070), - ssl_enabled: false, + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), + ssl_enabled: Self::default_ssl_enabled(), tsl_config: TslConfig::default(), } } } + +impl HttpTracker { + fn default_enabled() -> bool { + false + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) + } + + fn default_ssl_enabled() -> bool { + false + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 5089c496a..c2e3e5ee9 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -13,40 +13,61 @@ pub type AccessTokens = HashMap; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpApi { /// Weather the HTTP API is enabled or not. + #[serde(default = "HttpApi::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HttpApi::default_bind_address")] pub bind_address: SocketAddr, /// Weather the HTTP API will use SSL or not. + #[serde(default = "HttpApi::default_ssl_enabled")] pub ssl_enabled: bool, /// TSL config. Only used if `ssl_enabled` is true. #[serde(flatten)] + #[serde(default = "TslConfig::default")] pub tsl_config: TslConfig, /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have /// all permissions. + #[serde(default = "HttpApi::default_access_tokens")] pub access_tokens: AccessTokens, } impl Default for HttpApi { fn default() -> Self { Self { - enabled: true, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212), - ssl_enabled: false, + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), + ssl_enabled: Self::default_ssl_enabled(), tsl_config: TslConfig::default(), - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), + access_tokens: Self::default_access_tokens(), } } } impl HttpApi { + fn default_enabled() -> bool { + true + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) + } + + fn default_ssl_enabled() -> bool { + false + } + + fn default_access_tokens() -> AccessTokens { + [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect() + } + pub fn override_admin_token(&mut self, api_admin_token: &str) { self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index 1f772164e..f8387202e 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -5,18 +5,30 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { /// Weather the UDP tracker is enabled or not. + #[serde(default = "UdpTracker::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "UdpTracker::default_bind_address")] pub bind_address: SocketAddr, } impl Default for UdpTracker { fn default() -> Self { Self { - enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969), + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), } } } + +impl UdpTracker { + fn default_enabled() -> bool { + false + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969) + } +} diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index f2db06228..7678327ab 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,41 +1,10 @@ -announce_interval = 120 db_driver = "MySQL" db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - -[[udp_trackers]] -bind_address = "0.0.0.0:6969" -enabled = false [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 4a3ba03b6..da8259286 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,41 +1,9 @@ -announce_interval = 120 -db_driver = "Sqlite3" db_path = "/var/lib/torrust/tracker/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - -[[udp_trackers]] -bind_address = "0.0.0.0:6969" -enabled = false [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index 62e5b478e..bf6478492 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -1,41 +1,5 @@ -announce_interval = 120 -db_driver = "Sqlite3" -db_path = "./storage/tracker/lib/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = true -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api] -bind_address = "127.0.0.1:1212" enabled = true -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 3738704b5..e7d8fa279 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,41 +1,13 @@ -announce_interval = 120 -db_driver = "Sqlite3" db_path = "/var/lib/torrust/tracker/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true [[http_trackers]] -bind_address = "0.0.0.0:7070" enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "0.0.0.0:1313" diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 1e951d8fc..00f62628b 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -1,41 +1,9 @@ -announce_interval = 120 -db_driver = "Sqlite3" -db_path = "./storage/tracker/lib/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 log_level = "error" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false remove_peerless_torrents = false tracker_usage_statistics = false [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true -[[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - [http_api] -bind_address = "127.0.0.1:1212" enabled = false -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" From cf1bfb11ad8a2d92d9e47b065ab2580e596c23ce Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 07:35:38 +0100 Subject: [PATCH 070/217] chore(deps): update dependencies ```ouput Updating crates.io index Locking 13 packages to latest compatible versions Removing allocator-api2 v0.2.18 Updating async-channel v2.2.1 -> v2.3.0 Updating async-compression v0.4.9 -> v0.4.10 Updating brotli v5.0.0 -> v6.0.0 Updating bytemuck v1.15.0 -> v1.16.0 Updating errno v0.3.8 -> v0.3.9 Updating hashlink v0.9.0 -> v0.9.1 Updating piper v0.2.1 -> v0.2.2 Updating rustls-pki-types v1.6.0 -> v1.7.0 Updating serde v1.0.200 -> v1.0.201 Updating serde_derive v1.0.200 -> v1.0.201 Updating serde_json v1.0.116 -> v1.0.117 Updating syn v2.0.61 -> v2.0.63 Updating waker-fn v1.1.1 -> v1.2.0 ``` --- Cargo.lock | 121 +++++++++++++++++++++++++---------------------------- 1 file changed, 57 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e54601bcf..446477aac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,12 +64,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -207,9 +201,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" +checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a" dependencies = [ "concurrent-queue", "event-listener 5.3.0", @@ -220,9 +214,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "brotli", "flate2", @@ -253,7 +247,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.0", "async-executor", "async-io 2.3.2", "async-lock 3.3.0", @@ -363,7 +357,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -486,7 +480,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -573,7 +567,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -615,7 +609,7 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ - "async-channel 2.2.1", + "async-channel 2.3.0", "async-lock 3.3.0", "async-task", "futures-io", @@ -643,15 +637,15 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "syn_derive", ] [[package]] name = "brotli" -version = "5.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -704,9 +698,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" [[package]] name = "byteorder" @@ -858,7 +852,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1085,7 +1079,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1096,7 +1090,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1143,7 +1137,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1195,9 +1189,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1395,7 +1389,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1407,7 +1401,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1419,7 +1413,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1512,7 +1506,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -1650,14 +1644,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", - "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ "hashbrown 0.14.5", ] @@ -2208,7 +2201,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -2259,7 +2252,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "termcolor", "thiserror", ] @@ -2458,7 +2451,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -2528,7 +2521,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -2602,7 +2595,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -2619,9 +2612,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -2791,7 +2784,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "version_check", "yansi", ] @@ -3097,7 +3090,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.61", + "syn 2.0.63", "unicode-ident", ] @@ -3203,9 +3196,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f344d206c5e1b010eec27349b815a4805f70a778895959d70b74b9b529b30a" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -3315,9 +3308,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] @@ -3343,13 +3336,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3367,9 +3360,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "indexmap 2.2.6", "itoa", @@ -3395,7 +3388,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3446,7 +3439,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3580,9 +3573,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.61" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", @@ -3598,7 +3591,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3701,7 +3694,7 @@ checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -3786,7 +3779,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4095,7 +4088,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] @@ -4213,9 +4206,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -4263,7 +4256,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "wasm-bindgen-shared", ] @@ -4297,7 +4290,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4558,7 +4551,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.61", + "syn 2.0.63", ] [[package]] From da6a21eac969c96005f2b5b4deb0b1ea91926951 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 10:22:56 +0100 Subject: [PATCH 071/217] refactor: [#855] show toml file location in Figment errors Before this commit we loaded configuration in Figment always using a Toml string even if the configuration cames from a toml file. WHen there is an error Figment does not show the file location and that's one of the main advantages of using Figment. All errors point to the primary source of the configuration option. This commit fixes that problem leting Figment to load the configuration from the file when the user provides a file. Sample error: ``` Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... thread 'main' panicked at src/bootstrap/config.rs:45:32: called `Result::unwrap()` on an `Err` value: ConfigError { source: LocatedError { source: Error { tag: Tag(Default, 2), profile: Some(Profile(Uncased { string: "default" })), metadata: Some(Metadata { name: "TOML file", source: Some(File("/home/developer/torrust/torrust-tracker/./share/default/config/tracker.development.sqlite3.toml")), provide_location: Some(Location { file: "packages/configuration/src/v1/mod.rs", line: 330, col: 18 }), interpolater: }), path: [], kind: Message("TOML parse error at line 2, column 15\n |\n2 | enabled = truee\n | ^\nexpected newline, `#`\n"), prev: None }, location: Location { file: "packages/configuration/src/v1/mod.rs", line: 334, col: 41 } } } note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` Notice how the file path is included is the error: `/home/developer/torrust/torrust-tracker/./share/default/config/tracker.development.sqlite3.toml` --- packages/configuration/src/lib.rs | 47 ++++++++---------- packages/configuration/src/v1/mod.rs | 71 +++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index c393623df..9a00e6bbc 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -7,8 +7,8 @@ pub mod v1; use std::collections::HashMap; +use std::env; use std::sync::Arc; -use std::{env, fs}; use camino::Utf8PathBuf; use derive_more::Constructor; @@ -38,7 +38,8 @@ pub struct TrackerPolicy { /// Information required for loading config #[derive(Debug, Default, Clone)] pub struct Info { - tracker_toml: String, + config_toml: Option, + config_toml_path: String, api_admin_token: Option, } @@ -51,38 +52,30 @@ impl Info { /// #[allow(clippy::needless_pass_by_value)] pub fn new( - env_var_config: String, - env_var_path_config: String, - default_path_config: String, + env_var_config_toml: String, + env_var_config_toml_path: String, + default_config_toml_path: String, env_var_api_admin_token: String, ) -> Result { - let tracker_toml = if let Ok(tracker_toml) = env::var(&env_var_config) { - println!("Loading configuration from env var {env_var_config} ..."); + let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { + println!("Loading configuration from environment variable {config_toml} ..."); + Some(config_toml) + } else { + None + }; - tracker_toml + let config_toml_path = if let Ok(config_toml_path) = env::var(env_var_config_toml_path) { + println!("Loading configuration from file: `{config_toml_path}` ..."); + config_toml_path } else { - let config_path = if let Ok(config_path) = env::var(env_var_path_config) { - println!("Loading configuration file: `{config_path}` ..."); - - config_path - } else { - println!("Loading default configuration file: `{default_path_config}` ..."); - - default_path_config - }; - - fs::read_to_string(config_path) - .map_err(|e| Error::UnableToLoadFromConfigFile { - source: (Arc::new(e) as DynError).into(), - })? - .parse() - .map_err(|_e: std::convert::Infallible| Error::Infallible)? + println!("Loading configuration from default configuration file: `{default_config_toml_path}` ..."); + default_config_toml_path }; - let api_admin_token = env::var(env_var_api_admin_token).ok(); Ok(Self { - tracker_toml, - api_admin_token, + config_toml, + config_toml_path, + api_admin_token: env::var(env_var_api_admin_token).ok(), }) } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d19fedc87..8e15d65ca 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -250,6 +250,11 @@ use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; use crate::{Error, Info}; +/// Prefix for env vars that overwrite configuration options. +const CONFIG_OVERRIDE_PREFIX: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_"; +/// Path separator in env var names for nested values in configuration. +const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; + /// Core configuration for the tracker. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { @@ -315,9 +320,16 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::from(Serialized::defaults(Configuration::default())) - .merge(Toml::string(&info.tracker_toml)) - .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); + let figment = if let Some(config_toml) = &info.config_toml { + // Config in env var has priority over config file path + Figment::from(Serialized::defaults(Configuration::default())) + .merge(Toml::string(config_toml)) + .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) + } else { + Figment::from(Serialized::defaults(Configuration::default())) + .merge(Toml::file(&info.config_toml_path)) + .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) + }; let mut config: Configuration = figment.extract()?; @@ -449,11 +461,14 @@ mod tests { #[test] fn configuration_should_use_the_default_values_when_an_empty_configuration_is_provided_by_the_user() { - figment::Jail::expect_with(|_jail| { + figment::Jail::expect_with(|jail| { + jail.create_file("tracker.toml", "")?; + let empty_configuration = String::new(); let info = Info { - tracker_toml: empty_configuration, + config_toml: Some(empty_configuration), + config_toml_path: "tracker.toml".to_string(), api_admin_token: None, }; @@ -466,28 +481,59 @@ mod tests { } #[test] - fn configuration_should_be_loaded_from_a_toml_config_file() { + fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { + let config_toml = r#" + db_path = "OVERWRITTEN DEFAULT DB PATH" + "# + .to_string(); + let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(config_toml), + config_toml_path: String::new(), api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration, Configuration::default()); + assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + + Ok(()) + }); + } + + #[test] + fn default_configuration_could_be_overwritten_from_a_toml_config_file() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tracker.toml", + r#" + db_path = "OVERWRITTEN DEFAULT DB PATH" + "#, + )?; + + let info = Info { + config_toml: None, + config_toml_path: "tracker.toml".to_string(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); } #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_env_var() { + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_an_env_var() { figment::Jail::expect_with(|jail| { - jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); + jail.set_env("TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(default_config_toml()), + config_toml_path: String::new(), api_admin_token: None, }; @@ -506,7 +552,8 @@ mod tests { fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { figment::Jail::expect_with(|_jail| { let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(default_config_toml()), + config_toml_path: String::new(), api_admin_token: Some("NewToken".to_owned()), }; From 4de5e7d32efd1277138ef1b92602204cdf5f8375 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 10:39:20 +0100 Subject: [PATCH 072/217] refactor: move config env vars to configuration package --- packages/configuration/src/lib.rs | 24 ++++++++++++++++++------ src/bootstrap/config.rs | 22 ++-------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 9a00e6bbc..588feb87e 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -20,6 +20,19 @@ use torrust_tracker_located_error::{DynError, LocatedError}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; +// Environment variables + +/// The whole `tracker.toml` file content. It has priority over the config file. +/// Even if the file is not on the default path. +const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; + +/// The `tracker.toml` file location. +pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; + +/// Env var to overwrite API admin token. +/// Deprecated: use `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. +const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; + pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; pub type HttpTracker = v1::http_tracker::HttpTracker; @@ -51,12 +64,11 @@ impl Info { /// Will return `Err` if unable to obtain a configuration. /// #[allow(clippy::needless_pass_by_value)] - pub fn new( - env_var_config_toml: String, - env_var_config_toml_path: String, - default_config_toml_path: String, - env_var_api_admin_token: String, - ) -> Result { + pub fn new(default_config_toml_path: String) -> Result { + let env_var_config_toml = ENV_VAR_CONFIG.to_string(); + let env_var_config_toml_path = ENV_VAR_PATH_CONFIG.to_string(); + let env_var_api_admin_token = ENV_VAR_API_ADMIN_TOKEN.to_string(); + let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); Some(config_toml) diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs index 858fd59fc..03dfd9a2f 100644 --- a/src/bootstrap/config.rs +++ b/src/bootstrap/config.rs @@ -4,17 +4,6 @@ use torrust_tracker_configuration::{Configuration, Info}; -// Environment variables - -/// The whole `tracker.toml` file content. It has priority over the config file. -/// Even if the file is not on the default path. -const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; -const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; - -/// The `tracker.toml` file location. -pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; - -// Default values pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.development.sqlite3.toml"; /// It loads the application configuration from the environment. @@ -34,15 +23,8 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG`. #[must_use] pub fn initialize_configuration() -> Configuration { - let info = Info::new( - ENV_VAR_CONFIG.to_string(), - ENV_VAR_PATH_CONFIG.to_string(), - DEFAULT_PATH_CONFIG.to_string(), - ENV_VAR_API_ADMIN_TOKEN.to_string(), - ) - .unwrap(); - - Configuration::load(&info).unwrap() + let info = Info::new(DEFAULT_PATH_CONFIG.to_string()).expect("info to load configuration is not valid"); + Configuration::load(&info).expect("configuration should be loaded from provided info") } #[cfg(test)] From ef15e0b42adf46d8c5b5fed9bedf01b56662f1b4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 13:46:58 +0100 Subject: [PATCH 073/217] refactor: [#851] rename env vars ``` TORRUST_TRACKER_BACK_ -> TORRUST_TRACKER_ TORRUST_TRACKER_DATABASE_DRIVER -> TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER TORRUST_TRACKER_API_ADMIN_TOKEN -> TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN TORRUST_TRACKER_CONFIG -> TORRUST_TRACKER_CONFIG_TOML TORRUST_TRACKER_PATH_CONFIG -> TORRUST_TRACKER_CONFIG_TOML_PATH ``` DB_DRIVER values: `MySQL`, `Sqlite3`. Removed lowercase values `mysql` and `sqlite3` used in containers. Some enums use lowercase. This is a braking change for container but not for configuration. IN the future we could use lowercase also in the configuration. --- Containerfile | 8 ++++---- README.md | 8 ++++---- compose.yaml | 4 ++-- docs/benchmarking.md | 2 +- docs/containers.md | 14 +++++++------- docs/profiling.md | 4 ++-- packages/configuration/src/lib.rs | 15 +++++++-------- packages/configuration/src/v1/mod.rs | 2 +- packages/primitives/src/lib.rs | 4 +++- share/container/entry_script_sh | 20 ++++++++++---------- src/bootstrap/config.rs | 6 +++--- src/console/ci/e2e/runner.rs | 2 +- src/console/profiling.rs | 4 ++-- src/lib.rs | 6 +++--- 14 files changed, 50 insertions(+), 49 deletions(-) diff --git a/Containerfile b/Containerfile index 590b0a13b..79fae692f 100644 --- a/Containerfile +++ b/Containerfile @@ -95,16 +95,16 @@ FROM gcr.io/distroless/cc-debian12:debug as runtime RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/env", "/bin/"] COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec -ARG TORRUST_TRACKER_PATH_CONFIG="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_DATABASE_DRIVER="sqlite3" +ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 -ENV TORRUST_TRACKER_PATH_CONFIG=${TORRUST_TRACKER_PATH_CONFIG} -ENV TORRUST_TRACKER_DATABASE_DRIVER=${TORRUST_TRACKER_DATABASE_DRIVER} +ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/README.md b/README.md index 8431c00e4..754d2d5b7 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,14 @@ cp ./share/default/config/tracker.development.sqlite3.toml ./storage/tracker/etc vim ./storage/tracker/etc/tracker.toml # Run the tracker with the updated configuration: -TORRUST_TRACKER_PATH_CONFIG="./storage/tracker/etc/tracker.toml" cargo run +TORRUST_TRACKER_CONFIG_TOML_PATH="./storage/tracker/etc/tracker.toml" cargo run ``` _Optionally, you may choose to supply the entire configuration as an environmental variable:_ ```sh # Use a configuration supplied on an environmental variable: -TORRUST_TRACKER_CONFIG=$(cat "./storage/tracker/etc/tracker.toml") cargo run +TORRUST_TRACKER_CONFIG_TOML=$(cat "./storage/tracker/etc/tracker.toml") cargo run ``` _For deployment, you **should** override the `api_admin_token` by using an environmental variable:_ @@ -102,8 +102,8 @@ gpg --armor --gen-random 1 10 | tee ./storage/tracker/lib/tracker_api_admin_toke chmod go-rwx ./storage/tracker/lib/tracker_api_admin_token.secret # Override secret in configuration using an environmental variable: -TORRUST_TRACKER_CONFIG=$(cat "./storage/tracker/etc/tracker.toml") \ - TORRUST_TRACKER_API_ADMIN_TOKEN=$(cat "./storage/tracker/lib/tracker_api_admin_token.secret") \ +TORRUST_TRACKER_CONFIG_TOML=$(cat "./storage/tracker/etc/tracker.toml") \ + TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=$(cat "./storage/tracker/lib/tracker_api_admin_token.secret") \ cargo run ``` diff --git a/compose.yaml b/compose.yaml index 672ca6d0f..1d425c743 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,8 +4,8 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_DATABASE_DRIVER=${TORRUST_TRACKER_DATABASE_DRIVER:-mysql} - - TORRUST_TRACKER_API_ADMIN_TOKEN=${TORRUST_TRACKER_API_ADMIN_TOKEN:-MyAccessToken} + - TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side ports: diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 7c82df14c..2a3f1f8b0 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -39,7 +39,7 @@ Build and run the tracker: ```console cargo build --release -TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" ./target/release/torrust-tracker +TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" ./target/release/torrust-tracker ``` Run the load test with: diff --git a/docs/containers.md b/docs/containers.md index 6622e29b2..a0ba59d4b 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -147,10 +147,10 @@ Environmental variables are loaded through the `--env`, in the format `--env VAR The following environmental variables can be set: -- `TORRUST_TRACKER_PATH_CONFIG` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). -- `TORRUST_TRACKER_API_ADMIN_TOKEN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_DATABASE_DRIVER` - The database type used for the container, (options: `sqlite3`, `mysql`, default `sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. -- `TORRUST_TRACKER_CONFIG` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG=$(cat tracker-tracker.toml)`). +- `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). +- `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). - `HTTP_PORT` - The port for the HTTP tracker. This should match the port used in the configuration, (default `7070`). @@ -205,7 +205,7 @@ mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ ## Run Torrust Tracker Container Image docker run -it \ - --env TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + --env TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ --env USER_ID="$(id -u)" \ --publish 0.0.0.0:7070:7070/tcp \ --publish 0.0.0.0:6969:6969/udp \ @@ -227,7 +227,7 @@ mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ ## Run Torrust Tracker Container Image podman run -it \ - --env TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + --env TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ --env USER_ID="$(id -u)" \ --publish 0.0.0.0:7070:7070/tcp \ --publish 0.0.0.0:6969:6969/udp \ @@ -255,7 +255,7 @@ docker build --target release --tag torrust-tracker:release --file Containerfile mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ USER_ID=$(id -u) \ - TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ docker compose up --build ``` diff --git a/docs/profiling.md b/docs/profiling.md index 406560f3c..8038f9e77 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -35,7 +35,7 @@ To generate the graph you will need to: ```console cargo build --profile=release-debug --bin=profiling ./target/release/aquatic_udp_load_test -c "load-test-config.toml" -sudo TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 +sudo TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 ``` __NOTICE__: You need to install the `aquatic_udp_load_test` program. @@ -92,7 +92,7 @@ Build and the binary for profiling: ```console RUSTFLAGS='-g' cargo build --release --bin profiling \ - && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ + && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ && valgrind \ --tool=callgrind \ --callgrind-out-file=callgrind.out \ diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 588feb87e..b79081a13 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -24,14 +24,13 @@ pub const TORRENT_PEERS_LIMIT: usize = 74; /// The whole `tracker.toml` file content. It has priority over the config file. /// Even if the file is not on the default path. -const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; +const ENV_VAR_CONFIG_TOML: &str = "TORRUST_TRACKER_CONFIG_TOML"; /// The `tracker.toml` file location. -pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; +pub const ENV_VAR_CONFIG_TOML_PATH: &str = "TORRUST_TRACKER_CONFIG_TOML_PATH"; /// Env var to overwrite API admin token. -/// Deprecated: use `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. -const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; +const ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN"; pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; @@ -65,9 +64,9 @@ impl Info { /// #[allow(clippy::needless_pass_by_value)] pub fn new(default_config_toml_path: String) -> Result { - let env_var_config_toml = ENV_VAR_CONFIG.to_string(); - let env_var_config_toml_path = ENV_VAR_PATH_CONFIG.to_string(); - let env_var_api_admin_token = ENV_VAR_API_ADMIN_TOKEN.to_string(); + let env_var_config_toml = ENV_VAR_CONFIG_TOML.to_string(); + let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); + let env_var_api_admin_token = ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); @@ -146,7 +145,7 @@ impl AnnouncePolicy { pub enum Error { /// Unable to load the configuration from the environment variable. /// This error only occurs if there is no configuration file and the - /// `TORRUST_TRACKER_CONFIG` environment variable is not set. + /// `TORRUST_TRACKER_CONFIG_TOML` environment variable is not set. #[error("Unable to load from Environmental Variable: {source}")] UnableToLoadFromEnvironmentVariable { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8e15d65ca..b9d75c71d 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -9,7 +9,7 @@ //! with the same content as the file. //! //! Configuration can not only be loaded from a file, but also from an -//! environment variable `TORRUST_TRACKER_CONFIG`. This is useful when running +//! environment variable `TORRUST_TRACKER_CONFIG_TOML`. This is useful when running //! the tracker in a Docker container or environments where you do not have a //! persistent storage or you cannot inject a configuration file. Refer to //! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index aeb4d0d4e..47a837a9b 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -52,7 +52,9 @@ pub struct NumberOfBytes(pub i64); /// For more information about persistence. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] pub enum DatabaseDriver { - // TODO: Move to the database crate once that gets its own crate. + // TODO: + // - Move to the database crate once that gets its own crate. + // - Rename serialized values to lowercase: `sqlite3` and `mysql`. /// The Sqlite3 database driver. Sqlite3, /// The `MySQL` database driver. diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 4f98e6622..8c704ea67 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,29 +26,29 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_DATABASE_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_DATABASE_DRIVER" "sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "Sqlite3"; then - # Select sqlite3 empty database + # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" - # Select sqlite3 default configuration + # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_DATABASE_DRIVER" "mysql"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "MySQL"; then - # (no database file needed for mysql) + # (no database file needed for MySQL) - # Select default mysql configuration + # Select default MySQL configuration default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_DATABASE_DRIVER\"." - echo "Please Note: Supported Database Types: \"sqlite3\", \"mysql\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\"." + echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_DATABASE_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs index 03dfd9a2f..6b607bd6f 100644 --- a/src/bootstrap/config.rs +++ b/src/bootstrap/config.rs @@ -1,6 +1,6 @@ //! Initialize configuration from file or env var. //! -//! All environment variables are prefixed with `TORRUST_TRACKER_BACK_`. +//! All environment variables are prefixed with `TORRUST_TRACKER_`. use torrust_tracker_configuration::{Configuration, Info}; @@ -11,7 +11,7 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// There are two methods to inject the configuration: /// /// 1. By using a config file: `tracker.toml`. -/// 2. Environment variable: `TORRUST_TRACKER_CONFIG`. The variable contains the same contents as the `tracker.toml` file. +/// 2. Environment variable: `TORRUST_TRACKER_CONFIG_TOML`. The variable contains the same contents as the `tracker.toml` file. /// /// Environment variable has priority over the config file. /// @@ -20,7 +20,7 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// # Panics /// /// Will panic if it can't load the configuration from either -/// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG`. +/// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG_TOML`. #[must_use] pub fn initialize_configuration() -> Configuration { let info = Info::new(DEFAULT_PATH_CONFIG.to_string()).expect("info to load configuration is not valid"); diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index 1a4746800..945a87033 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -46,7 +46,7 @@ pub fn run() { // Besides, if we don't use port 0 we should get the port numbers from the tracker configuration. // We could not use docker, but the intention was to create E2E tests including containerization. let options = RunOptions { - env_vars: vec![("TORRUST_TRACKER_CONFIG".to_string(), tracker_config.to_string())], + env_vars: vec![("TORRUST_TRACKER_CONFIG_TOML".to_string(), tracker_config.to_string())], ports: vec![ "6969:6969/udp".to_string(), "7070:7070/tcp".to_string(), diff --git a/src/console/profiling.rs b/src/console/profiling.rs index e0867159f..52e11913f 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -27,7 +27,7 @@ //! //! ```text //! RUSTFLAGS='-g' cargo build --release --bin profiling \ -//! && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ +//! && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ //! && valgrind \ //! --tool=callgrind \ //! --callgrind-out-file=callgrind.out \ @@ -40,7 +40,7 @@ //! //! ```text //! RUSTFLAGS='-g' cargo build --release --bin profiling \ -//! && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ +//! && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ //! && valgrind \ //! --tool=callgrind \ //! --callgrind-out-file=callgrind.out \ diff --git a/src/lib.rs b/src/lib.rs index 22bc133e1..6fd5da15f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,10 +213,10 @@ //! documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). //! //! Alternatively to the `tracker.toml` file you can use one environment -//! variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: +//! variable `TORRUST_TRACKER_CONFIG_TOML` to pass the configuration to the tracker: //! //! ```text -//! TORRUST_TRACKER_CONFIG=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker +//! TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker //! ``` //! //! In the previous example you are just setting the env var with the contents @@ -225,7 +225,7 @@ //! The env var contains the same data as the `tracker.toml`. It's particularly //! useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). //! -//! > NOTICE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. +//! > NOTICE: The `TORRUST_TRACKER_CONFIG_TOML` env var has priority over the `tracker.toml` file. //! //! By default, if you don’t specify any `tracker.toml` file, the application //! will use `./share/default/config/tracker.development.sqlite3.toml`. From a4d2adfe8bd5f5c9dd4fd866e0f89b2b3c404154 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 15:27:37 +0100 Subject: [PATCH 074/217] feat!: remove deprecated env var The env var `TORRUST_TRACKER_API_ADMIN_TOKEN` was replaced with `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. After the migration to Figment all configuration options can be overwritten. --- packages/configuration/src/lib.rs | 6 ----- packages/configuration/src/v1/mod.rs | 34 +--------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index b79081a13..62792c271 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -29,9 +29,6 @@ const ENV_VAR_CONFIG_TOML: &str = "TORRUST_TRACKER_CONFIG_TOML"; /// The `tracker.toml` file location. pub const ENV_VAR_CONFIG_TOML_PATH: &str = "TORRUST_TRACKER_CONFIG_TOML_PATH"; -/// Env var to overwrite API admin token. -const ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN"; - pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; pub type HttpTracker = v1::http_tracker::HttpTracker; @@ -52,7 +49,6 @@ pub struct TrackerPolicy { pub struct Info { config_toml: Option, config_toml_path: String, - api_admin_token: Option, } impl Info { @@ -66,7 +62,6 @@ impl Info { pub fn new(default_config_toml_path: String) -> Result { let env_var_config_toml = ENV_VAR_CONFIG_TOML.to_string(); let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); - let env_var_api_admin_token = ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); @@ -86,7 +81,6 @@ impl Info { Ok(Self { config_toml, config_toml_path, - api_admin_token: env::var(env_var_api_admin_token).ok(), }) } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index b9d75c71d..8d45270b8 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -288,10 +288,6 @@ impl Default for Configuration { } impl Configuration { - fn override_api_admin_token(&mut self, api_admin_token: &str) { - self.http_api.override_admin_token(api_admin_token); - } - /// Returns the tracker public IP address id defined in the configuration, /// and `None` otherwise. #[must_use] @@ -331,11 +327,7 @@ impl Configuration { .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) }; - let mut config: Configuration = figment.extract()?; - - if let Some(ref token) = info.api_admin_token { - config.override_api_admin_token(token); - }; + let config: Configuration = figment.extract()?; Ok(config) } @@ -469,7 +461,6 @@ mod tests { let info = Info { config_toml: Some(empty_configuration), config_toml_path: "tracker.toml".to_string(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -491,7 +482,6 @@ mod tests { let info = Info { config_toml: Some(config_toml), config_toml_path: String::new(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -515,7 +505,6 @@ mod tests { let info = Info { config_toml: None, config_toml_path: "tracker.toml".to_string(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -534,27 +523,6 @@ mod tests { let info = Info { config_toml: Some(default_config_toml()), config_toml_path: String::new(), - api_admin_token: None, - }; - - let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - - assert_eq!( - configuration.http_api.access_tokens.get("admin"), - Some("NewToken".to_owned()).as_ref() - ); - - Ok(()) - }); - } - - #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { - figment::Jail::expect_with(|_jail| { - let info = Info { - config_toml: Some(default_config_toml()), - config_toml_path: String::new(), - api_admin_token: Some("NewToken".to_owned()), }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); From e1e107123a09893e08b0443ac9750cf6a3e6987f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 17:27:08 +0100 Subject: [PATCH 075/217] docs: update README - Fix missing line break after badgets. - Add roadmap draft. --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 754d2d5b7..ebd7c357b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Torrust Tracker -[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf]**Torrust Tracker** is a [BitTorrent][bittorrent] Tracker that matchmakes peers and collects statistics. Written in [Rust Language][rust] with the [Axum] web framework. _**This tracker aims to be respectful to established standards, (both [formal][BEP 00] and [otherwise][torrent_source_felid]).___ +[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf] + +**Torrust Tracker** is a [BitTorrent][bittorrent] Tracker that matchmakes peers and collects statistics. Written in [Rust Language][rust] with the [Axum] web framework. **This tracker aims to be respectful to established standards, (both [formal][BEP 00] and [otherwise][torrent_source_felid]).** > This is a [Torrust][torrust] project and is in active development. It is community supported as well as sponsored by [Nautilus Cyberneering][nautilus]. @@ -17,6 +19,44 @@ - [x] Support [newTrackon][newtrackon] checks. - [x] Persistent `SQLite3` or `MySQL` Databases. +## Roadmap + +Persistence: + +- [ ] Support other databases. + +Integrations: + +- [ ] Webhooks. + +Administration: + +- [ ] Improve categories and tag management. +- [ ] User management: list, search and ban users. +- [ ] Full-private mode. +- [ ] User statistics. + +Users: + +- [ ] Reset or change the password. +- [ ] User profile. +- [ ] Invitation system. +- [ ] User moderation. +- [ ] Add torrent providing only the info-hash. +- [ ] Improve search. + +Torrents: + +- [ ] Change the source field. +- [ ] Change the creator field. +- [ ] Implement BEP 19: WebSeed - HTTP/FTP Seeding (GetRight style). +- [ ] Implement BEP 32: BitTorrent DHT Extensions for IPv6. + +Others: + +- [ ] Multi-tracker +- [ ] Multi-language + ## Implemented BitTorrent Enhancement Proposals (BEPs) > > _[Learn more about BitTorrent Enhancement Proposals][BEP 00]_ From 80fc8d6b4d7b120b11984bbe229c6c7860da3909 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 18:01:47 +0100 Subject: [PATCH 076/217] docs: udpate roadmap in README I copied the Index roadmap isntead of the tracker one by mistake. --- README.md | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ebd7c357b..f83dbd936 100644 --- a/README.md +++ b/README.md @@ -21,41 +21,39 @@ ## Roadmap +Core: + +- [ ] New option `want_ip_from_query_string`. See . +- [ ] Permanent keys. See . +- [ ] Peer and torrents specific statistics. See . + Persistence: -- [ ] Support other databases. +- [ ] Support other databases like PostgreSQL. -Integrations: +Performance: -- [ ] Webhooks. +- [ ] More optimizations. See . -Administration: +Protocols: -- [ ] Improve categories and tag management. -- [ ] User management: list, search and ban users. -- [ ] Full-private mode. -- [ ] User statistics. +- [ ] WebTorrent. -Users: +Integrations: -- [ ] Reset or change the password. -- [ ] User profile. -- [ ] Invitation system. -- [ ] User moderation. -- [ ] Add torrent providing only the info-hash. -- [ ] Improve search. +- [ ] Monitoring (Prometheus). -Torrents: +Utils: -- [ ] Change the source field. -- [ ] Change the creator field. -- [ ] Implement BEP 19: WebSeed - HTTP/FTP Seeding (GetRight style). -- [ ] Implement BEP 32: BitTorrent DHT Extensions for IPv6. +- [ ] Tracker client. +- [ ] Tracker checker. Others: -- [ ] Multi-tracker -- [ ] Multi-language +- [ ] Support for Windows. +- [ ] Docker images for other architectures. + + ## Implemented BitTorrent Enhancement Proposals (BEPs) > From dadc216e8a3a75316f735d6b3debdab08d8e595a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:25:58 +0100 Subject: [PATCH 077/217] chore(deps): add cargo dependencies needed for axum timeouts We want to add timeouts for the Axum server configuration, in order to close HTTP connections when the client doesn't send any request after opening a HTTP connection. --- Cargo.lock | 5 +++++ Cargo.toml | 5 +++++ cSpell.json | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 446477aac..523ea575d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3891,8 +3891,11 @@ dependencies = [ "fern", "figment", "futures", + "futures-util", "hex-literal", + "http-body", "hyper", + "hyper-util", "lazy_static", "local-ip-address", "log", @@ -3900,6 +3903,7 @@ dependencies = [ "multimap", "parking_lot", "percent-encoding", + "pin-project-lite", "r2d2", "r2d2_mysql", "r2d2_sqlite", @@ -3920,6 +3924,7 @@ dependencies = [ "torrust-tracker-primitives", "torrust-tracker-test-helpers", "torrust-tracker-torrent-repository", + "tower", "tower-http", "trace", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 60652b160..5183c6067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,13 +46,17 @@ derive_more = "0" fern = "0" figment = "0.10.18" futures = "0" +futures-util = "0.3.30" hex-literal = "0" +http-body = "1.0.0" hyper = "1" +hyper-util = { version = "0.1.3", features = ["http1", "http2", "tokio"] } lazy_static = "1" log = { version = "0", features = ["release_max_level_info"] } multimap = "0" parking_lot = "0.12.1" percent-encoding = "2" +pin-project-lite = "0.2.14" r2d2 = "0" r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } @@ -72,6 +76,7 @@ torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = " torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "packages/located-error" } torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" } torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path = "packages/torrent-repository" } +tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } trace = "0" tracing = "0" diff --git a/cSpell.json b/cSpell.json index bd6c9d489..2b5cf55bf 100644 --- a/cSpell.json +++ b/cSpell.json @@ -50,6 +50,7 @@ "downloadedi", "dtolnay", "elif", + "Eray", "filesd", "flamegraph", "Freebox", @@ -73,6 +74,7 @@ "intervali", "Joakim", "kallsyms", + "Karatay", "kcachegrind", "kexec", "keyout", @@ -107,6 +109,7 @@ "Pando", "peekable", "peerlist", + "programatik", "proot", "proto", "Quickstart", @@ -137,6 +140,7 @@ "sharktorrent", "SHLVL", "skiplist", + "slowloris", "socketaddr", "sqllite", "subsec", From 112b76d79da76381d4ee8f04828c74a438d83308 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:27:50 +0100 Subject: [PATCH 078/217] fix: [#612] add timeout for time waiting for the first API requests Adds a timeout for the Tracker API for the time the server waits for the first request from the client after openning a new HTTP connection. --- src/servers/apis/server.rs | 9 +- src/servers/custom_axum_server.rs | 275 ++++++++++++++++++++++++++++++ src/servers/mod.rs | 1 + 3 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 src/servers/custom_axum_server.rs diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 9317d6ec0..57d2629ae 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -37,6 +37,7 @@ use torrust_tracker_configuration::AccessTokens; use super::routes::router; use crate::bootstrap::jobs::Started; use crate::core::Tracker; +use crate::servers::custom_axum_server::{self, TimeoutAcceptor}; use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; use crate::servers::signals::{graceful_shutdown, Halted}; @@ -177,7 +178,7 @@ impl ApiServer { /// Or if there request returns an error code. #[must_use] pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/api/health_check"); + let url = format!("http://{binding}/api/health_check"); // DevSkim: ignore DS137138 let info = format!("checking api health check at: {url}"); @@ -234,13 +235,15 @@ impl Launcher { let running = Box::pin(async { match tls { - Some(tls) => axum_server::from_tcp_rustls(socket, tls) + Some(tls) => custom_axum_server::from_tcp_rustls_with_timeouts(socket, tls) .handle(handle) + .acceptor(TimeoutAcceptor) .serve(router.into_make_service_with_connect_info::()) .await .expect("Axum server for tracker API crashed."), - None => axum_server::from_tcp(socket) + None => custom_axum_server::from_tcp_with_timeouts(socket) .handle(handle) + .acceptor(TimeoutAcceptor) .serve(router.into_make_service_with_connect_info::()) .await .expect("Axum server for tracker API crashed."), diff --git a/src/servers/custom_axum_server.rs b/src/servers/custom_axum_server.rs new file mode 100644 index 000000000..5705ef24e --- /dev/null +++ b/src/servers/custom_axum_server.rs @@ -0,0 +1,275 @@ +//! Wrapper for Axum server to add timeouts. +//! +//! Copyright (c) Eray Karatay ([@programatik29](https://github.com/programatik29)). +//! +//! See: . +//! +//! If a client opens a HTTP connection and it does not send any requests, the +//! connection is closed after a timeout. You can test it with: +//! +//! ```text +//! telnet 127.0.0.1 1212 +//! Trying 127.0.0.1... +//! Connected to 127.0.0.1. +//! Escape character is '^]'. +//! Connection closed by foreign host. +//! ``` +//! +//! If you want to know more about Axum and timeouts see . +use std::future::Ready; +use std::io::ErrorKind; +use std::net::TcpListener; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use axum_server::accept::Accept; +use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig}; +use axum_server::Server; +use futures_util::{ready, Future}; +use http_body::{Body, Frame}; +use hyper::Response; +use hyper_util::rt::TokioTimer; +use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use tokio::time::{Instant, Sleep}; +use tower::Service; + +const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(5); +const HTTP2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5); +const HTTP2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); + +#[must_use] +pub fn from_tcp_with_timeouts(socket: TcpListener) -> Server { + add_timeouts(axum_server::from_tcp(socket)) +} + +#[must_use] +pub fn from_tcp_rustls_with_timeouts(socket: TcpListener, tls: RustlsConfig) -> Server { + add_timeouts(axum_server::from_tcp_rustls(socket, tls)) +} + +fn add_timeouts(mut server: Server) -> Server { + server.http_builder().http1().timer(TokioTimer::new()); + server.http_builder().http2().timer(TokioTimer::new()); + + server.http_builder().http1().header_read_timeout(HTTP1_HEADER_READ_TIMEOUT); + server + .http_builder() + .http2() + .keep_alive_timeout(HTTP2_KEEP_ALIVE_TIMEOUT) + .keep_alive_interval(HTTP2_KEEP_ALIVE_INTERVAL); + + server +} + +#[derive(Clone)] +pub struct TimeoutAcceptor; + +impl Accept for TimeoutAcceptor { + type Stream = TimeoutStream; + type Service = TimeoutService; + type Future = Ready>; + + fn accept(&self, stream: I, service: S) -> Self::Future { + let (tx, rx) = mpsc::unbounded_channel(); + + let stream = TimeoutStream::new(stream, HTTP1_HEADER_READ_TIMEOUT, rx); + let service = TimeoutService::new(service, tx); + + std::future::ready(Ok((stream, service))) + } +} + +#[derive(Clone)] +pub struct TimeoutService { + inner: S, + sender: UnboundedSender, +} + +impl TimeoutService { + fn new(inner: S, sender: UnboundedSender) -> Self { + Self { inner, sender } + } +} + +impl Service for TimeoutService +where + S: Service>, +{ + type Response = Response>; + type Error = S::Error; + type Future = TimeoutServiceFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + // send timer wait signal + let _ = self.sender.send(TimerSignal::Wait); + + TimeoutServiceFuture::new(self.inner.call(req), self.sender.clone()) + } +} + +pin_project! { + pub struct TimeoutServiceFuture { + #[pin] + inner: F, + sender: Option>, + } +} + +impl TimeoutServiceFuture { + fn new(inner: F, sender: UnboundedSender) -> Self { + Self { + inner, + sender: Some(sender), + } + } +} + +impl Future for TimeoutServiceFuture +where + F: Future, E>>, +{ + type Output = Result>, E>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.inner.poll(cx).map(|result| { + result.map(|response| { + response.map(|body| TimeoutBody::new(body, this.sender.take().expect("future polled after ready"))) + }) + }) + } +} + +enum TimerSignal { + Wait, + Reset, +} + +pin_project! { + pub struct TimeoutBody { + #[pin] + inner: B, + sender: UnboundedSender, + } +} + +impl TimeoutBody { + fn new(inner: B, sender: UnboundedSender) -> Self { + Self { inner, sender } + } +} + +impl Body for TimeoutBody { + type Data = B::Data; + type Error = B::Error; + + fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>>> { + let this = self.project(); + let option = ready!(this.inner.poll_frame(cx)); + + if option.is_none() { + let _ = this.sender.send(TimerSignal::Reset); + } + + Poll::Ready(option) + } + + fn is_end_stream(&self) -> bool { + let is_end_stream = self.inner.is_end_stream(); + + if is_end_stream { + let _ = self.sender.send(TimerSignal::Reset); + } + + is_end_stream + } + + fn size_hint(&self) -> http_body::SizeHint { + self.inner.size_hint() + } +} + +pub struct TimeoutStream { + inner: IO, + // hyper requires unpin + sleep: Pin>, + duration: Duration, + waiting: bool, + receiver: UnboundedReceiver, + finished: bool, +} + +impl TimeoutStream { + fn new(inner: IO, duration: Duration, receiver: UnboundedReceiver) -> Self { + Self { + inner, + sleep: Box::pin(tokio::time::sleep(duration)), + duration, + waiting: false, + receiver, + finished: false, + } + } +} + +impl AsyncRead for TimeoutStream { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + if !self.finished { + match Pin::new(&mut self.receiver).poll_recv(cx) { + // reset the timer + Poll::Ready(Some(TimerSignal::Reset)) => { + self.waiting = false; + + let deadline = Instant::now() + self.duration; + self.sleep.as_mut().reset(deadline); + } + // enter waiting mode (for response body last chunk) + Poll::Ready(Some(TimerSignal::Wait)) => self.waiting = true, + Poll::Ready(None) => self.finished = true, + Poll::Pending => (), + } + } + + if !self.waiting { + // return error if timer is elapsed + if let Poll::Ready(()) = self.sleep.as_mut().poll(cx) { + return Poll::Ready(Err(std::io::Error::new(ErrorKind::TimedOut, "request header read timed out"))); + } + } + + Pin::new(&mut self.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for TimeoutStream { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + Pin::new(&mut self.inner).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } +} diff --git a/src/servers/mod.rs b/src/servers/mod.rs index b0e222d2a..0c9cc5dd8 100644 --- a/src/servers/mod.rs +++ b/src/servers/mod.rs @@ -1,5 +1,6 @@ //! Servers. Services that can be started and stopped. pub mod apis; +pub mod custom_axum_server; pub mod health_check_api; pub mod http; pub mod registar; From 9e42a1a452804f1f14f5e6a99c38bdccabbc5001 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:51:25 +0100 Subject: [PATCH 079/217] feat: [#612] tower middleware to apply timeouts to requests --- src/servers/apis/routes.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs index e3d1ef446..087bcfa4a 100644 --- a/src/servers/apis/routes.rs +++ b/src/servers/apis/routes.rs @@ -8,12 +8,15 @@ use std::sync::Arc; use std::time::Duration; +use axum::error_handling::HandleErrorLayer; use axum::http::HeaderName; use axum::response::Response; use axum::routing::get; -use axum::{middleware, Router}; -use hyper::Request; +use axum::{middleware, BoxError, Router}; +use hyper::{Request, StatusCode}; use torrust_tracker_configuration::AccessTokens; +use tower::timeout::TimeoutLayer; +use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; @@ -25,6 +28,8 @@ use super::v1::context::health_check::handlers::health_check_handler; use super::v1::middlewares::auth::State; use crate::core::Tracker; +const TIMEOUT: Duration = Duration::from_secs(5); + /// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc, access_tokens: Arc) -> Router { @@ -73,4 +78,11 @@ pub fn router(tracker: Arc, access_tokens: Arc) -> Router }), ) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer( + ServiceBuilder::new() + // this middleware goes above `TimeoutLayer` because it will receive + // errors returned by `TimeoutLayer` + .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) + .layer(TimeoutLayer::new(TIMEOUT)), + ) } From 23d5e5e14d93d66dd51ec5095b3d3e199e727db3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 15:31:15 +0100 Subject: [PATCH 080/217] fix: [#613] add timeout for time waiting for the first HTTP tracker request Adds a timeout to the HTTP tracker for the time the server waits for the first request from the client after openning a new HTTP connection. It also adds a tower middleware for timeouts in requests. --- src/servers/http/server.rs | 7 +++++-- src/servers/http/v1/routes.rs | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 7c8148f22..a68f7d16c 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -12,6 +12,7 @@ use tokio::sync::oneshot::{Receiver, Sender}; use super::v1::routes::router; use crate::bootstrap::jobs::Started; use crate::core::Tracker; +use crate::servers::custom_axum_server::{self, TimeoutAcceptor}; use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; use crate::servers::signals::{graceful_shutdown, Halted}; @@ -60,13 +61,15 @@ impl Launcher { let running = Box::pin(async { match tls { - Some(tls) => axum_server::from_tcp_rustls(socket, tls) + Some(tls) => custom_axum_server::from_tcp_rustls_with_timeouts(socket, tls) .handle(handle) + .acceptor(TimeoutAcceptor) .serve(app.into_make_service_with_connect_info::()) .await .expect("Axum server crashed."), - None => axum_server::from_tcp(socket) + None => custom_axum_server::from_tcp_with_timeouts(socket) .handle(handle) + .acceptor(TimeoutAcceptor) .serve(app.into_make_service_with_connect_info::()) .await .expect("Axum server crashed."), diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index 05cd38713..c54da51a3 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -3,12 +3,15 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; +use axum::error_handling::HandleErrorLayer; use axum::http::HeaderName; use axum::response::Response; use axum::routing::get; -use axum::Router; +use axum::{BoxError, Router}; use axum_client_ip::SecureClientIpSource; -use hyper::Request; +use hyper::{Request, StatusCode}; +use tower::timeout::TimeoutLayer; +use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; @@ -18,6 +21,8 @@ use tracing::{Level, Span}; use super::handlers::{announce, health_check, scrape}; use crate::core::Tracker; +const TIMEOUT: Duration = Duration::from_secs(5); + /// It adds the routes to the router. /// /// > **NOTICE**: it's added a layer to get the client IP from the connection @@ -69,4 +74,11 @@ pub fn router(tracker: Arc, server_socket_addr: SocketAddr) -> Router { }), ) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer( + ServiceBuilder::new() + // this middleware goes above `TimeoutLayer` because it will receive + // errors returned by `TimeoutLayer` + .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) + .layer(TimeoutLayer::new(TIMEOUT)), + ) } From 23c52b191c62ca39b7b9947f3167b4bd6cf8fbae Mon Sep 17 00:00:00 2001 From: Gabriel Grondin Date: Sun, 19 May 2024 19:34:08 +0200 Subject: [PATCH 081/217] Fix REAADME HTTP port --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f83dbd936..306a8620c 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ The following services are provided by the default configuration: - UDP _(tracker)_ - `udp://127.0.0.1:6969/announce`. - HTTP _(tracker)_ - - `http://127.0.0.1:6969/announce`. + - `http://127.0.0.1:7070/announce`. - API _(management)_ - `http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken`. From 9be936622b5199d12f022aa197927aaf15a09915 Mon Sep 17 00:00:00 2001 From: Gabriel GRONDIN Date: Mon, 20 May 2024 18:12:54 +0200 Subject: [PATCH 082/217] Fix and improved bootstrap jobs module test. On O.S. with different language than english the BadTlsConfig source error message will differ from the compared one. This will fix this issue by matching the enum itself instead of relying on a string compare. --- src/bootstrap/jobs/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index d288989b5..e20d243c6 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -50,7 +50,7 @@ mod tests { use camino::Utf8PathBuf; use torrust_tracker_configuration::TslConfig; - use super::make_rust_tls; + use super::{make_rust_tls, Error}; #[tokio::test] async fn it_should_error_on_bad_tls_config() { @@ -65,9 +65,7 @@ mod tests { .expect("tls_was_enabled") .expect_err("bad_cert_and_key_files"); - assert!(err - .to_string() - .contains("bad tls config: No such file or directory (os error 2)")); + assert!(matches!(err, Error::BadTlsConfig { source: _ })); } #[tokio::test] @@ -83,7 +81,7 @@ mod tests { .expect("tls_was_enabled") .expect_err("missing_config"); - assert_eq!(err.to_string(), "tls config missing"); + assert!(matches!(err, Error::MissingTlsConfig { location: _ })); } } From 932e66e14533ea15f3acff526b852e8ecafb938b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 15:44:08 +0100 Subject: [PATCH 083/217] feat: [#870] add privacy methods to the TrackerMode The tracker mode can be: - Public (Non-whitelisted) - Listed (Whitelisted) - Private (Non-whitelisted) - PrivateListed (Whitelisted) They should have been two different flags (in my opinion): - Visibility: public or private - Whitelisted: true or false So we would have the same four convinations: - Not whitelisted: - Public - Private - Whitelisted - Public - Private That's a pending refactor. For this commits, the goal is just to align this enum with what we added to the Index so we can use this enum in the Index via the primmitives crate. --- packages/primitives/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index 47a837a9b..aa3af27e3 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -85,3 +85,21 @@ pub enum TrackerMode { #[serde(rename = "private_listed")] PrivateListed, } + +impl Default for TrackerMode { + fn default() -> Self { + Self::Public + } +} + +impl TrackerMode { + #[must_use] + pub fn is_open(&self) -> bool { + matches!(self, TrackerMode::Public | TrackerMode::Listed) + } + + #[must_use] + pub fn is_close(&self) -> bool { + !self.is_open() + } +} From 74d8f7918bd6c25c8dea8d208659ec6a3199bead Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 16:05:39 +0100 Subject: [PATCH 084/217] feat: [#870] remove Copy trait from TrackerMode --- packages/primitives/src/lib.rs | 2 +- src/core/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index aa3af27e3..eccd220f9 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -67,7 +67,7 @@ pub type PersistentTorrents = BTreeMap; /// /// Refer to [Torrust Tracker Configuration](https://docs.rs/torrust-tracker-configuration) /// to know how to configure the tracker to run in each mode. -#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub enum TrackerMode { /// Will track every new info hash and serve every peer. #[serde(rename = "public")] diff --git a/src/core/mod.rs b/src/core/mod.rs index 18a6028f7..2b61e3031 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -547,7 +547,7 @@ impl Tracker { ) -> Result { let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); - let mode = config.mode; + let mode = config.mode.clone(); Ok(Tracker { //config, From 0c9da2f48b04e149991f5cc9a86b25ee8d2ceb43 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 16:19:20 +0100 Subject: [PATCH 085/217] feat: [#870] implement traits Dispaly and FromStr for TrackerMode --- packages/primitives/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index eccd220f9..454635e8d 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -5,6 +5,8 @@ //! by the tracker server crate, but also by other crates in the Torrust //! ecosystem. use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; use std::time::Duration; use info_hash::InfoHash; @@ -92,6 +94,32 @@ impl Default for TrackerMode { } } +impl fmt::Display for TrackerMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display_str = match self { + TrackerMode::Public => "public", + TrackerMode::Listed => "listed", + TrackerMode::Private => "private", + TrackerMode::PrivateListed => "private_listed", + }; + write!(f, "{display_str}") + } +} + +impl FromStr for TrackerMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "public" => Ok(TrackerMode::Public), + "listed" => Ok(TrackerMode::Listed), + "private" => Ok(TrackerMode::Private), + "private_listed" => Ok(TrackerMode::PrivateListed), + _ => Err(format!("Unknown tracker mode: {s}")), + } + } +} + impl TrackerMode { #[must_use] pub fn is_open(&self) -> bool { From 9e71e718afd420266cbc1e1e43dcce073e0f8e3f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 May 2024 09:23:18 +0100 Subject: [PATCH 086/217] chore(deps): update dependencies ```output $ cargo update Updating crates.io index Updating addr2line v0.21.0 -> v0.22.0 (latest: v0.23.0) Updating anyhow v1.0.83 -> v1.0.86 Updating async-channel v2.3.0 -> v2.3.1 Updating async-compression v0.4.10 -> v0.4.11 Updating async-executor v1.11.0 -> v1.12.0 Updating backtrace v0.3.71 -> v0.3.72 Updating blocking v1.6.0 -> v1.6.1 Updating brotli-decompressor v4.0.0 -> v4.0.1 Updating camino v1.1.6 -> v1.1.7 Updating cc v1.0.97 -> v1.0.98 Updating clang-sys v1.7.0 -> v1.8.1 Updating crc32fast v1.4.0 -> v1.4.2 Updating crossbeam-channel v0.5.12 -> v0.5.13 Updating crossbeam-utils v0.8.19 -> v0.8.20 Updating darling v0.20.8 -> v0.20.9 Updating darling_core v0.20.8 -> v0.20.9 Updating darling_macro v0.20.8 -> v0.20.9 Updating either v1.11.0 -> v1.12.0 Updating event-listener v5.3.0 -> v5.3.1 Updating figment v0.10.18 -> v0.10.19 Updating gimli v0.28.1 -> v0.29.0 (latest: v0.30.0) Updating h2 v0.4.4 -> v0.4.5 Updating hyper-util v0.1.3 -> v0.1.5 Updating instant v0.1.12 -> v0.1.13 Updating libc v0.2.154 -> v0.2.155 Updating libz-sys v1.1.16 -> v1.1.18 Updating linux-raw-sys v0.4.13 -> v0.4.14 (latest: v0.6.4) Updating miniz_oxide v0.7.2 -> v0.7.3 Updating native-tls v0.2.11 -> v0.2.12 Updating object v0.32.2 -> v0.35.0 (latest: v0.36.0) Updating parking_lot v0.12.2 -> v0.12.3 Updating plotters v0.3.5 -> v0.3.6 Updating plotters-backend v0.3.5 -> v0.3.6 Updating plotters-svg v0.3.5 -> v0.3.6 Updating proc-macro2 v1.0.82 -> v1.0.84 Updating ringbuf v0.4.0 -> v0.4.1 Updating rstest v0.19.0 -> v0.20.0 Updating rstest_macros v0.19.0 -> v0.20.0 Updating rustversion v1.0.16 -> v1.0.17 Updating serde v1.0.201 -> v1.0.203 Updating serde_derive v1.0.201 -> v1.0.203 Updating serde_spanned v0.6.5 -> v0.6.6 Removing strsim v0.10.0 Updating syn v2.0.63 -> v2.0.66 Updating thiserror v1.0.60 -> v1.0.61 Updating thiserror-impl v1.0.60 -> v1.0.61 Updating tokio v1.37.0 -> v1.38.0 Updating tokio-macros v2.2.0 -> v2.3.0 Updating toml v0.8.12 -> v0.8.13 Updating toml_datetime v0.6.5 -> v0.6.6 Updating toml_edit v0.22.12 -> v0.22.13 Updating winnow v0.6.8 -> v0.6.9 ``` --- Cargo.lock | 290 ++++++++++++++++++++++++++--------------------------- 1 file changed, 141 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 523ea575d..4e08b1b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "aquatic_peer_id" @@ -201,12 +201,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2776ead772134d55b62dd45e59a79e21612d85d0af729b8b7d3967d601a62a" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.3.0", "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", @@ -214,9 +213,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "brotli", "flate2", @@ -230,9 +229,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" +checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" dependencies = [ "async-task", "concurrent-queue", @@ -247,7 +246,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.0", + "async-channel 2.3.1", "async-executor", "async-io 2.3.2", "async-lock 3.3.0", @@ -357,7 +356,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -480,7 +479,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -508,9 +507,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -567,7 +566,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -605,12 +604,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.3.0", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", "futures-io", "futures-lite 2.3.0", @@ -637,7 +635,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "syn_derive", ] @@ -654,9 +652,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -716,9 +714,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -740,9 +738,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" dependencies = [ "jobserver", "libc", @@ -812,9 +810,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -840,7 +838,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -852,7 +850,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -931,9 +929,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -991,9 +989,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -1038,9 +1036,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -1060,9 +1058,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -1070,27 +1068,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.63", + "strsim", + "syn 2.0.66", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1137,7 +1135,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1158,9 +1156,9 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encoding_rs" @@ -1226,9 +1224,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -1251,7 +1249,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 5.3.0", + "event-listener 5.3.1", "pin-project-lite", ] @@ -1293,9 +1291,9 @@ dependencies = [ [[package]] name = "figment" -version = "0.10.18" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic", "parking_lot", @@ -1389,7 +1387,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1401,7 +1399,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1413,7 +1411,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1506,7 +1504,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -1568,9 +1566,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1592,15 +1590,15 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap 2.2.6", "slab", @@ -1770,9 +1768,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -1857,9 +1855,9 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2045,9 +2043,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -2072,9 +2070,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "pkg-config", @@ -2089,9 +2087,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" @@ -2159,9 +2157,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2201,7 +2199,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2252,7 +2250,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "termcolor", "thiserror", ] @@ -2306,11 +2304,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2409,9 +2406,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -2451,7 +2448,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2480,9 +2477,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2521,7 +2518,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2595,7 +2592,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -2629,9 +2626,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -2642,15 +2639,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -2769,9 +2766,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -2784,7 +2781,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "version_check", "yansi", ] @@ -3029,9 +3026,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2542bc32f4c763f52a2eb375cb0b76c5aa5771f569af74299e84dca51d988a2f" +checksum = "5c65e4c865bc3d2e3294493dff0acf7e6c259d066e34e22059fa9c39645c3636" dependencies = [ "crossbeam-utils", ] @@ -3067,9 +3064,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +checksum = "27059f51958c5f8496a6f79511e7c0ac396dd815dc8894e9b6e2efb5779cf6f0" dependencies = [ "futures", "futures-timer", @@ -3079,18 +3076,19 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +checksum = "e6132d64df104c0b3ea7a6ad7766a43f587bd773a4a9cf4cd59296d426afaf3a" dependencies = [ "cfg-if", "glob", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.63", + "syn 2.0.66", "unicode-ident", ] @@ -3168,7 +3166,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -3212,9 +3210,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -3308,9 +3306,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -3336,13 +3334,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3388,14 +3386,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -3439,7 +3437,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3538,12 +3536,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -3573,9 +3565,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -3591,7 +3583,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3679,22 +3671,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3755,9 +3747,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -3773,13 +3765,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -3817,21 +3809,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.13", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -3860,15 +3852,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.9", ] [[package]] @@ -4093,7 +4085,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] @@ -4261,7 +4253,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -4295,7 +4287,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4506,9 +4498,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] @@ -4556,7 +4548,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.63", + "syn 2.0.66", ] [[package]] From a3df726c9108f658274ac2d2870b3767b0398401 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 May 2024 09:50:57 +0100 Subject: [PATCH 087/217] fix: clippy errors --- cSpell.json | 1 + packages/clock/src/lib.rs | 6 +- packages/test-helpers/src/configuration.rs | 6 +- src/bootstrap/jobs/http_tracker.rs | 2 +- src/bootstrap/jobs/tracker_apis.rs | 8 +- src/bootstrap/jobs/udp_tracker.rs | 4 +- src/console/clients/udp/checker.rs | 2 +- src/console/profiling.rs | 8 +- src/core/databases/mod.rs | 4 +- src/core/mod.rs | 8 +- src/core/torrent/mod.rs | 6 +- src/lib.rs | 6 +- src/servers/apis/mod.rs | 22 ++-- .../apis/v1/context/auth_key/handlers.rs | 2 +- src/servers/apis/v1/context/auth_key/mod.rs | 10 +- .../v1/context/torrent/resources/torrent.rs | 4 +- .../apis/v1/context/whitelist/handlers.rs | 4 +- src/servers/apis/v1/context/whitelist/mod.rs | 8 +- src/servers/apis/v1/mod.rs | 2 +- src/servers/http/mod.rs | 54 ++++----- src/servers/http/server.rs | 6 +- .../http/v1/extractors/authentication_key.rs | 6 +- src/servers/http/v1/requests/announce.rs | 8 +- src/servers/http/v1/responses/error.rs | 4 +- src/servers/http/v1/routes.rs | 2 +- src/servers/http/v1/services/announce.rs | 4 +- src/servers/http/v1/services/scrape.rs | 4 +- src/servers/udp/connection_cookie.rs | 10 +- src/servers/udp/handlers.rs | 3 +- src/servers/udp/mod.rs | 108 +++++++++--------- src/servers/udp/peer_builder.rs | 3 +- src/servers/udp/server.rs | 10 +- 32 files changed, 165 insertions(+), 170 deletions(-) diff --git a/cSpell.json b/cSpell.json index 2b5cf55bf..ef807f035 100644 --- a/cSpell.json +++ b/cSpell.json @@ -64,6 +64,7 @@ "Hydranode", "hyperthread", "Icelake", + "iiiiiiiiiiiiiiiiiiiid", "imdl", "impls", "incompletei", diff --git a/packages/clock/src/lib.rs b/packages/clock/src/lib.rs index 9fc67cb54..295d22c16 100644 --- a/packages/clock/src/lib.rs +++ b/packages/clock/src/lib.rs @@ -17,11 +17,11 @@ //! ``` //! //! > **NOTICE**: internally the `Duration` is stores it's main unit as seconds in a `u64` and it will -//! overflow in 584.9 billion years. +//! > overflow in 584.9 billion years. //! //! > **NOTICE**: the timestamp does not depend on the time zone. That gives you -//! the ability to use the clock regardless of the underlying system time zone -//! configuration. See [Unix time Wikipedia entry](https://en.wikipedia.org/wiki/Unix_time). +//! > the ability to use the clock regardless of the underlying system time zone +//! > configuration. See [Unix time Wikipedia entry](https://en.wikipedia.org/wiki/Unix_time). pub mod clock; pub mod conv; diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 86ed57b9e..15ecd5280 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -13,11 +13,11 @@ use crate::random; /// > **NOTICE**: This configuration is not meant to be used in production. /// /// > **NOTICE**: Port 0 is used for ephemeral ports, which means that the OS -/// will assign a random free port for the tracker to use. +/// > will assign a random free port for the tracker to use. /// /// > **NOTICE**: You can change the log level to `debug` to see the logs of the -/// tracker while running the tests. That can be particularly useful when -/// debugging tests. +/// > tracker while running the tests. That can be particularly useful when +/// > debugging tests. /// /// # Panics /// diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index d8a976b98..9ae8995fc 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -3,7 +3,7 @@ //! The function [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) starts a new HTTP tracker server. //! //! > **NOTICE**: the application can launch more than one HTTP tracker on different ports. -//! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. +//! > Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. //! //! The [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) function spawns a new asynchronous task, //! that tasks is the "**launcher**". The "**launcher**" starts the actual server and sends a message back to the main application. diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 120c960ef..834574edb 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -4,8 +4,8 @@ //! function starts a the HTTP tracker REST API. //! //! > **NOTICE**: that even thought there is only one job the API has different -//! versions. API consumers can choose which version to use. The API version is -//! part of the URL, for example: `http://localhost:1212/api/v1/stats`. +//! > versions. API consumers can choose which version to use. The API version is +//! > part of the URL, for example: `http://localhost:1212/api/v1/stats`. //! //! The [`tracker_apis::start_job`](crate::bootstrap::jobs::tracker_apis::start_job) //! function spawns a new asynchronous task, that tasks is the "**launcher**". @@ -38,8 +38,8 @@ use crate::servers::registar::ServiceRegistrationForm; /// application process to notify the API server was successfully started. /// /// > **NOTICE**: it does not mean the API server is ready to receive requests. -/// It only means the new server started. It might take some time to the server -/// to be ready to accept request. +/// > It only means the new server started. It might take some time to the server +/// > to be ready to accept request. #[derive(Debug)] pub struct ApiServerJobStarted(); diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index bb1cdb492..853cb7461 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -4,8 +4,8 @@ //! function starts a new UDP tracker server. //! //! > **NOTICE**: that the application can launch more than one UDP tracker -//! on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) -//! for the configuration options. +//! > on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) +//! > for the configuration options. use std::sync::Arc; use log::debug; diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index d51492041..37928f0df 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -104,7 +104,7 @@ impl Client { /// /// - It can't connect to the remote UDP socket. /// - It can't make a connection request successfully to the remote UDP - /// server (after successfully connecting to the remote UDP socket). + /// server (after successfully connecting to the remote UDP socket). /// /// # Panics /// diff --git a/src/console/profiling.rs b/src/console/profiling.rs index 52e11913f..d77e55966 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -12,9 +12,9 @@ //! ``` //! //! > NOTICE: valgrind executes the program you wan to profile and waits until -//! it ends. Since the tracker is a service and does not end the profiling -//! binary accepts an arguments with the duration you want to run the tracker, -//! so that it terminates automatically after that period of time. +//! > it ends. Since the tracker is a service and does not end the profiling +//! > binary accepts an arguments with the duration you want to run the tracker, +//! > so that it terminates automatically after that period of time. //! //! # Run profiling //! @@ -81,7 +81,7 @@ //! ``` //! //! > NOTICE: We are using an specific tracker configuration for profiling that -//! removes all features except the UDP tracker and sets the logging level to `error`. +//! > removes all features except the UDP tracker and sets the logging level to `error`. //! //! Build the aquatic UDP load test command: //! diff --git a/src/core/databases/mod.rs b/src/core/databases/mod.rs index c08aed76a..e3fb9ad60 100644 --- a/src/core/databases/mod.rs +++ b/src/core/databases/mod.rs @@ -8,7 +8,7 @@ //! - [`Sqlite`](crate::core::databases::sqlite::Sqlite) //! //! > **NOTICE**: There are no database migrations. If there are any changes, -//! we will implemented them or provide a script to migrate to the new schema. +//! > we will implemented them or provide a script to migrate to the new schema. //! //! The persistent objects are: //! @@ -25,7 +25,7 @@ //! `completed` | 20 | The number of peers that have ever completed downloading the torrent associated to this entry. See [`Entry`](torrust_tracker_torrent_repository::entry::Entry) for more information. //! //! > **NOTICE**: The peer list for a torrent is not persisted. Since peer have to re-announce themselves on intervals, the data is be -//! regenerated again after some minutes. +//! > regenerated again after some minutes. //! //! # Torrent whitelist //! diff --git a/src/core/mod.rs b/src/core/mod.rs index 2b61e3031..e81ad2a94 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -219,7 +219,7 @@ //! The torrent entry has two attributes: //! //! - `completed`: which is hte number of peers that have completed downloading the torrent file/s. As they have completed downloading, -//! they have a full version of the torrent data, and they can provide the full data to other peers. That's why they are also known as "seeders". +//! they have a full version of the torrent data, and they can provide the full data to other peers. That's why they are also known as "seeders". //! - `peers`: an indexed and orderer list of peer for the torrent. Each peer contains the data received from the peer in the `announce` request. //! //! The [`torrent`] module not only contains the original data obtained from peer via `announce` requests, it also contains @@ -401,7 +401,7 @@ //! - `scrapes_handled`: number of `scrape` handled requests by the tracker //! //! > **NOTICE**: as the HTTP tracker does not have an specific `connection` request like the UDP tracker, `connections_handled` are -//! increased on every `announce` and `scrape` requests. +//! > increased on every `announce` and `scrape` requests. //! //! The tracker exposes an event sender API that allows the tracker users to send events. When a higher application service handles a //! `connection` , `announce` or `scrape` requests, it notifies the `Tracker` by sending statistics events. @@ -467,8 +467,8 @@ use crate::CurrentClock; /// authentication and other services. /// /// > **NOTICE**: the `Tracker` is not responsible for handling the network layer. -/// Typically, the `Tracker` is used by a higher application service that handles -/// the network layer. +/// > Typically, the `Tracker` is used by a higher application service that handles +/// > the network layer. pub struct Tracker { announce_policy: AnnouncePolicy, /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite) diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index 286a7e047..38311864b 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -20,10 +20,10 @@ //! //! - The number of peers that have completed downloading the torrent since the tracker started collecting metrics. //! - The number of peers that have completed downloading the torrent and are still active, that means they are actively participating in the network, -//! by announcing themselves periodically to the tracker. Since they have completed downloading they have a full copy of the torrent data. Peers with a -//! full copy of the data are called "seeders". +//! by announcing themselves periodically to the tracker. Since they have completed downloading they have a full copy of the torrent data. Peers with a +//! full copy of the data are called "seeders". //! - The number of peers that have NOT completed downloading the torrent and are still active, that means they are actively participating in the network. -//! Peer that don not have a full copy of the torrent data are called "leechers". +//! Peer that don not have a full copy of the torrent data are called "leechers". //! use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; diff --git a/src/lib.rs b/src/lib.rs index 6fd5da15f..39d0b5b3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,8 +103,8 @@ //! ``` //! //! > **NOTICE**: those are the commands for `Ubuntu`. If you are using a -//! different OS, you will need to install the equivalent packages. Please -//! refer to the documentation of your OS. +//! > different OS, you will need to install the equivalent packages. Please +//! > refer to the documentation of your OS. //! //! With the default configuration you will need to create the `storage` directory: //! @@ -231,7 +231,7 @@ //! will use `./share/default/config/tracker.development.sqlite3.toml`. //! //! > IMPORTANT: Every time you change the configuration you need to restart the -//! service. +//! > service. //! //! # Usage //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index ef37026fe..47d40c654 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -1,7 +1,7 @@ //! The tracker REST API with all its versions. //! //! > **NOTICE**: This API should not be exposed directly to the internet, it is -//! intended for internal use only. +//! > intended for internal use only. //! //! Endpoints for the latest API: [v1]. //! @@ -124,16 +124,16 @@ //! ``` //! //! > **NOTICE**: If you are using a reverse proxy like NGINX, you can skip this -//! step and use NGINX for the SSL instead. See -//! [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) +//! > step and use NGINX for the SSL instead. See +//! > [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) //! //! > **NOTICE**: You can generate a self-signed certificate for localhost using -//! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). -//! That's particularly useful for testing purposes. Once you have the certificate -//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) -//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) -//! options in the configuration file with the paths to the certificate -//! (`localhost.crt`) and key (`localhost.key`) files. +//! > OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). +//! > That's particularly useful for testing purposes. Once you have the certificate +//! > you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) +//! > and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) +//! > options in the configuration file with the paths to the certificate +//! > (`localhost.crt`) and key (`localhost.key`) files. //! //! # Versioning //! @@ -153,8 +153,8 @@ //! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). //! //! > **NOTICE**: we are using [curl](https://curl.se/) in the API examples. -//! And you have to use quotes around the URL in order to avoid unexpected -//! errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. +//! > And you have to use quotes around the URL in order to avoid unexpected +//! > errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. pub mod routes; pub mod server; pub mod v1; diff --git a/src/servers/apis/v1/context/auth_key/handlers.rs b/src/servers/apis/v1/context/auth_key/handlers.rs index a6c8bf812..792d9507e 100644 --- a/src/servers/apis/v1/context/auth_key/handlers.rs +++ b/src/servers/apis/v1/context/auth_key/handlers.rs @@ -46,7 +46,7 @@ pub async fn generate_auth_key_handler(State(tracker): State>, Path /// /// - `POST /api/v1/key/120`. It will generate a new key valid for two minutes. /// - `DELETE /api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. It will delete the -/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. +/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. /// /// > **NOTICE**: this may change in the future, in the [API v2](https://github.com/torrust/torrust-tracker/issues/144). #[derive(Deserialize)] diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index 11bc8a43f..330249b58 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -51,9 +51,9 @@ //! ``` //! //! > **NOTICE**: `valid_until` and `expiry_time` represent the same time. -//! `valid_until` is the number of seconds since the Unix epoch -//! ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` -//! is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). +//! > `valid_until` is the number of seconds since the Unix epoch +//! > ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` +//! > is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). //! //! **Resource** //! @@ -96,8 +96,8 @@ //! ``` //! //! > **NOTICE**: a `500` status code will be returned and the body is not a -//! valid JSON. It's a text body containing the serialized-to-display error -//! message. +//! > valid JSON. It's a text body containing the serialized-to-display error +//! > message. //! //! # Reload authentication keys //! diff --git a/src/servers/apis/v1/context/torrent/resources/torrent.rs b/src/servers/apis/v1/context/torrent/resources/torrent.rs index 2f1ace5c9..0d65b3eb6 100644 --- a/src/servers/apis/v1/context/torrent/resources/torrent.rs +++ b/src/servers/apis/v1/context/torrent/resources/torrent.rs @@ -2,8 +2,8 @@ //! //! - `Torrent` is the full torrent resource. //! - `ListItem` is a list item resource on a torrent list. `ListItem` does -//! include a `peers` field but it is always `None` in the struct and `null` in -//! the JSON response. +//! include a `peers` field but it is always `None` in the struct and `null` in +//! the JSON response. use serde::{Deserialize, Serialize}; use crate::core::services::torrent::{BasicInfo, Info}; diff --git a/src/servers/apis/v1/context/whitelist/handlers.rs b/src/servers/apis/v1/context/whitelist/handlers.rs index c88f8cc1d..32e434918 100644 --- a/src/servers/apis/v1/context/whitelist/handlers.rs +++ b/src/servers/apis/v1/context/whitelist/handlers.rs @@ -42,7 +42,7 @@ pub async fn add_torrent_to_whitelist_handler( /// /// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. /// - `500` with serialized error in debug format if the torrent couldn't be -/// removed from the whitelisted. +/// removed from the whitelisted. /// /// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#remove-a-torrent-from-the-whitelist) /// for more information about this endpoint. @@ -65,7 +65,7 @@ pub async fn remove_torrent_from_whitelist_handler( /// /// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. /// - `500` with serialized error in debug format if the torrent whitelist -/// couldn't be reloaded from the database. +/// couldn't be reloaded from the database. /// /// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#reload-the-whitelist) /// for more information about this endpoint. diff --git a/src/servers/apis/v1/context/whitelist/mod.rs b/src/servers/apis/v1/context/whitelist/mod.rs index 2bb35ef65..79da43fdc 100644 --- a/src/servers/apis/v1/context/whitelist/mod.rs +++ b/src/servers/apis/v1/context/whitelist/mod.rs @@ -11,12 +11,12 @@ //! torrents in the whitelist. The whitelist can be updated using the API. //! //! > **NOTICE**: the whitelist is only used when the tracker is configured to -//! in `listed` or `private_listed` modes. Refer to the -//! [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) -//! to know how to enable the those modes. +//! > in `listed` or `private_listed` modes. Refer to the +//! > [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) +//! > to know how to enable the those modes. //! //! > **NOTICE**: if the tracker is not running in `listed` or `private_listed` -//! modes the requests to the whitelist API will be ignored. +//! > modes the requests to the whitelist API will be ignored. //! //! # Endpoints //! diff --git a/src/servers/apis/v1/mod.rs b/src/servers/apis/v1/mod.rs index 213ee9335..372ae0ff9 100644 --- a/src/servers/apis/v1/mod.rs +++ b/src/servers/apis/v1/mod.rs @@ -12,7 +12,7 @@ //! > **NOTICE**: //! - The authentication keys are only used by the HTTP tracker. //! - The whitelist is only used when the tracker is running in `listed` or -//! `private_listed` mode. +//! `private_listed` mode. //! //! Refer to the [authentication middleware](crate::servers::apis::v1::middlewares::auth) //! for more information about the authentication process. diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index 3ef85e600..e50e3c351 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -39,7 +39,7 @@ //! **Query parameters** //! //! > **NOTICE**: you can click on the parameter name to see a full description -//! after extracting and parsing the parameter from the URL query component. +//! > after extracting and parsing the parameter from the URL query component. //! //! Parameter | Type | Description | Required | Default | Example //! ---|---|---|---|---|--- @@ -58,40 +58,40 @@ //! request for more information about the parameters. //! //! > **NOTICE**: the [BEP 03](https://www.bittorrent.org/beps/bep_0003.html) -//! defines only the `ip` and `event` parameters as optional. However, the -//! tracker assigns default values to the optional parameters if they are not -//! provided. +//! > defines only the `ip` and `event` parameters as optional. However, the +//! > tracker assigns default values to the optional parameters if they are not +//! > provided. //! //! > **NOTICE**: the `peer_addr` parameter is not part of the original -//! specification. But the peer IP was added in the -//! [UDP Tracker protocol](https://www.bittorrent.org/beps/bep_0015.html). It is -//! used to provide the peer's IP address to the tracker, but it is ignored by -//! the tracker. The tracker uses the IP address of the peer that sent the -//! request or the right-most-ip in the `X-Forwarded-For` header if the tracker -//! is behind a reverse proxy. +//! > specification. But the peer IP was added in the +//! > [UDP Tracker protocol](https://www.bittorrent.org/beps/bep_0015.html). It is +//! > used to provide the peer's IP address to the tracker, but it is ignored by +//! > the tracker. The tracker uses the IP address of the peer that sent the +//! > request or the right-most-ip in the `X-Forwarded-For` header if the tracker +//! > is behind a reverse proxy. //! //! > **NOTICE**: the maximum number of peers that the tracker can return is -//! `74`. Defined with a hardcoded const [`TORRENT_PEERS_LIMIT`](torrust_tracker_configuration::TORRENT_PEERS_LIMIT). -//! Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) -//! for more information about this limitation. +//! > `74`. Defined with a hardcoded const [`TORRENT_PEERS_LIMIT`](torrust_tracker_configuration::TORRENT_PEERS_LIMIT). +//! > Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) +//! > for more information about this limitation. //! //! > **NOTICE**: the `info_hash` parameter is NOT a `URL` encoded string param. -//! It is percent encode of the raw `info_hash` bytes (40 bytes). URL `GET` params -//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a -//! 20-byte SHA1. Check the [`percent_encoding`] -//! module to know more about the encoding. +//! > It is percent encode of the raw `info_hash` bytes (40 bytes). URL `GET` params +//! > can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! > 20-byte SHA1. Check the [`percent_encoding`] +//! > module to know more about the encoding. //! //! > **NOTICE**: the `peer_id` parameter is NOT a `URL` encoded string param. -//! It is percent encode of the raw peer ID bytes (20 bytes). URL `GET` params -//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a -//! 20-byte SHA1. Check the [`percent_encoding`] -//! module to know more about the encoding. +//! > It is percent encode of the raw peer ID bytes (20 bytes). URL `GET` params +//! > can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! > 20-byte SHA1. Check the [`percent_encoding`] +//! > module to know more about the encoding. //! //! > **NOTICE**: by default, the tracker returns the non-compact peer list when -//! no `compact` parameter is provided or is empty. The -//! [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) suggests to do the -//! opposite. The tracker should return the compact peer list by default and -//! return the non-compact peer list if the `compact` parameter is `0`. +//! > no `compact` parameter is provided or is empty. The +//! > [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) suggests to do the +//! > opposite. The tracker should return the compact peer list by default and +//! > return the non-compact peer list if the `compact` parameter is `0`. //! //! **Sample announce URL** //! @@ -223,7 +223,7 @@ //! [`info_hash`](crate::servers::http::v1::requests::scrape::Scrape::info_hashes) | percent encoded of 20-byte array | The `Info Hash` of the torrent. | Yes | No | `%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00` //! //! > **NOTICE**: you can scrape multiple torrents at the same time by passing -//! multiple `info_hash` parameters. +//! > multiple `info_hash` parameters. //! //! Refer to the [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) //! request for more information about the parameters. @@ -238,7 +238,7 @@ //! `info_hash` parameters: `info_hash=%81%00%0...00%00%00&info_hash=%82%00%0...00%00%00` //! //! > **NOTICE**: the maximum number of torrents you can scrape at the same time -//! is `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! > is `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). //! //! **Sample response** //! diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index a68f7d16c..33e20a84e 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -27,7 +27,7 @@ use crate::servers::signals::{graceful_shutdown, Halted}; /// /// - The channel to send the shutdown signal to the server is closed. /// - The task to shutdown the server on the spawned server failed to execute to -/// completion. +/// completion. #[derive(Debug)] pub enum Error { Error(String), @@ -107,8 +107,8 @@ pub type RunningHttpServer = HttpServer; /// server but always keeping the same configuration. /// /// > **NOTICE**: if the configurations changes after running the server it will -/// reset to the initial value after stopping the server. This struct is not -/// intended to persist configurations between runs. +/// > reset to the initial value after stopping the server. This struct is not +/// > intended to persist configurations between runs. #[allow(clippy::module_name_repetitions)] pub struct HttpServer { /// The state of the server: `running` or `stopped`. diff --git a/src/servers/http/v1/extractors/authentication_key.rs b/src/servers/http/v1/extractors/authentication_key.rs index b8d3e7d50..985e32371 100644 --- a/src/servers/http/v1/extractors/authentication_key.rs +++ b/src/servers/http/v1/extractors/authentication_key.rs @@ -39,9 +39,9 @@ //! ``` //! //! > **NOTICE**: the returned HTTP status code is always `200` for authentication errors. -//! Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) -//! nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html) -//! specifications specify any HTTP status code for authentication errors. +//! > Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! > nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html) +//! > specifications specify any HTTP status code for authentication errors. use std::panic::Location; use axum::async_trait; diff --git a/src/servers/http/v1/requests/announce.rs b/src/servers/http/v1/requests/announce.rs index 39a6c1846..83cc7ddf9 100644 --- a/src/servers/http/v1/requests/announce.rs +++ b/src/servers/http/v1/requests/announce.rs @@ -51,12 +51,12 @@ const COMPACT: &str = "compact"; /// ``` /// /// > **NOTICE**: The [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) -/// specifies that only the peer `IP` and `event`are optional. However, the -/// tracker defines default values for some of the mandatory params. +/// > specifies that only the peer `IP` and `event`are optional. However, the +/// > tracker defines default values for some of the mandatory params. /// /// > **NOTICE**: The struct does not contain the `IP` of the peer. It's not -/// mandatory and it's not used by the tracker. The `IP` is obtained from the -/// request itself. +/// > mandatory and it's not used by the tracker. The `IP` is obtained from the +/// > request itself. #[derive(Debug, PartialEq)] pub struct Announce { // Mandatory params diff --git a/src/servers/http/v1/responses/error.rs b/src/servers/http/v1/responses/error.rs index 1cc31ad4e..c406c797a 100644 --- a/src/servers/http/v1/responses/error.rs +++ b/src/servers/http/v1/responses/error.rs @@ -9,8 +9,8 @@ //! why the query failed, and no other keys are required."_ //! //! > **NOTICE**: error responses are bencoded and always have a `200 OK` status -//! code. The official `BitTorrent` specification does not specify the status -//! code. +//! > code. The official `BitTorrent` specification does not specify the status +//! > code. use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::Serialize; diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index c54da51a3..14641dc1d 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -26,7 +26,7 @@ const TIMEOUT: Duration = Duration::from_secs(5); /// It adds the routes to the router. /// /// > **NOTICE**: it's added a layer to get the client IP from the connection -/// info. The tracker could use the connection info to get the client IP. +/// > info. The tracker could use the connection info to get the client IP. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc, server_socket_addr: SocketAddr) -> Router { Router::new() diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index e3bef3973..253140fbc 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -24,8 +24,8 @@ use crate::core::{statistics, AnnounceData, Tracker}; /// - The number of TCP `announce` requests handled by the HTTP tracker. /// /// > **NOTICE**: as the HTTP tracker does not requires a connection request -/// like the UDP tracker, the number of TCP connections is incremented for -/// each `announce` request. +/// > like the UDP tracker, the number of TCP connections is incremented for +/// > each `announce` request. pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut peer::Peer) -> AnnounceData { let original_peer_ip = peer.peer_addr.ip(); diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index a6a40186f..bf9fbd933 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -23,8 +23,8 @@ use crate::core::{statistics, ScrapeData, Tracker}; /// - The number of TCP `scrape` requests handled by the HTTP tracker. /// /// > **NOTICE**: as the HTTP tracker does not requires a connection request -/// like the UDP tracker, the number of TCP connections is incremented for -/// each `scrape` request. +/// > like the UDP tracker, the number of TCP connections is incremented for +/// > each `scrape` request. pub async fn invoke(tracker: &Arc, info_hashes: &Vec, original_peer_ip: &IpAddr) -> ScrapeData { let scrape_data = tracker.scrape(info_hashes).await; diff --git a/src/servers/udp/connection_cookie.rs b/src/servers/udp/connection_cookie.rs index af3a28702..c15ad114c 100644 --- a/src/servers/udp/connection_cookie.rs +++ b/src/servers/udp/connection_cookie.rs @@ -46,10 +46,10 @@ //! Peer C connects at timestamp 180 slot 1 -> connection ID will be valid from timestamp 180 to 360 //! ``` //! > **NOTICE**: connection ID is always the same for a given peer -//! (socket address) and time slot. +//! > (socket address) and time slot. //! //! > **NOTICE**: connection ID will be valid for two time extents, **not two -//! minutes**. It'll be valid for the the current time extent and the next one. +//! > minutes**. It'll be valid for the the current time extent and the next one. //! //! Refer to [`Connect`](crate::servers::udp#connect) for more information about //! the connection process. @@ -62,10 +62,8 @@ //! //! ## Disadvantages //! -//! - It's not very flexible. The connection ID is only valid for a certain -//! amount of time. -//! - It's not very accurate. The connection ID is valid for more than two -//! minutes. +//! - It's not very flexible. The connection ID is only valid for a certain amount of time. +//! - It's not very accurate. The connection ID is valid for more than two minutes. use std::net::SocketAddr; use std::panic::Location; diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index fee00a0bd..4064cf041 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -31,8 +31,7 @@ use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; /// It's responsible for: /// /// - Parsing the incoming packet. -/// - Delegating the request to the correct handler depending on the request -/// type. +/// - Delegating the request to the correct handler depending on the request type. /// /// It will return an `Error` response if the request is invalid. pub(crate) async fn handle_packet(udp_request: UdpRequest, tracker: &Arc, socket: Arc) -> Response { diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index fa4e8e926..3062a4393 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -5,7 +5,7 @@ //! The UDP tracker is a simple UDP server that responds to these requests: //! //! - `Connect`: used to get a connection ID which must be provided on each -//! request in order to avoid spoofing the source address of the UDP packets. +//! request in order to avoid spoofing the source address of the UDP packets. //! - `Announce`: used to announce the presence of a peer to the tracker. //! - `Scrape`: used to get information about a torrent. //! @@ -22,10 +22,10 @@ //! for more information about the UDP tracker protocol. //! //! > **NOTICE**: [BEP-41](https://www.bittorrent.org/beps/bep_0041.html) is not -//! implemented yet. +//! > implemented yet. //! //! > **NOTICE**: we are using the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) -//! crate so requests and responses are handled by it. +//! > crate so requests and responses are handled by it. //! //! > **NOTICE**: all values are send in network byte order ([big endian](https://en.wikipedia.org/wiki/Endianness)). //! @@ -83,23 +83,23 @@ //! spoofing can be explained as follows: //! //! 1. No connection state: Unlike TCP, UDP is a connectionless protocol, -//! meaning that it does not establish a connection between two endpoints before -//! exchanging data. As a result, it is more susceptible to IP spoofing, where -//! an attacker sends packets with a forged source IP address, tricking the -//! receiver into believing that they are coming from a legitimate source. +//! meaning that it does not establish a connection between two endpoints before +//! exchanging data. As a result, it is more susceptible to IP spoofing, where +//! an attacker sends packets with a forged source IP address, tricking the +//! receiver into believing that they are coming from a legitimate source. //! //! 2. Mitigating IP spoofing: To mitigate IP spoofing in the UDP tracker -//! protocol, a connection ID is used. When a client wants to interact with a -//! tracker, it sends a "connect" request to the tracker, which, in turn, -//! responds with a unique connection ID. This connection ID must be included in -//! all subsequent requests from the client to the tracker. +//! protocol, a connection ID is used. When a client wants to interact with a +//! tracker, it sends a "connect" request to the tracker, which, in turn, +//! responds with a unique connection ID. This connection ID must be included in +//! all subsequent requests from the client to the tracker. //! //! 3. Validating requests: By requiring the connection ID, the tracker can -//! verify that the requests are coming from the same client that initially sent -//! the "connect" request. If an attacker attempts to spoof the client's IP -//! address, they would also need to know the valid connection ID to be accepted -//! by the tracker. This makes it significantly more challenging for an attacker -//! to spoof IP addresses and disrupt the P2P network. +//! verify that the requests are coming from the same client that initially sent +//! the "connect" request. If an attacker attempts to spoof the client's IP +//! address, they would also need to know the valid connection ID to be accepted +//! by the tracker. This makes it significantly more challenging for an attacker +//! to spoof IP addresses and disrupt the P2P network. //! //! There are different ways to generate a connection ID. The most common way is //! to generate a time bound secret. The secret is generated using a time based @@ -161,9 +161,9 @@ //! 8 | [`i32`](std::i64) | `connection_id` | Generated by the tracker to authenticate the client. | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` //! //! > **NOTICE**: the `connection_id` is used when further information is -//! exchanged with the tracker, to identify the client. This `connection_id` can -//! be reused for multiple requests, but if it's cached for too long, it will -//! not be valid anymore. +//! > exchanged with the tracker, to identify the client. This `connection_id` can +//! > be reused for multiple requests, but if it's cached for too long, it will +//! > not be valid anymore. //! //! > **NOTICE**: `Hex` column is a signed 2's complement. //! @@ -243,41 +243,41 @@ //! circumstances might include: //! //! 1. Network Address Translation (NAT): In cases where a peer is behind a NAT, -//! the private IP address of the peer is not directly routable over the -//! internet. The NAT device translates the private IP address to a public one -//! when sending packets to the tracker. The public IP address is what the -//! tracker sees as the source IP of the incoming request. However, if the peer -//! provides its private IP address in the announce request, the tracker can use -//! this information to facilitate communication between peers in the same -//! private network. +//! the private IP address of the peer is not directly routable over the +//! internet. The NAT device translates the private IP address to a public one +//! when sending packets to the tracker. The public IP address is what the +//! tracker sees as the source IP of the incoming request. However, if the peer +//! provides its private IP address in the announce request, the tracker can use +//! this information to facilitate communication between peers in the same +//! private network. //! //! 2. Proxy or VPN usage: If a peer uses a proxy or VPN service to connect to -//! the tracker, the source IP address seen by the tracker will be the one -//! assigned by the proxy or VPN server. In this case, if the peer provides its -//! actual IP address in the announce request, the tracker can use it to -//! establish a direct connection with other peers, bypassing the proxy or VPN -//! server. This might improve performance or help in cases where some peers -//! cannot connect to the proxy or VPN server. +//! the tracker, the source IP address seen by the tracker will be the one +//! assigned by the proxy or VPN server. In this case, if the peer provides its +//! actual IP address in the announce request, the tracker can use it to +//! establish a direct connection with other peers, bypassing the proxy or VPN +//! server. This might improve performance or help in cases where some peers +//! cannot connect to the proxy or VPN server. //! //! 3. Tracker is behind a NAT, firewall, proxy, VPN, or load balancer: In cases -//! where the tracker is behind a NAT, firewall, proxy, VPN, or load balancer, -//! the source IP address of the incoming request will be the public IP address -//! of the NAT, firewall, proxy, VPN, or load balancer. If the peer provides its -//! private IP address in the announce request, the tracker can use this -//! information to establish a direct connection with the peer. +//! where the tracker is behind a NAT, firewall, proxy, VPN, or load balancer, +//! the source IP address of the incoming request will be the public IP address +//! of the NAT, firewall, proxy, VPN, or load balancer. If the peer provides its +//! private IP address in the announce request, the tracker can use this +//! information to establish a direct connection with the peer. //! //! It's important to note that using the provided IP address can pose security //! risks, as malicious peers might spoof their IP addresses in the announce //! request to perform various types of attacks. //! //! > **NOTICE**: The current tracker behavior is to ignore the IP address -//! provided by the peer, and use the source IP address of the incoming request, -//! when the tracker is not running behind a proxy, and to use the right-most IP -//! address in the `X-Forwarded-For` header when the tracker is running behind a -//! proxy. +//! > provided by the peer, and use the source IP address of the incoming request, +//! > when the tracker is not running behind a proxy, and to use the right-most IP +//! > address in the `X-Forwarded-For` header when the tracker is running behind a +//! > proxy. //! //! > **NOTICE**: The tracker also changes the peer IP address to the tracker -//! external IP when the peer is using a loopback IP address. +//! > external IP when the peer is using a loopback IP address. //! //! **Sample announce request (UDP packet)** //! @@ -317,11 +317,11 @@ //! 101 | N bytes | | | | //! //! > **NOTICE**: bytes after offset 98 are part of the [BEP-41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html). -//! There are three options defined for byte 98: `0x0` (`EndOfOptions`), `0x1` (`NOP`) and `0x2` (`URLData`). +//! > There are three options defined for byte 98: `0x0` (`EndOfOptions`), `0x1` (`NOP`) and `0x2` (`URLData`). //! //! > **NOTICE**: `num_want` is being ignored by the tracker. Refer to -//! [issue 262](https://github.com/torrust/torrust-tracker/issues/262) for more -//! information. +//! > [issue 262](https://github.com/torrust/torrust-tracker/issues/262) for more +//! > information. //! //! **Announce request (parsed struct)** //! @@ -342,7 +342,7 @@ //! `port` | [`Port`](aquatic_udp_protocol::common::Port) | `17548` //! //! > **NOTICE**: the `peers_wanted` field is the `num_want` field in the UDP -//! packet. +//! > packet. //! //! We are using a wrapper struct for the aquatic [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest) //! struct, because we have our internal [`InfoHash`](torrust_tracker_primitives::info_hash::InfoHash) @@ -374,7 +374,7 @@ //! > **NOTICE**: `Hex` column is a signed 2's complement. //! //! > **NOTICE**: `IP address` should always be set to 0 when the peer is using -//! `IPv6`. +//! > `IPv6`. //! //! **Sample announce response (UDP packet)** //! @@ -413,7 +413,7 @@ //! ``` //! //! > **NOTICE**: there are 6 bytes per peer (4 bytes for the `IPv4` address and -//! 2 bytes for the TCP port). +//! > 2 bytes for the TCP port). //! //! UDP packet fields (`IPv4` peer list): //! @@ -433,7 +433,7 @@ //! ``` //! //! > **NOTICE**: there are 18 bytes per peer (16 bytes for the `IPv6` address and -//! 2 bytes for the TCP port). +//! > 2 bytes for the TCP port). //! //! UDP packet fields (`IPv6` peer list): //! @@ -446,7 +446,7 @@ //! > **NOTICE**: `Hex` column is a signed 2's complement. //! //! > **NOTICE**: the peer list does not include the peer that sent the announce -//! request. +//! > request. //! //! **Announce response (struct)** //! @@ -478,10 +478,10 @@ //! - [incomplete](torrust_tracker_primitives::swarm_metadata::SwarmMetadata::incomplete) //! //! > **NOTICE**: up to about 74 torrents can be scraped at once. A full scrape -//! can't be done with this protocol. This is a limitation of the UDP protocol. -//! Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). -//! Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) -//! for more information about this limitation. +//! > can't be done with this protocol. This is a limitation of the UDP protocol. +//! > Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! > Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) +//! > for more information about this limitation. //! //! #### Scrape Request //! diff --git a/src/servers/udp/peer_builder.rs b/src/servers/udp/peer_builder.rs index 104f42a73..e54a23443 100644 --- a/src/servers/udp/peer_builder.rs +++ b/src/servers/udp/peer_builder.rs @@ -14,8 +14,7 @@ use crate::CurrentClock; /// # Arguments /// /// * `announce_wrapper` - The announce request to extract the peer info from. -/// * `peer_ip` - The real IP address of the peer, not the one in the announce -/// request. +/// * `peer_ip` - The real IP address of the peer, not the one in the announce request. #[must_use] pub fn from_request(announce_wrapper: &AnnounceWrapper, peer_ip: &IpAddr) -> peer::Peer { let announce_event = match aquatic_udp_protocol::AnnounceEvent::from(announce_wrapper.announce_request.event) { diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index b02b9802d..be4c36d40 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -2,8 +2,7 @@ //! //! There are two main types in this module: //! -//! - [`UdpServer`]: a controller to -//! start and stop the server. +//! - [`UdpServer`]: a controller to start and stop the server. //! - [`Udp`]: the server launcher. //! //! The `UdpServer` is an state machine for a given configuration. This struct @@ -49,8 +48,7 @@ use crate::shared::bit_torrent::tracker::udp::MAX_PACKET_SIZE; /// /// Some errors triggered while stopping the server are: /// -/// - The [`UdpServer`] cannot send the -/// shutdown signal to the spawned UDP service thread. +/// - The [`UdpServer`] cannot send the shutdown signal to the spawned UDP service thread. #[derive(Debug)] pub enum Error { /// Any kind of error starting or stopping the server. @@ -78,8 +76,8 @@ pub type RunningUdpServer = UdpServer; /// server but always keeping the same configuration. /// /// > **NOTICE**: if the configurations changes after running the server it will -/// reset to the initial value after stopping the server. This struct is not -/// intended to persist configurations between runs. +/// > reset to the initial value after stopping the server. This struct is not +/// > intended to persist configurations between runs. #[allow(clippy::module_name_repetitions)] pub struct UdpServer { /// The state of the server: `running` or `stopped`. From 4de7793f79c4731e33e139fb26183747fac080fc Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 20 May 2024 17:22:03 +0200 Subject: [PATCH 088/217] feat: [#670] new JSON serialization for connect and error aquatic responses --- src/console/clients/udp/app.rs | 15 ++++++++++--- src/console/clients/udp/responses.rs | 32 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index f07044d09..9621cec52 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -60,7 +60,7 @@ use std::net::{SocketAddr, ToSocketAddrs}; use std::str::FromStr; use anyhow::Context; -use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Scrape}; +use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Connect, Error, Scrape}; use aquatic_udp_protocol::{Port, TransactionId}; use clap::{Parser, Subcommand}; use log::{debug, LevelFilter}; @@ -68,7 +68,7 @@ use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; use url::Url; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::{AnnounceResponseDto, ScrapeResponseDto}; +use crate::console::clients::udp::responses::{AnnounceResponseDto, ConnectResponseDto, ErrorResponseDto, ScrapeResponseDto}; const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; @@ -171,6 +171,11 @@ async fn handle_scrape(tracker_socket_addr: &SocketAddr, info_hashes: &[TorrustI fn print_response(response: Response) -> anyhow::Result<()> { match response { + Connect(response) => { + let pretty_json = serde_json::to_string_pretty(&ConnectResponseDto::from(response)) + .context("connect response JSON serialization")?; + println!("{pretty_json}"); + } AnnounceIpv4(response) => { let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) .context("announce IPv4 response JSON serialization")?; @@ -186,7 +191,11 @@ fn print_response(response: Response) -> anyhow::Result<()> { serde_json::to_string_pretty(&ScrapeResponseDto::from(response)).context("scrape response JSON serialization")?; println!("{pretty_json}"); } - _ => println!("{response:#?}"), // todo: serialize to JSON all aquatic responses. + Error(response) => { + let pretty_json = + serde_json::to_string_pretty(&ErrorResponseDto::from(response)).context("error response JSON serialization")?; + println!("{pretty_json}"); + } }; Ok(()) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 8ea1a978b..eb6b386fd 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,9 +1,24 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use aquatic_udp_protocol::{AnnounceResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; +use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; +#[derive(Serialize)] +pub struct ConnectResponseDto { + transaction_id: i32, + connection_id: i64, +} + +impl From for ConnectResponseDto { + fn from(connect: ConnectResponse) -> Self { + Self { + transaction_id: connect.transaction_id.0.into(), + connection_id: connect.connection_id.0.into(), + } + } +} + #[derive(Serialize)] pub struct AnnounceResponseDto { transaction_id: i32, @@ -68,6 +83,21 @@ impl From for ScrapeResponseDto { } } +#[derive(Serialize)] +pub struct ErrorResponseDto { + transaction_id: i32, + message: String, +} + +impl From for ErrorResponseDto { + fn from(error: ErrorResponse) -> Self { + Self { + transaction_id: error.transaction_id.0.into(), + message: error.message.to_string(), + } + } +} + #[derive(Serialize)] struct TorrentStats { seeders: i32, From 625db48761f7272d1467d62afc45e2a5d3f720eb Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 15:54:56 +0200 Subject: [PATCH 089/217] refactor: [#670] new trait for printing responses in JSON format and enum for Dto wrapper --- src/console/clients/udp/responses.rs | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index eb6b386fd..5a39a04d4 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,9 +1,46 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; +use anyhow::Context; +use aquatic_udp_protocol::Response::{self}; use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; +pub trait DtoToJson { + fn print_response(&self) -> anyhow::Result<()> + where + Self: Serialize, + { + let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; + println!("{pretty_json}"); + + Ok(()) + } +} + +#[derive(Serialize)] +pub enum ResponseDto { + Connect(ConnectResponseDto), + AnnounceIpv4(AnnounceResponseDto), + AnnounceIpv6(AnnounceResponseDto), + Scrape(ScrapeResponseDto), + Error(ErrorResponseDto), +} + +impl From for ResponseDto { + fn from(response: Response) -> Self { + match response { + Response::Connect(response) => ResponseDto::Connect(ConnectResponseDto::from(response)), + Response::AnnounceIpv4(response) => ResponseDto::AnnounceIpv4(AnnounceResponseDto::from(response)), + Response::AnnounceIpv6(response) => ResponseDto::AnnounceIpv6(AnnounceResponseDto::from(response)), + Response::Scrape(response) => ResponseDto::Scrape(ScrapeResponseDto::from(response)), + Response::Error(response) => ResponseDto::Error(ErrorResponseDto::from(response)), + } + } +} + +impl DtoToJson for ResponseDto {} + #[derive(Serialize)] pub struct ConnectResponseDto { transaction_id: i32, From 08e87ca01f7bc8b8bf1ae72e1e4c442adfa3356b Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 15:59:10 +0200 Subject: [PATCH 090/217] refactor: [#670] new print_response function from trait implemented --- src/console/clients/udp/app.rs | 40 ++++------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 9621cec52..1675fd9ed 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -60,15 +60,14 @@ use std::net::{SocketAddr, ToSocketAddrs}; use std::str::FromStr; use anyhow::Context; -use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Connect, Error, Scrape}; -use aquatic_udp_protocol::{Port, TransactionId}; +use aquatic_udp_protocol::{Port, Response, TransactionId}; use clap::{Parser, Subcommand}; use log::{debug, LevelFilter}; use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; use url::Url; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::{AnnounceResponseDto, ConnectResponseDto, ErrorResponseDto, ScrapeResponseDto}; +use crate::console::clients::udp::responses::{DtoToJson, ResponseDto}; const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; @@ -117,7 +116,8 @@ pub async fn run() -> anyhow::Result<()> { } => handle_scrape(&tracker_socket_addr, &info_hashes).await?, }; - print_response(response) + let response_dto: ResponseDto = response.into(); + response_dto.print_response() } fn setup_logging(level: LevelFilter) { @@ -169,38 +169,6 @@ async fn handle_scrape(tracker_socket_addr: &SocketAddr, info_hashes: &[TorrustI .await } -fn print_response(response: Response) -> anyhow::Result<()> { - match response { - Connect(response) => { - let pretty_json = serde_json::to_string_pretty(&ConnectResponseDto::from(response)) - .context("connect response JSON serialization")?; - println!("{pretty_json}"); - } - AnnounceIpv4(response) => { - let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) - .context("announce IPv4 response JSON serialization")?; - println!("{pretty_json}"); - } - AnnounceIpv6(response) => { - let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) - .context("announce IPv6 response JSON serialization")?; - println!("{pretty_json}"); - } - Scrape(response) => { - let pretty_json = - serde_json::to_string_pretty(&ScrapeResponseDto::from(response)).context("scrape response JSON serialization")?; - println!("{pretty_json}"); - } - Error(response) => { - let pretty_json = - serde_json::to_string_pretty(&ErrorResponseDto::from(response)).context("error response JSON serialization")?; - println!("{pretty_json}"); - } - }; - - Ok(()) -} - fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result { debug!("Tracker socket address: {tracker_socket_addr_str:#?}"); From 74f4cb0aaf5de8ea0e9c09833f2e98f27e80ade3 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 17:29:17 +0200 Subject: [PATCH 091/217] refactor: [#670] added error message for pint_response function --- src/console/clients/udp/responses.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 5a39a04d4..83e4da506 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -7,6 +7,10 @@ use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv use serde::Serialize; pub trait DtoToJson { + /// # Errors + /// + /// Will return an error if serialization fails. + /// fn print_response(&self) -> anyhow::Result<()> where Self: Serialize, From 5a529cc4c8676177820a8e3770c49573f152a479 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 00:43:25 +0200 Subject: [PATCH 092/217] refactor: [#670] new mod for responses logic and refactors to json serialization trait --- src/console/clients/udp/app.rs | 9 +++++-- .../udp/{responses.rs => responses/dto.rs} | 19 --------------- src/console/clients/udp/responses/json.rs | 24 +++++++++++++++++++ src/console/clients/udp/responses/mod.rs | 2 ++ 4 files changed, 33 insertions(+), 21 deletions(-) rename src/console/clients/udp/{responses.rs => responses/dto.rs} (90%) create mode 100644 src/console/clients/udp/responses/json.rs create mode 100644 src/console/clients/udp/responses/mod.rs diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 1675fd9ed..b4d08b26d 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -67,7 +67,8 @@ use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; use url::Url; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::{DtoToJson, ResponseDto}; +use crate::console::clients::udp::responses::dto::ResponseDto; +use crate::console::clients::udp::responses::json::ToJson; const ASSIGNED_BY_OS: u16 = 0; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; @@ -117,7 +118,11 @@ pub async fn run() -> anyhow::Result<()> { }; let response_dto: ResponseDto = response.into(); - response_dto.print_response() + let response_json = response_dto.to_json_string()?; + + print!("{response_json}"); + + Ok(()) } fn setup_logging(level: LevelFilter) { diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses/dto.rs similarity index 90% rename from src/console/clients/udp/responses.rs rename to src/console/clients/udp/responses/dto.rs index 83e4da506..989231061 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses/dto.rs @@ -1,27 +1,10 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use anyhow::Context; use aquatic_udp_protocol::Response::{self}; use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; -pub trait DtoToJson { - /// # Errors - /// - /// Will return an error if serialization fails. - /// - fn print_response(&self) -> anyhow::Result<()> - where - Self: Serialize, - { - let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; - println!("{pretty_json}"); - - Ok(()) - } -} - #[derive(Serialize)] pub enum ResponseDto { Connect(ConnectResponseDto), @@ -43,8 +26,6 @@ impl From for ResponseDto { } } -impl DtoToJson for ResponseDto {} - #[derive(Serialize)] pub struct ConnectResponseDto { transaction_id: i32, diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs new file mode 100644 index 000000000..1e25acac2 --- /dev/null +++ b/src/console/clients/udp/responses/json.rs @@ -0,0 +1,24 @@ +use anyhow::Context; +use serde::Serialize; + +use super::dto::ResponseDto; + +pub trait ToJson { + /// + /// Returns a string with the JSON serialized version of the response + /// + /// # Errors + /// + /// Will return an error if serialization fails. + /// + fn to_json_string(&self) -> anyhow::Result + where + Self: Serialize, + { + let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; + + Ok(pretty_json) + } +} + +impl ToJson for ResponseDto {} diff --git a/src/console/clients/udp/responses/mod.rs b/src/console/clients/udp/responses/mod.rs new file mode 100644 index 000000000..e6d2e5e51 --- /dev/null +++ b/src/console/clients/udp/responses/mod.rs @@ -0,0 +1,2 @@ +pub mod dto; +pub mod json; From 32416ee2239942891896260e9dfc4e0bf31d9ad1 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 12:32:35 +0200 Subject: [PATCH 093/217] refactor: [#670] changed DTOs and variable names --- src/console/clients/udp/app.rs | 6 ++-- src/console/clients/udp/responses/dto.rs | 42 +++++++++++------------ src/console/clients/udp/responses/json.rs | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index b4d08b26d..d2c986cd9 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -67,7 +67,7 @@ use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; use url::Url; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::dto::ResponseDto; +use crate::console::clients::udp::responses::dto::SerializableResponse; use crate::console::clients::udp::responses::json::ToJson; const ASSIGNED_BY_OS: u16 = 0; @@ -117,8 +117,8 @@ pub async fn run() -> anyhow::Result<()> { } => handle_scrape(&tracker_socket_addr, &info_hashes).await?, }; - let response_dto: ResponseDto = response.into(); - let response_json = response_dto.to_json_string()?; + let response: SerializableResponse = response.into(); + let response_json = response.to_json_string()?; print!("{response_json}"); diff --git a/src/console/clients/udp/responses/dto.rs b/src/console/clients/udp/responses/dto.rs index 989231061..93320b0f7 100644 --- a/src/console/clients/udp/responses/dto.rs +++ b/src/console/clients/udp/responses/dto.rs @@ -6,33 +6,33 @@ use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv use serde::Serialize; #[derive(Serialize)] -pub enum ResponseDto { - Connect(ConnectResponseDto), - AnnounceIpv4(AnnounceResponseDto), - AnnounceIpv6(AnnounceResponseDto), - Scrape(ScrapeResponseDto), - Error(ErrorResponseDto), +pub enum SerializableResponse { + Connect(ConnectSerializableResponse), + AnnounceIpv4(AnnounceSerializableResponse), + AnnounceIpv6(AnnounceSerializableResponse), + Scrape(ScrapeSerializableResponse), + Error(ErrorSerializableResponse), } -impl From for ResponseDto { +impl From for SerializableResponse { fn from(response: Response) -> Self { match response { - Response::Connect(response) => ResponseDto::Connect(ConnectResponseDto::from(response)), - Response::AnnounceIpv4(response) => ResponseDto::AnnounceIpv4(AnnounceResponseDto::from(response)), - Response::AnnounceIpv6(response) => ResponseDto::AnnounceIpv6(AnnounceResponseDto::from(response)), - Response::Scrape(response) => ResponseDto::Scrape(ScrapeResponseDto::from(response)), - Response::Error(response) => ResponseDto::Error(ErrorResponseDto::from(response)), + Response::Connect(response) => SerializableResponse::Connect(ConnectSerializableResponse::from(response)), + Response::AnnounceIpv4(response) => SerializableResponse::AnnounceIpv4(AnnounceSerializableResponse::from(response)), + Response::AnnounceIpv6(response) => SerializableResponse::AnnounceIpv6(AnnounceSerializableResponse::from(response)), + Response::Scrape(response) => SerializableResponse::Scrape(ScrapeSerializableResponse::from(response)), + Response::Error(response) => SerializableResponse::Error(ErrorSerializableResponse::from(response)), } } } #[derive(Serialize)] -pub struct ConnectResponseDto { +pub struct ConnectSerializableResponse { transaction_id: i32, connection_id: i64, } -impl From for ConnectResponseDto { +impl From for ConnectSerializableResponse { fn from(connect: ConnectResponse) -> Self { Self { transaction_id: connect.transaction_id.0.into(), @@ -42,7 +42,7 @@ impl From for ConnectResponseDto { } #[derive(Serialize)] -pub struct AnnounceResponseDto { +pub struct AnnounceSerializableResponse { transaction_id: i32, announce_interval: i32, leechers: i32, @@ -50,7 +50,7 @@ pub struct AnnounceResponseDto { peers: Vec, } -impl From> for AnnounceResponseDto { +impl From> for AnnounceSerializableResponse { fn from(announce: AnnounceResponse) -> Self { Self { transaction_id: announce.fixed.transaction_id.0.into(), @@ -66,7 +66,7 @@ impl From> for AnnounceResponseDto { } } -impl From> for AnnounceResponseDto { +impl From> for AnnounceSerializableResponse { fn from(announce: AnnounceResponse) -> Self { Self { transaction_id: announce.fixed.transaction_id.0.into(), @@ -83,12 +83,12 @@ impl From> for AnnounceResponseDto { } #[derive(Serialize)] -pub struct ScrapeResponseDto { +pub struct ScrapeSerializableResponse { transaction_id: i32, torrent_stats: Vec, } -impl From for ScrapeResponseDto { +impl From for ScrapeSerializableResponse { fn from(scrape: ScrapeResponse) -> Self { Self { transaction_id: scrape.transaction_id.0.into(), @@ -106,12 +106,12 @@ impl From for ScrapeResponseDto { } #[derive(Serialize)] -pub struct ErrorResponseDto { +pub struct ErrorSerializableResponse { transaction_id: i32, message: String, } -impl From for ErrorResponseDto { +impl From for ErrorSerializableResponse { fn from(error: ErrorResponse) -> Self { Self { transaction_id: error.transaction_id.0.into(), diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs index 1e25acac2..74558c8f5 100644 --- a/src/console/clients/udp/responses/json.rs +++ b/src/console/clients/udp/responses/json.rs @@ -1,7 +1,7 @@ use anyhow::Context; use serde::Serialize; -use super::dto::ResponseDto; +use super::dto::SerializableResponse; pub trait ToJson { /// @@ -21,4 +21,4 @@ pub trait ToJson { } } -impl ToJson for ResponseDto {} +impl ToJson for SerializableResponse {} From 0157d96cd29bc79f1396d34088f6d109d6a59d61 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 13:16:47 +0200 Subject: [PATCH 094/217] refactor:[#670] fix clippy errors --- src/console/clients/udp/responses/json.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs index 74558c8f5..5d2bd6b89 100644 --- a/src/console/clients/udp/responses/json.rs +++ b/src/console/clients/udp/responses/json.rs @@ -3,6 +3,7 @@ use serde::Serialize; use super::dto::SerializableResponse; +#[allow(clippy::module_name_repetitions)] pub trait ToJson { /// /// Returns a string with the JSON serialized version of the response From f5d843b29e828c712845405a927c07ad867aea3c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 07:50:42 +0100 Subject: [PATCH 095/217] docs: add benchmarking to torrent repo README --- packages/torrent-repository/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/torrent-repository/README.md b/packages/torrent-repository/README.md index 98d7d922b..ffc71f1d7 100644 --- a/packages/torrent-repository/README.md +++ b/packages/torrent-repository/README.md @@ -1,6 +1,27 @@ -# Torrust Tracker Configuration +# Torrust Tracker Torrent Repository -A library to provide torrent repository to the [Torrust Tracker](https://github.com/torrust/torrust-tracker). +A library to provide a torrent repository to the [Torrust Tracker](https://github.com/torrust/torrust-tracker). + +## Benchmarking + +```console +cargo bench -p torrust-tracker-torrent-repository +``` + +Example partial output: + +```output + Running benches/repository_benchmark.rs (target/release/deps/repository_benchmark-a9b0013c8d09c3c3) +add_one_torrent/RwLockStd + time: [63.057 ns 63.242 ns 63.506 ns] +Found 12 outliers among 100 measurements (12.00%) + 2 (2.00%) low severe + 2 (2.00%) low mild + 2 (2.00%) high mild + 6 (6.00%) high severe +add_one_torrent/RwLockStdMutexStd + time: [62.505 ns 63.077 ns 63.817 ns] +``` ## Documentation From 6e87d3e1a37d94fd3886a7420214a6e4746c7215 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 08:02:30 +0100 Subject: [PATCH 096/217] chore(deps): update dependencies ```output cargo update Updating crates.io index Locking 23 packages to latest compatible versions Updating anstyle-query v1.0.3 -> v1.1.0 Updating async-io v2.3.2 -> v2.3.3 Updating async-lock v3.3.0 -> v3.4.0 Updating borsh v1.5.0 -> v1.5.1 Updating borsh-derive v1.5.0 -> v1.5.1 Updating cc v1.0.98 -> v1.0.99 Updating cfg_aliases v0.1.1 -> v0.2.1 Updating clap v4.5.4 -> v4.5.6 Updating clap_builder v4.5.2 -> v4.5.6 Updating clap_derive v4.5.4 -> v4.5.5 Updating clap_lex v0.7.0 -> v0.7.1 Removing event-listener v4.0.3 Removing event-listener-strategy v0.4.0 Updating piper v0.2.2 -> v0.2.3 Updating polling v3.7.0 -> v3.7.1 Updating proc-macro2 v1.0.84 -> v1.0.85 Updating regex v1.10.4 -> v1.10.5 Updating regex-automata v0.4.6 -> v0.4.7 Updating regex-syntax v0.8.3 -> v0.8.4 Updating rstest v0.20.0 -> v0.21.0 Updating rstest_macros v0.20.0 -> v0.21.0 Updating toml v0.8.13 -> v0.8.14 Updating toml_edit v0.22.13 -> v0.22.14 Updating utf8parse v0.2.1 -> v0.2.2 Updating winnow v0.6.9 -> v0.6.13 ``` --- Cargo.lock | 131 ++++++++++++++++++++++------------------------------- 1 file changed, 55 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e08b1b4d..d3f5766d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -206,7 +206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener-strategy 0.5.2", + "event-listener-strategy", "futures-core", "pin-project-lite", ] @@ -248,8 +248,8 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.3.3", + "async-lock 3.4.0", "blocking", "futures-lite 2.3.0", "once_cell", @@ -278,17 +278,17 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.0", + "polling 3.7.1", "rustix 0.38.34", "slab", "tracing", @@ -306,12 +306,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.3.1", + "event-listener-strategy", "pin-project-lite", ] @@ -617,9 +617,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", "cfg_aliases", @@ -627,9 +627,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", @@ -738,9 +738,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", @@ -764,9 +764,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" dependencies = [ "clap_builder", "clap_derive", @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" dependencies = [ "anstream", "anstyle", @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -855,9 +855,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "cmake" @@ -1211,17 +1211,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - [[package]] name = "event-listener" version = "5.3.1" @@ -1233,16 +1222,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - [[package]] name = "event-listener-strategy" version = "0.5.2" @@ -2609,9 +2588,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +checksum = "ae1d5c74c9876f070d3e8fd503d748c7d974c3e48da8f41350fa5222ef9b4391" dependencies = [ "atomic-waker", "fastrand 2.1.0", @@ -2670,9 +2649,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", @@ -2766,9 +2745,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -2925,9 +2904,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2937,9 +2916,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -2948,9 +2927,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "relative-path" @@ -3064,9 +3043,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27059f51958c5f8496a6f79511e7c0ac396dd815dc8894e9b6e2efb5779cf6f0" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" dependencies = [ "futures", "futures-timer", @@ -3076,9 +3055,9 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6132d64df104c0b3ea7a6ad7766a43f587bd773a4a9cf4cd59296d426afaf3a" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" dependencies = [ "cfg-if", "glob", @@ -3809,14 +3788,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.13", + "toml_edit 0.22.14", ] [[package]] @@ -3852,15 +3831,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.9", + "winnow 0.6.13", ] [[package]] @@ -4169,9 +4148,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -4498,9 +4477,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.9" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] From 3ccc0e41599dff371e4f8bc49eaa4d972b2f3627 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 15:12:08 +0100 Subject: [PATCH 097/217] chore(deps): add cargo dependency tracing We will move from `log` to `tracing` crate. --- Cargo.lock | 82 +++++++++++++++++++++++++++++++ Cargo.toml | 1 + packages/located-error/Cargo.toml | 1 + 3 files changed, 84 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d3f5766d5..ab7512536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2339,6 +2339,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.5" @@ -2448,6 +2458,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.0" @@ -3441,6 +3457,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3668,6 +3693,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -3899,6 +3934,7 @@ dependencies = [ "tower-http", "trace", "tracing", + "tracing-subscriber", "url", "uuid", "zerocopy", @@ -3943,6 +3979,7 @@ version = "3.0.0-alpha.12-develop" dependencies = [ "log", "thiserror", + "tracing", ] [[package]] @@ -4074,6 +4111,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4162,6 +4238,12 @@ dependencies = [ "rand", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index 5183c6067..6166831e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } trace = "0" tracing = "0" +tracing-subscriber = { version = "0.3.18", features = ["json"] } url = "2" uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" diff --git a/packages/located-error/Cargo.toml b/packages/located-error/Cargo.toml index fa3d1d76d..f34f9bc88 100644 --- a/packages/located-error/Cargo.toml +++ b/packages/located-error/Cargo.toml @@ -16,6 +16,7 @@ version.workspace = true [dependencies] log = { version = "0", features = ["release_max_level_info"] } +tracing = "0.1.40" [dev-dependencies] thiserror = "1" From 6e06b2e7aaeacb5307e494e28c4f45f08a96892b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 15:30:25 +0100 Subject: [PATCH 098/217] refactor: [#884] move from log to tracing crate --- packages/located-error/src/lib.rs | 2 +- src/app.rs | 2 +- src/bootstrap/jobs/health_check_api.rs | 2 +- src/bootstrap/jobs/http_tracker.rs | 2 +- src/bootstrap/jobs/mod.rs | 2 +- src/bootstrap/jobs/torrent_cleanup.rs | 2 +- src/bootstrap/jobs/tracker_apis.rs | 2 +- src/bootstrap/jobs/udp_tracker.rs | 2 +- src/bootstrap/logging.rs | 73 ++++++++++++------- src/console/ci/e2e/docker.rs | 2 +- src/console/ci/e2e/logs_parser.rs | 34 ++++----- src/console/ci/e2e/runner.rs | 27 ++----- src/console/ci/e2e/tracker_checker.rs | 2 +- src/console/ci/e2e/tracker_container.rs | 2 +- src/console/clients/checker/app.rs | 27 ++----- src/console/clients/checker/checks/http.rs | 2 +- src/console/clients/checker/checks/udp.rs | 2 +- src/console/clients/udp/app.rs | 27 ++----- src/console/clients/udp/checker.rs | 2 +- src/console/profiling.rs | 2 +- src/core/auth.rs | 2 +- src/core/databases/mysql.rs | 2 +- src/core/mod.rs | 2 +- src/core/statistics.rs | 2 +- src/main.rs | 2 +- src/servers/apis/server.rs | 2 +- .../apis/v1/context/torrent/handlers.rs | 2 +- src/servers/health_check_api/server.rs | 3 +- src/servers/http/server.rs | 2 +- src/servers/http/v1/handlers/announce.rs | 2 +- src/servers/http/v1/handlers/scrape.rs | 2 +- src/servers/registar.rs | 2 +- src/servers/signals.rs | 2 +- src/servers/udp/handlers.rs | 2 +- src/servers/udp/server.rs | 2 +- src/shared/bit_torrent/tracker/udp/client.rs | 2 +- tests/servers/health_check_api/environment.rs | 2 +- 37 files changed, 112 insertions(+), 141 deletions(-) diff --git a/packages/located-error/src/lib.rs b/packages/located-error/src/lib.rs index 49e135600..bfd4d4a86 100644 --- a/packages/located-error/src/lib.rs +++ b/packages/located-error/src/lib.rs @@ -33,7 +33,7 @@ use std::error::Error; use std::panic::Location; use std::sync::Arc; -use log::debug; +use tracing::debug; pub type DynError = Arc; diff --git a/src/app.rs b/src/app.rs index fcb01a696..b41f4098e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,9 +23,9 @@ //! - Tracker REST API: the tracker API can be enabled/disabled. use std::sync::Arc; -use log::warn; use tokio::task::JoinHandle; use torrust_tracker_configuration::Configuration; +use tracing::warn; use crate::bootstrap::jobs::{health_check_api, http_tracker, torrent_cleanup, tracker_apis, udp_tracker}; use crate::servers::registar::Registar; diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index fdedaa3e9..c22a4cf95 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -14,10 +14,10 @@ //! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) //! for the API configuration options. -use log::info; use tokio::sync::oneshot; use tokio::task::JoinHandle; use torrust_tracker_configuration::HealthCheckApi; +use tracing::info; use super::Started; use crate::servers::health_check_api::server; diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 9ae8995fc..e9eb6bc16 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -14,9 +14,9 @@ use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; +use tracing::info; use super::make_rust_tls; use crate::core; diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index e20d243c6..316e5746c 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -89,10 +89,10 @@ use std::panic::Location; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::info; use thiserror::Error; use torrust_tracker_configuration::TslConfig; use torrust_tracker_located_error::{DynError, LocatedError}; +use tracing::info; /// Error returned by the Bootstrap Process. #[derive(Error, Debug)] diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs index bd3b2e332..992e7e644 100644 --- a/src/bootstrap/jobs/torrent_cleanup.rs +++ b/src/bootstrap/jobs/torrent_cleanup.rs @@ -13,9 +13,9 @@ use std::sync::Arc; use chrono::Utc; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::v1::core::Core; +use tracing::info; use crate::core; diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 834574edb..3c1f13255 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -24,9 +24,9 @@ use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::info; use tokio::task::JoinHandle; use torrust_tracker_configuration::{AccessTokens, HttpApi}; +use tracing::info; use super::make_rust_tls; use crate::core; diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index 853cb7461..2c09e6de2 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -8,9 +8,9 @@ //! > for the configuration options. use std::sync::Arc; -use log::debug; use tokio::task::JoinHandle; use torrust_tracker_configuration::UdpTracker; +use tracing::debug; use crate::core; use crate::servers::registar::ServiceRegistrationForm; diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 5c7e93811..914ede0c4 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -12,55 +12,72 @@ //! Refer to the [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) to know how to change log settings. use std::sync::Once; -use log::{info, LevelFilter}; use torrust_tracker_configuration::{Configuration, LogLevel}; +use tracing::info; +use tracing::level_filters::LevelFilter; static INIT: Once = Once::new(); /// It redirects the log info to the standard output with the log level defined in the configuration pub fn setup(cfg: &Configuration) { - let level = config_level_or_default(&cfg.core.log_level); + let tracing_level = config_level_or_default(&cfg.core.log_level); - if level == log::LevelFilter::Off { + if tracing_level == LevelFilter::OFF { return; } INIT.call_once(|| { - stdout_config(level); + tracing_stdout_init(tracing_level, &TraceStyle::Default); }); } fn config_level_or_default(log_level: &Option) -> LevelFilter { match log_level { - None => log::LevelFilter::Info, + None => LevelFilter::INFO, Some(level) => match level { - LogLevel::Off => LevelFilter::Off, - LogLevel::Error => LevelFilter::Error, - LogLevel::Warn => LevelFilter::Warn, - LogLevel::Info => LevelFilter::Info, - LogLevel::Debug => LevelFilter::Debug, - LogLevel::Trace => LevelFilter::Trace, + LogLevel::Off => LevelFilter::OFF, + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::TRACE, }, } } -fn stdout_config(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } +fn tracing_stdout_init(filter: LevelFilter, style: &TraceStyle) { + let builder = tracing_subscriber::fmt().with_max_level(filter); + + let () = match style { + TraceStyle::Default => builder.init(), + TraceStyle::Pretty(display_filename) => builder.pretty().with_file(*display_filename).init(), + TraceStyle::Compact => builder.compact().init(), + TraceStyle::Json => builder.json().init(), + }; info!("logging initialized."); } + +#[derive(Debug)] +pub enum TraceStyle { + Default, + Pretty(bool), + Compact, + Json, +} + +impl std::fmt::Display for TraceStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let style = match self { + TraceStyle::Default => "Default Style", + TraceStyle::Pretty(path) => match path { + true => "Pretty Style with File Paths", + false => "Pretty Style without File Paths", + }, + TraceStyle::Compact => "Compact Style", + TraceStyle::Json => "Json Format", + }; + + f.write_str(style) + } +} diff --git a/src/console/ci/e2e/docker.rs b/src/console/ci/e2e/docker.rs index c024efbae..26b7f7708 100644 --- a/src/console/ci/e2e/docker.rs +++ b/src/console/ci/e2e/docker.rs @@ -4,7 +4,7 @@ use std::process::{Command, Output}; use std::thread::sleep; use std::time::{Duration, Instant}; -use log::{debug, info}; +use tracing::{debug, info}; /// Docker command wrapper. pub struct Docker {} diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 6d3349196..2d215a569 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,9 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "[UDP TRACKER][INFO] Starting on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "[HTTP TRACKER][INFO] Starting on: "; -const HEALTH_CHECK_PATTERN: &str = "[HEALTH CHECK API][INFO] Starting on: "; +const UDP_TRACKER_PATTERN: &str = "INFO UDP TRACKER: Starting on: udp://"; +const HTTP_TRACKER_PATTERN: &str = "INFO HTTP TRACKER: Starting on: "; +const HEALTH_CHECK_PATTERN: &str = "INFO HEALTH CHECK API: Starting on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -18,17 +18,17 @@ impl RunningServices { /// For example, from this logs: /// /// ```text - /// Loading default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-01-24T16:36:14.614898789+00:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized. - /// 2024-01-24T16:36:14.615586025+00:00 [UDP TRACKER][INFO] Starting on: udp://0.0.0.0:6969 - /// 2024-01-24T16:36:14.615623705+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled - /// 2024-01-24T16:36:14.615694484+00:00 [HTTP TRACKER][INFO] Starting on: http://0.0.0.0:7070 - /// 2024-01-24T16:36:14.615710534+00:00 [HTTP TRACKER][INFO] Started on: http://0.0.0.0:7070 - /// 2024-01-24T16:36:14.615716574+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled - /// 2024-01-24T16:36:14.615764904+00:00 [API][INFO] Starting on http://127.0.0.1:1212 - /// 2024-01-24T16:36:14.615767264+00:00 [API][INFO] Started on http://127.0.0.1:1212 - /// 2024-01-24T16:36:14.615777574+00:00 [HEALTH CHECK API][INFO] Starting on: http://127.0.0.1:1313 - /// 2024-01-24T16:36:14.615791124+00:00 [HEALTH CHECK API][INFO] Started on: http://127.0.0.1:1313 + /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... + /// 2024-06-10T14:26:10.040894Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T14:26:10.041363Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T14:26:10.041386Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:26:10.041420Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T14:26:10.041516Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T14:26:10.041521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:26:10.041611Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T14:26:10.041614Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T14:26:10.041623Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T14:26:10.041657Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -86,9 +86,9 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { let logs = "\ - [UDP TRACKER][INFO] Starting on: udp://0.0.0.0:8080\n\ - [HTTP TRACKER][INFO] Starting on: 0.0.0.0:9090\n\ - [HEALTH CHECK API][INFO] Starting on: 0.0.0.0:10010"; + INFO UDP TRACKER: Starting on: udp://0.0.0.0:8080\n\ + INFO HTTP TRACKER: Starting on: 0.0.0.0:9090\n\ + INFO HEALTH CHECK API: Starting on: 0.0.0.0:10010"; let running_services = RunningServices::parse_from_logs(logs); assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]); diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index 945a87033..c44ce464e 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -3,7 +3,8 @@ //! ```text //! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml //! ``` -use log::{debug, info, LevelFilter}; +use tracing::info; +use tracing::level_filters::LevelFilter; use super::tracker_container::TrackerContainer; use crate::console::ci::e2e::docker::RunOptions; @@ -32,7 +33,7 @@ pub struct Arguments { /// /// Will panic if it can't not perform any of the operations. pub fn run() { - setup_runner_logging(LevelFilter::Info); + tracing_stdout_init(LevelFilter::INFO); let args = parse_arguments(); @@ -76,25 +77,9 @@ pub fn run() { info!("Tracker container final state:\n{:#?}", tracker_container); } -fn setup_runner_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); +fn tracing_stdout_init(filter: LevelFilter) { + tracing_subscriber::fmt().with_max_level(filter).init(); + info!("logging initialized."); } fn parse_arguments() -> Arguments { diff --git a/src/console/ci/e2e/tracker_checker.rs b/src/console/ci/e2e/tracker_checker.rs index edc679802..b2fd7df2e 100644 --- a/src/console/ci/e2e/tracker_checker.rs +++ b/src/console/ci/e2e/tracker_checker.rs @@ -1,7 +1,7 @@ use std::io; use std::process::Command; -use log::info; +use tracing::info; /// Runs the Tracker Checker. /// diff --git a/src/console/ci/e2e/tracker_container.rs b/src/console/ci/e2e/tracker_container.rs index 5a4d11d02..0cb4fec7c 100644 --- a/src/console/ci/e2e/tracker_container.rs +++ b/src/console/ci/e2e/tracker_container.rs @@ -1,8 +1,8 @@ use std::time::Duration; -use log::{debug, error, info}; use rand::distributions::Alphanumeric; use rand::Rng; +use tracing::{debug, error, info}; use super::docker::{RunOptions, RunningContainer}; use super::logs_parser::RunningServices; diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index 82ea800d0..84802688d 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -17,7 +17,8 @@ use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; -use log::{debug, LevelFilter}; +use tracing::info; +use tracing::level_filters::LevelFilter; use super::config::Configuration; use super::console::Console; @@ -40,7 +41,7 @@ struct Args { /// /// Will return an error if the configuration was not provided. pub async fn run() -> Result> { - setup_logging(LevelFilter::Info); + tracing_stdout_init(LevelFilter::INFO); let args = Args::parse(); @@ -56,25 +57,9 @@ pub async fn run() -> Result> { Ok(service.run_checks().await) } -fn setup_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); +fn tracing_stdout_init(filter: LevelFilter) { + tracing_subscriber::fmt().with_max_level(filter).init(); + info!("logging initialized."); } fn setup_config(args: Args) -> Result { diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index e526b5e57..57f8c3015 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,8 +1,8 @@ use std::str::FromStr; -use log::debug; use reqwest::Url as ServiceUrl; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use url::Url; use super::structs::{CheckerOutput, Status}; diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 6458190d4..072aa5ca7 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -2,8 +2,8 @@ use std::net::SocketAddr; use aquatic_udp_protocol::{Port, TransactionId}; use hex_literal::hex; -use log::debug; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use crate::console::clients::checker::checks::structs::{CheckerOutput, Status}; use crate::console::clients::checker::service::{CheckError, CheckResult}; diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index d2c986cd9..c780157f4 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -62,8 +62,9 @@ use std::str::FromStr; use anyhow::Context; use aquatic_udp_protocol::{Port, Response, TransactionId}; use clap::{Parser, Subcommand}; -use log::{debug, LevelFilter}; use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; +use tracing::level_filters::LevelFilter; +use tracing::{debug, info}; use url::Url; use crate::console::clients::udp::checker; @@ -102,7 +103,7 @@ enum Command { /// /// pub async fn run() -> anyhow::Result<()> { - setup_logging(LevelFilter::Info); + tracing_stdout_init(LevelFilter::INFO); let args = Args::parse(); @@ -125,25 +126,9 @@ pub async fn run() -> anyhow::Result<()> { Ok(()) } -fn setup_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); +fn tracing_stdout_init(filter: LevelFilter) { + tracing_subscriber::fmt().with_max_level(filter).init(); + info!("logging initialized."); } async fn handle_announce(tracker_socket_addr: &SocketAddr, info_hash: &TorrustInfoHash) -> anyhow::Result { diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index 37928f0df..afde63d12 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -6,9 +6,9 @@ use aquatic_udp_protocol::{ AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, ScrapeRequest, TransactionId, }; -use log::debug; use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash as TorrustInfoHash; +use tracing::debug; use crate::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient}; diff --git a/src/console/profiling.rs b/src/console/profiling.rs index d77e55966..c95354d6f 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -159,8 +159,8 @@ use std::env; use std::time::Duration; -use log::info; use tokio::time::sleep; +use tracing::info; use crate::{app, bootstrap}; diff --git a/src/core/auth.rs b/src/core/auth.rs index b5326a373..94d455d7e 100644 --- a/src/core/auth.rs +++ b/src/core/auth.rs @@ -42,7 +42,6 @@ use std::sync::Arc; use std::time::Duration; use derive_more::Display; -use log::debug; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; @@ -51,6 +50,7 @@ use torrust_tracker_clock::clock::Time; use torrust_tracker_clock::conv::convert_from_timestamp_to_datetime_utc; use torrust_tracker_located_error::{DynError, LocatedError}; use torrust_tracker_primitives::DurationSinceUnixEpoch; +use tracing::debug; use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH; use crate::CurrentClock; diff --git a/src/core/databases/mysql.rs b/src/core/databases/mysql.rs index ca95fa0b9..ebb002d31 100644 --- a/src/core/databases/mysql.rs +++ b/src/core/databases/mysql.rs @@ -3,13 +3,13 @@ use std::str::FromStr; use std::time::Duration; use async_trait::async_trait; -use log::debug; use r2d2::Pool; use r2d2_mysql::mysql::prelude::Queryable; use r2d2_mysql::mysql::{params, Opts, OptsBuilder}; use r2d2_mysql::MySqlConnectionManager; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::{DatabaseDriver, PersistentTorrents}; +use tracing::debug; use super::{Database, Error}; use crate::core::auth::{self, Key}; diff --git a/src/core/mod.rs b/src/core/mod.rs index e81ad2a94..6af28199f 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -442,7 +442,6 @@ use std::sync::Arc; use std::time::Duration; use derive_more::Constructor; -use log::debug; use tokio::sync::mpsc::error::SendError; use torrust_tracker_clock::clock::Time; use torrust_tracker_configuration::v1::core::Core; @@ -453,6 +452,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, TrackerMode}; use torrust_tracker_torrent_repository::entry::EntrySync; use torrust_tracker_torrent_repository::repository::Repository; +use tracing::debug; use self::auth::Key; use self::error::Error; diff --git a/src/core/statistics.rs b/src/core/statistics.rs index f38662cdd..d7192f5d1 100644 --- a/src/core/statistics.rs +++ b/src/core/statistics.rs @@ -20,11 +20,11 @@ use std::sync::Arc; use async_trait::async_trait; -use log::debug; #[cfg(test)] use mockall::{automock, predicate::str}; use tokio::sync::mpsc::error::SendError; use tokio::sync::{mpsc, RwLock, RwLockReadGuard}; +use tracing::debug; const CHANNEL_BUFFER_SIZE: usize = 65_535; diff --git a/src/main.rs b/src/main.rs index bd07f4a58..bad1fdb1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use log::info; use torrust_tracker::{app, bootstrap}; +use tracing::info; #[tokio::main] async fn main() { diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 57d2629ae..7c5b8983b 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -30,9 +30,9 @@ use axum_server::tls_rustls::RustlsConfig; use axum_server::Handle; use derive_more::Constructor; use futures::future::BoxFuture; -use log::{debug, error, info}; use tokio::sync::oneshot::{Receiver, Sender}; use torrust_tracker_configuration::AccessTokens; +use tracing::{debug, error, info}; use super::routes::router; use crate::bootstrap::jobs::Started; diff --git a/src/servers/apis/v1/context/torrent/handlers.rs b/src/servers/apis/v1/context/torrent/handlers.rs index 15f70c8b6..b2418c689 100644 --- a/src/servers/apis/v1/context/torrent/handlers.rs +++ b/src/servers/apis/v1/context/torrent/handlers.rs @@ -7,11 +7,11 @@ use std::sync::Arc; use axum::extract::{Path, State}; use axum::response::{IntoResponse, Response}; use axum_extra::extract::Query; -use log::debug; use serde::{de, Deserialize, Deserializer}; use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; +use tracing::debug; use super::responses::{torrent_info_response, torrent_list_response, torrent_not_known_response}; use crate::core::services::torrent::{get_torrent_info, get_torrents, get_torrents_page}; diff --git a/src/servers/health_check_api/server.rs b/src/servers/health_check_api/server.rs index 05ed605f4..f03753573 100644 --- a/src/servers/health_check_api/server.rs +++ b/src/servers/health_check_api/server.rs @@ -12,14 +12,13 @@ use axum::{Json, Router}; use axum_server::Handle; use futures::Future; use hyper::Request; -use log::debug; use serde_json::json; use tokio::sync::oneshot::{Receiver, Sender}; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; use tower_http::trace::{DefaultMakeSpan, TraceLayer}; -use tracing::{Level, Span}; +use tracing::{debug, Level, Span}; use crate::bootstrap::jobs::Started; use crate::servers::health_check_api::handlers::health_check_handler; diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 33e20a84e..5c33fc8fa 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -6,8 +6,8 @@ use axum_server::tls_rustls::RustlsConfig; use axum_server::Handle; use derive_more::Constructor; use futures::future::BoxFuture; -use log::info; use tokio::sync::oneshot::{Receiver, Sender}; +use tracing::info; use super::v1::routes::router; use crate::bootstrap::jobs::Started; diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs index e9198f20c..0b009f700 100644 --- a/src/servers/http/v1/handlers/announce.rs +++ b/src/servers/http/v1/handlers/announce.rs @@ -11,10 +11,10 @@ use std::sync::Arc; use axum::extract::State; use axum::response::{IntoResponse, Response}; -use log::debug; use torrust_tracker_clock::clock::Time; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::{peer, NumberOfBytes}; +use tracing::debug; use crate::core::auth::Key; use crate::core::{AnnounceData, Tracker}; diff --git a/src/servers/http/v1/handlers/scrape.rs b/src/servers/http/v1/handlers/scrape.rs index d6b39cc53..172607637 100644 --- a/src/servers/http/v1/handlers/scrape.rs +++ b/src/servers/http/v1/handlers/scrape.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use axum::extract::State; use axum::response::{IntoResponse, Response}; -use log::debug; +use tracing::debug; use crate::core::auth::Key; use crate::core::{ScrapeData, Tracker}; diff --git a/src/servers/registar.rs b/src/servers/registar.rs index 9c23573c4..6058595ba 100644 --- a/src/servers/registar.rs +++ b/src/servers/registar.rs @@ -5,9 +5,9 @@ use std::net::SocketAddr; use std::sync::Arc; use derive_more::Constructor; -use log::debug; use tokio::sync::Mutex; use tokio::task::JoinHandle; +use tracing::debug; /// A [`ServiceHeathCheckResult`] is returned by a completed health check. pub type ServiceHeathCheckResult = Result; diff --git a/src/servers/signals.rs b/src/servers/signals.rs index 42fd868e8..0a1a06312 100644 --- a/src/servers/signals.rs +++ b/src/servers/signals.rs @@ -2,8 +2,8 @@ use std::time::Duration; use derive_more::Display; -use log::info; use tokio::time::sleep; +use tracing::info; /// This is the message that the "launcher" spawned task receives from the main /// application process to notify the service to shutdown. diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 4064cf041..858d6606c 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -10,10 +10,10 @@ use aquatic_udp_protocol::{ ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; -use log::debug; use tokio::net::UdpSocket; use torrust_tracker_located_error::DynError; use torrust_tracker_primitives::info_hash::InfoHash; +use tracing::debug; use uuid::Uuid; use zerocopy::network_endian::I32; diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index be4c36d40..dd30d9d6d 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -22,13 +22,13 @@ use std::sync::Arc; use aquatic_udp_protocol::Response; use derive_more::Constructor; -use log::{debug, error, info, trace}; use ringbuf::traits::{Consumer, Observer, Producer}; use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::sync::oneshot; use tokio::task::{AbortHandle, JoinHandle}; use tokio::{select, task}; +use tracing::{debug, error, info, trace}; use super::UdpRequest; use crate::bootstrap::jobs::Started; diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 81209efb6..45b51ad35 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -6,9 +6,9 @@ use std::time::Duration; use anyhow::{anyhow, Context, Result}; use aquatic_udp_protocol::{ConnectRequest, Request, Response, TransactionId}; -use log::debug; use tokio::net::UdpSocket; use tokio::time; +use tracing::debug; use zerocopy::network_endian::I32; use crate::shared::bit_torrent::tracker::udp::{source_address, MAX_PACKET_SIZE}; diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs index c200beaeb..a50ad5156 100644 --- a/tests/servers/health_check_api/environment.rs +++ b/tests/servers/health_check_api/environment.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; use std::sync::Arc; -use log::debug; use tokio::sync::oneshot::{self, Sender}; use tokio::task::JoinHandle; use torrust_tracker::bootstrap::jobs::Started; @@ -9,6 +8,7 @@ use torrust_tracker::servers::health_check_api::server; use torrust_tracker::servers::registar::Registar; use torrust_tracker::servers::signals::{self, Halted}; use torrust_tracker_configuration::HealthCheckApi; +use tracing::debug; #[derive(Debug)] pub enum Error { From 69f100ab7d38cde396546ea3b1a34f7c718bd62d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:01:40 +0100 Subject: [PATCH 099/217] refactor: [#884] move from log to tracing crate --- packages/configuration/src/lib.rs | 2 +- .../config/tracker.e2e.container.sqlite3.toml | 4 + src/bootstrap/logging.rs | 2 +- src/console/ci/e2e/docker.rs | 2 + src/console/ci/e2e/logs_parser.rs | 73 +++++++++++++------ src/console/ci/e2e/runner.rs | 7 +- src/console/ci/e2e/tracker_container.rs | 4 +- src/console/clients/checker/app.rs | 2 +- src/console/clients/udp/app.rs | 2 +- src/servers/udp/server.rs | 2 +- 10 files changed, 70 insertions(+), 30 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 62792c271..46ece96ab 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -64,7 +64,7 @@ impl Info { let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { - println!("Loading configuration from environment variable {config_toml} ..."); + println!("Loading configuration from environment variable:\n {config_toml}"); Some(config_toml) } else { None diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index e7d8fa279..767b56116 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -11,3 +11,7 @@ ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + +[health_check_api] +# Must be bound to wildcard IP to be accessible from outside the container +bind_address = "0.0.0.0:1313" diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 914ede0c4..5194f06ea 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -46,7 +46,7 @@ fn config_level_or_default(log_level: &Option) -> LevelFilter { } fn tracing_stdout_init(filter: LevelFilter, style: &TraceStyle) { - let builder = tracing_subscriber::fmt().with_max_level(filter); + let builder = tracing_subscriber::fmt().with_max_level(filter).with_ansi(false); let () = match style { TraceStyle::Default => builder.init(), diff --git a/src/console/ci/e2e/docker.rs b/src/console/ci/e2e/docker.rs index 26b7f7708..32a0c3e56 100644 --- a/src/console/ci/e2e/docker.rs +++ b/src/console/ci/e2e/docker.rs @@ -176,6 +176,8 @@ impl Docker { let output_str = String::from_utf8_lossy(&output.stdout); + info!("Waiting until container is healthy: {:?}", output_str); + if output_str.contains("(healthy)") { return true; } diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 2d215a569..a9277524e 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,9 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "INFO UDP TRACKER: Starting on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "INFO HTTP TRACKER: Starting on: "; -const HEALTH_CHECK_PATTERN: &str = "INFO HEALTH CHECK API: Starting on: "; +const UDP_TRACKER_PATTERN: &str = "UDP TRACKER: Started on: udp://"; +const HTTP_TRACKER_PATTERN: &str = "HTTP TRACKER: Started on: "; +const HEALTH_CHECK_PATTERN: &str = "HEALTH CHECK API: Started on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -19,16 +19,17 @@ impl RunningServices { /// /// ```text /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-06-10T14:26:10.040894Z INFO torrust_tracker::bootstrap::logging: logging initialized. - /// 2024-06-10T14:26:10.041363Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - /// 2024-06-10T14:26:10.041386Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:26:10.041420Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - /// 2024-06-10T14:26:10.041516Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - /// 2024-06-10T14:26:10.041521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:26:10.041611Z INFO API: Starting on http://127.0.0.1:1212 - /// 2024-06-10T14:26:10.041614Z INFO API: Started on http://127.0.0.1:1212 - /// 2024-06-10T14:26:10.041623Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - /// 2024-06-10T14:26:10.041657Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + /// 2024-06-10T14:59:57.973525Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T14:59:57.974306Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T14:59:57.974316Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + /// 2024-06-10T14:59:57.974332Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:59:57.974366Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T14:59:57.974513Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T14:59:57.974521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:59:57.974615Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T14:59:57.974618Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T14:59:57.974643Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T14:59:57.974760Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -46,6 +47,9 @@ impl RunningServices { /// ] /// } /// ``` + /// + /// NOTICE: Using colors in the console output could affect this method + /// due to the hidden control chars. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { let mut udp_trackers: Vec = Vec::new(); @@ -85,20 +89,45 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let logs = "\ - INFO UDP TRACKER: Starting on: udp://0.0.0.0:8080\n\ - INFO HTTP TRACKER: Starting on: 0.0.0.0:9090\n\ - INFO HEALTH CHECK API: Starting on: 0.0.0.0:10010"; - let running_services = RunningServices::parse_from_logs(logs); + let log = r#" + Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" + + [[udp_trackers]] + enabled = true + + [[http_trackers]] + enabled = true + ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" + ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + + [http_api] + ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" + ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + ... + Loading configuration from file: `/etc/torrust/tracker/tracker.toml` ... + 2024-06-10T15:09:54.411031Z INFO torrust_tracker::bootstrap::logging: logging initialized. + 2024-06-10T15:09:54.415084Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T15:09:54.415091Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + 2024-06-10T15:09:54.415104Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T15:09:54.415130Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + 2024-06-10T15:09:54.415266Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + 2024-06-10T15:09:54.415275Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T15:09:54.415403Z INFO API: Starting on http://127.0.0.1:1212 + 2024-06-10T15:09:54.415411Z INFO API: Started on http://127.0.0.1:1212 + 2024-06-10T15:09:54.415430Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + 2024-06-10T15:09:54.415472Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + "#; + + let running_services = RunningServices::parse_from_logs(log); - assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]); - assert_eq!(running_services.http_trackers, vec!["127.0.0.1:9090"]); - assert_eq!(running_services.health_checks, vec!["127.0.0.1:10010/health_check"]); + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); + assert_eq!(running_services.http_trackers, vec!["http://127.0.0.1:7070"]); + assert_eq!(running_services.health_checks, vec!["http://127.0.0.1:1313/health_check"]); } #[test] fn it_should_ignore_logs_with_no_matching_lines() { - let logs = "[Other Service][INFO] Starting on: 0.0.0.0:7070"; + let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; let running_services = RunningServices::parse_from_logs(logs); assert!(running_services.udp_trackers.is_empty()); diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index c44ce464e..aeb28b777 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -60,6 +60,11 @@ pub fn run() { let running_services = tracker_container.running_services(); + info!( + "Running services:\n {}", + serde_json::to_string_pretty(&running_services).expect("running services to be serializable to JSON") + ); + assert_there_is_at_least_one_service_per_type(&running_services); let tracker_checker_config = @@ -78,7 +83,7 @@ pub fn run() { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).init(); + tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); info!("logging initialized."); } diff --git a/src/console/ci/e2e/tracker_container.rs b/src/console/ci/e2e/tracker_container.rs index 0cb4fec7c..dc7036faa 100644 --- a/src/console/ci/e2e/tracker_container.rs +++ b/src/console/ci/e2e/tracker_container.rs @@ -2,7 +2,7 @@ use std::time::Duration; use rand::distributions::Alphanumeric; use rand::Rng; -use tracing::{debug, error, info}; +use tracing::{error, info}; use super::docker::{RunOptions, RunningContainer}; use super::logs_parser::RunningServices; @@ -72,7 +72,7 @@ impl TrackerContainer { pub fn running_services(&self) -> RunningServices { let logs = Docker::logs(&self.name).expect("Logs should be captured from running container"); - debug!("Parsing running services from logs. Logs :\n{logs}"); + info!("Parsing running services from logs. Logs :\n{logs}"); RunningServices::parse_from_logs(&logs) } diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index 84802688d..ade1d4820 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -58,7 +58,7 @@ pub async fn run() -> Result> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).init(); + tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); info!("logging initialized."); } diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index c780157f4..323fca1b6 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -127,7 +127,7 @@ pub async fn run() -> anyhow::Result<()> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).init(); + tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); info!("logging initialized."); } diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index dd30d9d6d..b2c72258d 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -273,7 +273,7 @@ impl Udp { .send(Started { address }) .expect("the UDP Tracker service should not be dropped"); - debug!(target: "UDP TRACKER", "Started on: udp://{}", address); + info!(target: "UDP TRACKER", "Started on: udp://{}", address); let stop = running.abort_handle(); From 7de259524724bc0036e075b4ee4df1d9fefd53c9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:04:15 +0100 Subject: [PATCH 100/217] chore(deps): [#884] remove unused crate log We have moved to tracing crate. --- Cargo.lock | 2 -- Cargo.toml | 1 - packages/located-error/Cargo.toml | 1 - 3 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab7512536..c71ee890d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3904,7 +3904,6 @@ dependencies = [ "hyper-util", "lazy_static", "local-ip-address", - "log", "mockall", "multimap", "parking_lot", @@ -3977,7 +3976,6 @@ dependencies = [ name = "torrust-tracker-located-error" version = "3.0.0-alpha.12-develop" dependencies = [ - "log", "thiserror", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 6166831e2..94ad9a02c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ http-body = "1.0.0" hyper = "1" hyper-util = { version = "0.1.3", features = ["http1", "http2", "tokio"] } lazy_static = "1" -log = { version = "0", features = ["release_max_level_info"] } multimap = "0" parking_lot = "0.12.1" percent-encoding = "2" diff --git a/packages/located-error/Cargo.toml b/packages/located-error/Cargo.toml index f34f9bc88..4b2c73178 100644 --- a/packages/located-error/Cargo.toml +++ b/packages/located-error/Cargo.toml @@ -15,7 +15,6 @@ rust-version.workspace = true version.workspace = true [dependencies] -log = { version = "0", features = ["release_max_level_info"] } tracing = "0.1.40" [dev-dependencies] From d6fd11a0b736a4e20abfdea02cb84c79c64f7168 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:23:55 +0100 Subject: [PATCH 101/217] test: [#884] add test for parsing array of services from app logs The tracker can run multiple UDP or HTTP trackers. We parse the services from app output but there was not test for multiple services of the same type (UDP or HTTP tracker). --- src/console/ci/e2e/logs_parser.rs | 72 +++++++++++++++++++------------ 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index a9277524e..2a1876a11 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -19,17 +19,19 @@ impl RunningServices { /// /// ```text /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-06-10T14:59:57.973525Z INFO torrust_tracker::bootstrap::logging: logging initialized. - /// 2024-06-10T14:59:57.974306Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - /// 2024-06-10T14:59:57.974316Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 - /// 2024-06-10T14:59:57.974332Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:59:57.974366Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - /// 2024-06-10T14:59:57.974513Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - /// 2024-06-10T14:59:57.974521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:59:57.974615Z INFO API: Starting on http://127.0.0.1:1212 - /// 2024-06-10T14:59:57.974618Z INFO API: Started on http://127.0.0.1:1212 - /// 2024-06-10T14:59:57.974643Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - /// 2024-06-10T14:59:57.974760Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + /// 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T16:07:39.990205Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868 + /// 2024-06-10T16:07:39.990215Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6868 + /// 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + /// 2024-06-10T16:07:39.990261Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T16:07:39.990303Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T16:07:39.990439Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T16:07:39.990448Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T16:07:39.990563Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -48,7 +50,7 @@ impl RunningServices { /// } /// ``` /// - /// NOTICE: Using colors in the console output could affect this method + /// NOTICE: Using colors in the console output could affect this method /// due to the hidden control chars. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { @@ -89,7 +91,7 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let log = r#" + let logs = r#" Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] @@ -103,22 +105,22 @@ mod tests { [http_api] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - ... - Loading configuration from file: `/etc/torrust/tracker/tracker.toml` ... - 2024-06-10T15:09:54.411031Z INFO torrust_tracker::bootstrap::logging: logging initialized. - 2024-06-10T15:09:54.415084Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - 2024-06-10T15:09:54.415091Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 - 2024-06-10T15:09:54.415104Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - 2024-06-10T15:09:54.415130Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - 2024-06-10T15:09:54.415266Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - 2024-06-10T15:09:54.415275Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - 2024-06-10T15:09:54.415403Z INFO API: Starting on http://127.0.0.1:1212 - 2024-06-10T15:09:54.415411Z INFO API: Started on http://127.0.0.1:1212 - 2024-06-10T15:09:54.415430Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - 2024-06-10T15:09:54.415472Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + + Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... + 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. + 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990261Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T16:07:39.990303Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + 2024-06-10T16:07:39.990439Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + 2024-06-10T16:07:39.990448Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T16:07:39.990563Z INFO API: Starting on http://127.0.0.1:1212 + 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 + 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 "#; - let running_services = RunningServices::parse_from_logs(log); + let running_services = RunningServices::parse_from_logs(logs); assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); assert_eq!(running_services.http_trackers, vec!["http://127.0.0.1:7070"]); @@ -128,6 +130,7 @@ mod tests { #[test] fn it_should_ignore_logs_with_no_matching_lines() { let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; + let running_services = RunningServices::parse_from_logs(logs); assert!(running_services.udp_trackers.is_empty()); @@ -135,6 +138,21 @@ mod tests { assert!(running_services.health_checks.is_empty()); } + #[test] + fn it_should_parse_multiple_services() { + let logs = " + 2024-06-10T16:07:39.990205Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868 + 2024-06-10T16:07:39.990215Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6868 + + 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + "; + + let running_services = RunningServices::parse_from_logs(logs); + + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6868", "127.0.0.1:6969"]); + } + #[test] fn it_should_replace_wildcard_ip_with_localhost() { let address = "0.0.0.0:8080"; From ec88dbfffdeac08bbc3aa69de90ffad9e2711023 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:24:25 +0100 Subject: [PATCH 102/217] chore(deps): remove unused dependencies log and fern We have moved from crate log to tracing. --- Cargo.lock | 10 ---------- Cargo.toml | 1 - 2 files changed, 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c71ee890d..36508e261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,15 +1259,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - [[package]] name = "figment" version = "0.10.19" @@ -3894,7 +3885,6 @@ dependencies = [ "crossbeam-skiplist", "dashmap", "derive_more", - "fern", "figment", "futures", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 94ad9a02c..418bcb3ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" -fern = "0" figment = "0.10.18" futures = "0" futures-util = "0.3.30" From c08de7519498ea8495fb3d200ae918a4c4076f7f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 18:35:10 +0100 Subject: [PATCH 103/217] refactor: [#659] use clap and anyhow in E2E test runner You can execute the E2E runner with: ```bash cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" ``` Or: ```bash TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.e2e.container.sqlite3.toml" cargo run --bin e2e_tests_runner ``` Or: ```bash TORRUST_TRACKER_CONFIG_TOML=$(cat "./share/default/config/tracker.e2e.container.sqlite3.toml") cargo run --bin e2e_tests_runner ``` --- .github/workflows/testing.yaml | 2 +- src/bin/e2e_tests_runner.rs | 4 +- src/console/ci/e2e/runner.rs | 85 +++++++++++++++++++++++----------- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 620670f97..abe6f0a60 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -152,4 +152,4 @@ jobs: - id: test name: Run E2E Tests - run: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml + run: cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" diff --git a/src/bin/e2e_tests_runner.rs b/src/bin/e2e_tests_runner.rs index b21459d2e..eb91c0d86 100644 --- a/src/bin/e2e_tests_runner.rs +++ b/src/bin/e2e_tests_runner.rs @@ -1,6 +1,6 @@ //! Program to run E2E tests. use torrust_tracker::console::ci::e2e; -fn main() { - e2e::runner::run(); +fn main() -> anyhow::Result<()> { + e2e::runner::run() } diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index aeb28b777..a80b65ce2 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -1,8 +1,26 @@ //! Program to run E2E tests. //! +//! You can execute it with (passing a TOML config file path): +//! +//! ```text +//! cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" +//! ``` +//! +//! Or: +//! //! ```text -//! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml +//! TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.e2e.container.sqlite3.toml" cargo run --bin e2e_tests_runner" //! ``` +//! +//! You can execute it with (directly passing TOML config): +//! +//! ```text +//! TORRUST_TRACKER_CONFIG_TOML=$(cat "./share/default/config/tracker.e2e.container.sqlite3.toml") cargo run --bin e2e_tests_runner +//! ``` +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; use tracing::info; use tracing::level_filters::LevelFilter; @@ -19,25 +37,38 @@ use crate::console::ci::e2e::tracker_checker::{self}; Should we remove the image too? */ -const NUMBER_OF_ARGUMENTS: usize = 2; const CONTAINER_IMAGE: &str = "torrust-tracker:local"; const CONTAINER_NAME_PREFIX: &str = "tracker_"; -pub struct Arguments { - pub tracker_config_path: String, +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Path to the JSON configuration file. + #[clap(short, long, env = "TORRUST_TRACKER_CONFIG_TOML_PATH")] + config_toml_path: Option, + + /// Direct configuration content in JSON. + #[clap(env = "TORRUST_TRACKER_CONFIG_TOML", hide_env_values = true)] + config_toml: Option, } /// Script to run E2E tests. /// +/// # Errors +/// +/// Will return an error if it can't load the tracker configuration from arguments. +/// /// # Panics /// /// Will panic if it can't not perform any of the operations. -pub fn run() { +pub fn run() -> anyhow::Result<()> { tracing_stdout_init(LevelFilter::INFO); - let args = parse_arguments(); + let args = Args::parse(); - let tracker_config = load_tracker_configuration(&args.tracker_config_path); + let tracker_config = load_tracker_configuration(&args)?; + + info!("tracker config:\n{tracker_config}"); let mut tracker_container = TrackerContainer::new(CONTAINER_IMAGE, CONTAINER_NAME_PREFIX); @@ -80,36 +111,36 @@ pub fn run() { tracker_container.remove(); info!("Tracker container final state:\n{:#?}", tracker_container); + + Ok(()) } fn tracing_stdout_init(filter: LevelFilter) { tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); - info!("logging initialized."); + info!("Logging initialized."); } -fn parse_arguments() -> Arguments { - let args: Vec = std::env::args().collect(); - - if args.len() < NUMBER_OF_ARGUMENTS { - eprintln!("Usage: cargo run --bin e2e_tests_runner "); - eprintln!("For example: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml"); - std::process::exit(1); - } - - let config_path = &args[1]; - - Arguments { - tracker_config_path: config_path.to_string(), +fn load_tracker_configuration(args: &Args) -> anyhow::Result { + match (args.config_toml_path.clone(), args.config_toml.clone()) { + (Some(config_path), _) => { + info!( + "Reading tracker configuration from file: {} ...", + config_path.to_string_lossy() + ); + load_config_from_file(&config_path) + } + (_, Some(config_content)) => { + info!("Reading tracker configuration from env var ..."); + Ok(config_content) + } + _ => Err(anyhow::anyhow!("No configuration provided")), } } -fn load_tracker_configuration(tracker_config_path: &str) -> String { - info!("Reading tracker configuration from file: {} ...", tracker_config_path); - read_file(tracker_config_path) -} +fn load_config_from_file(path: &PathBuf) -> anyhow::Result { + let config = std::fs::read_to_string(path).with_context(|| format!("CSan't read config file {path:?}"))?; -fn read_file(path: &str) -> String { - std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Can't read file {path}")) + Ok(config) } fn assert_there_is_at_least_one_service_per_type(running_services: &RunningServices) { From f8a9976ec90481bedbc9eedbd4a38a42d7163bfe Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Jun 2024 14:40:22 +0100 Subject: [PATCH 104/217] docs: [#770] update benchmarking docs We are now using criterion to benchmark the torrent repository implementations. --- docs/benchmarking.md | 96 ++++++++++++------ ...ry-implementations-benchmarking-report.png | Bin 0 -> 630867 bytes 2 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 docs/media/torrent-repository-implementations-benchmarking-report.png diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 2a3f1f8b0..1758e0de4 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -96,7 +96,7 @@ Announce responses per info hash: - p100: 361 ``` -> IMPORTANT: The performance of th Torrust UDP Tracker is drastically decreased with these log levels: `info`, `debug`, `trace`. +> IMPORTANT: The performance of the Torrust UDP Tracker is drastically decreased with these log levels: `info`, `debug`, `trace`. ```output Requests out: 40719.21/second @@ -226,46 +226,76 @@ Using a PC with: ## Repository benchmarking +### Requirements + +You need to install the `gnuplot` package. + +```console +sudo apt install gnuplot +``` + +### Run + You can run it with: ```console -cargo run --release -p torrust-torrent-repository-benchmarks -- --threads 4 --sleep 0 --compare true +cargo bench -p torrust-tracker-torrent-repository ``` -It tests the different implementation for the internal torrent storage. +It tests the different implementations for the internal torrent storage. The output should be something like this: ```output -tokio::sync::RwLock> -add_one_torrent: Avg/AdjAvg: (60ns, 59ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (10.909457ms, 0ns) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (13.88879ms, 0ns) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (7.772484ms, 7.782535ms) - -std::sync::RwLock> -add_one_torrent: Avg/AdjAvg: (43ns, 39ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (4.020937ms, 4.020937ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (5.896177ms, 5.768448ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (3.883823ms, 3.883823ms) - -std::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (51ns, 49ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (3.252314ms, 3.149109ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (8.411094ms, 8.411094ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.106086ms, 4.106086ms) - -tokio::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (91ns, 90ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (3.542378ms, 3.435695ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (15.651172ms, 15.651172ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.368189ms, 4.257572ms) - -tokio::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (111ns, 109ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (6.590677ms, 6.808535ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (16.572217ms, 16.30488ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.073221ms, 4.000122ms) + Running benches/repository_benchmark.rs (target/release/deps/repository_benchmark-2f7830898bbdfba4) +add_one_torrent/RwLockStd + time: [60.936 ns 61.383 ns 61.764 ns] +Found 24 outliers among 100 measurements (24.00%) + 15 (15.00%) high mild + 9 (9.00%) high severe +add_one_torrent/RwLockStdMutexStd + time: [60.829 ns 60.937 ns 61.053 ns] +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high severe +add_one_torrent/RwLockStdMutexTokio + time: [96.034 ns 96.243 ns 96.545 ns] +Found 6 outliers among 100 measurements (6.00%) + 4 (4.00%) high mild + 2 (2.00%) high severe +add_one_torrent/RwLockTokio + time: [108.25 ns 108.66 ns 109.06 ns] +Found 2 outliers among 100 measurements (2.00%) + 2 (2.00%) low mild +add_one_torrent/RwLockTokioMutexStd + time: [109.03 ns 109.11 ns 109.19 ns] +Found 4 outliers among 100 measurements (4.00%) + 1 (1.00%) low mild + 1 (1.00%) high mild + 2 (2.00%) high severe +Benchmarking add_one_torrent/RwLockTokioMutexTokio: Collecting 100 samples in estimated 1.0003 s (7.1M iterationsadd_one_torrent/RwLockTokioMutexTokio + time: [139.64 ns 140.11 ns 140.62 ns] +``` + +After running it you should have a new directory containing the criterion reports: + +```console +target/criterion/ +├── add_multiple_torrents_in_parallel +├── add_one_torrent +├── report +├── update_multiple_torrents_in_parallel +└── update_one_torrent_in_parallel ``` +You can see one report for each of the operations we are considering for benchmarking: + +- Add multiple torrents in parallel. +- Add one torrent. +- Update multiple torrents in parallel. +- Update one torrent in parallel. + +Each report look like the following: + +![Torrent repository implementations benchmarking report](./media/torrent-repository-implementations-benchmarking-report.png) + ## Other considerations -We are testing new repository implementations that allow concurrent writes. See . +If you are interested in knowing more about the tracker performance or contribute to improve its performance you ca join the [performance optimizations discussion](https://github.com/torrust/torrust-tracker/discussions/774). diff --git a/docs/media/torrent-repository-implementations-benchmarking-report.png b/docs/media/torrent-repository-implementations-benchmarking-report.png new file mode 100644 index 0000000000000000000000000000000000000000..ee87c6d42292b707338a1917782b39da1357dba9 GIT binary patch literal 630867 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

`RCsMA%N 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 literal 0 HcmV?d00001 From 322b976a087535c1710f15d87684f70f7787e150 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 15:38:06 +0100 Subject: [PATCH 105/217] chore(deps): update dependencies ```s cargo update Updating crates.io index Locking 35 packages to latest compatible versions Updating backtrace v0.3.72 -> v0.3.73 Updating clap v4.5.6 -> v4.5.7 Updating clap_builder v4.5.6 -> v4.5.7 Adding displaydoc v0.2.4 Updating http-body-util v0.1.1 -> v0.1.2 Updating httparse v1.8.0 -> v1.9.3 Adding icu_collections v1.5.0 Adding icu_locid v1.5.0 Adding icu_locid_transform v1.5.0 Adding icu_locid_transform_data v1.5.0 Adding icu_normalizer v1.5.0 Adding icu_normalizer_data v1.5.0 Adding icu_properties v1.5.0 Adding icu_properties_data v1.5.0 Adding icu_provider v1.5.0 Adding icu_provider_macros v1.5.0 Updating idna v0.5.0 -> v1.0.0 Adding litemap v0.7.3 Updating memchr v2.7.2 -> v2.7.4 Updating object v0.35.0 -> v0.36.0 Updating redox_syscall v0.5.1 -> v0.5.2 Adding stable_deref_trait v1.2.0 Adding synstructure v0.13.1 Adding tinystr v0.7.6 Removing unicode-bidi v0.3.15 Removing unicode-normalization v0.1.23 Updating url v2.5.0 -> v2.5.1 Adding utf16_iter v1.0.5 Adding utf8_iter v1.0.4 Adding write16 v1.0.0 Adding writeable v0.5.5 Adding yoke v0.7.4 Adding yoke-derive v0.7.4 Adding zerofrom v0.1.4 Adding zerofrom-derive v0.1.4 Adding zerovec v0.10.2 Adding zerovec-derive v0.10.2 ``` --- Cargo.lock | 316 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 278 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36508e261..1c10516b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,9 +507,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.72" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.6" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.6" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -1148,6 +1148,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "downcast" version = "0.11.0" @@ -1676,12 +1687,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -1689,9 +1700,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "httpdate" @@ -1779,6 +1790,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1787,12 +1916,14 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -2061,6 +2192,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "local-ip-address" version = "0.6.1" @@ -2109,9 +2246,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2386,9 +2523,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -2902,9 +3039,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags 2.5.0", ] @@ -3525,6 +3662,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3593,6 +3736,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3725,6 +3879,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -4172,27 +4336,12 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -4201,15 +4350,27 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -4564,6 +4725,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -4579,6 +4752,30 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.34" @@ -4600,6 +4797,49 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "zstd" version = "0.13.1" From a88082aa40c99a1d59082b396210e3f2e39f68df Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:22:43 +0100 Subject: [PATCH 106/217] fix: [#893] enable color in logs --- src/bootstrap/logging.rs | 2 +- src/console/ci/e2e/logs_parser.rs | 57 +++++++++++++++++++++++++----- src/console/ci/e2e/runner.rs | 2 +- src/console/clients/checker/app.rs | 2 +- src/console/clients/udp/app.rs | 2 +- 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 5194f06ea..14756565f 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -46,7 +46,7 @@ fn config_level_or_default(log_level: &Option) -> LevelFilter { } fn tracing_stdout_init(filter: LevelFilter, style: &TraceStyle) { - let builder = tracing_subscriber::fmt().with_max_level(filter).with_ansi(false); + let builder = tracing_subscriber::fmt().with_max_level(filter).with_ansi(true); let () = match style { TraceStyle::Default => builder.init(), diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 2a1876a11..ff4028f80 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,16 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "UDP TRACKER: Started on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "HTTP TRACKER: Started on: "; -const HEALTH_CHECK_PATTERN: &str = "HEALTH CHECK API: Started on: "; +const INFO_LOG_LEVEL: &str = "INFO"; + +const UDP_TRACKER_TARGET: &str = "UDP TRACKER"; +const UDP_TRACKER_SOCKET_ADDR_START_PATTERN: &str = "Started on: udp://"; + +const HTTP_TRACKER_TARGET: &str = "HTTP TRACKER"; +const HTTP_TRACKER_URL_START_PATTERN: &str = "Started on: "; + +const HEALTH_CHECK_TARGET: &str = "HEALTH CHECK API"; +const HEALTH_CHECK_URL_START_PATTERN: &str = "Started on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -59,11 +66,11 @@ impl RunningServices { let mut health_checks: Vec = Vec::new(); for line in logs.lines() { - if let Some(address) = Self::extract_address_if_matches(line, UDP_TRACKER_PATTERN) { + if let Some(address) = Self::extract_udp_tracker_url(line) { udp_trackers.push(address); - } else if let Some(address) = Self::extract_address_if_matches(line, HTTP_TRACKER_PATTERN) { + } else if let Some(address) = Self::extract_http_tracker_url(line) { http_trackers.push(address); - } else if let Some(address) = Self::extract_address_if_matches(line, HEALTH_CHECK_PATTERN) { + } else if let Some(address) = Self::extract_health_check_api_url(line) { health_checks.push(format!("{address}/health_check")); } } @@ -75,9 +82,32 @@ impl RunningServices { } } - fn extract_address_if_matches(line: &str, pattern: &str) -> Option { - line.find(pattern) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + pattern.len()..].trim())) + fn extract_udp_tracker_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(UDP_TRACKER_TARGET) { + return None; + }; + + line.find(UDP_TRACKER_SOCKET_ADDR_START_PATTERN).map(|start| { + Self::replace_wildcard_ip_with_localhost(line[start + UDP_TRACKER_SOCKET_ADDR_START_PATTERN.len()..].trim()) + }) + } + + fn extract_http_tracker_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(HTTP_TRACKER_TARGET) { + return None; + }; + + line.find(HTTP_TRACKER_URL_START_PATTERN) + .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HTTP_TRACKER_URL_START_PATTERN.len()..].trim())) + } + + fn extract_health_check_api_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(HEALTH_CHECK_TARGET) { + return None; + }; + + line.find(HEALTH_CHECK_URL_START_PATTERN) + .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HEALTH_CHECK_URL_START_PATTERN.len()..].trim())) } fn replace_wildcard_ip_with_localhost(address: &str) -> String { @@ -127,6 +157,15 @@ mod tests { assert_eq!(running_services.health_checks, vec!["http://127.0.0.1:1313/health_check"]); } + #[test] + fn it_should_support_colored_output() { + let logs = "\x1b[2m2024-06-14T14:40:13.028824Z\x1b[0m \x1b[33mINFO\x1b[0m \x1b[2mUDP TRACKER\x1b[0m: \x1b[37mStarted on: udp://0.0.0.0:6969\x1b[0m"; + + let running_services = RunningServices::parse_from_logs(logs); + + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); + } + #[test] fn it_should_ignore_logs_with_no_matching_lines() { let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index a80b65ce2..a3d61894e 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -116,7 +116,7 @@ pub fn run() -> anyhow::Result<()> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); + tracing_subscriber::fmt().with_max_level(filter).init(); info!("Logging initialized."); } diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index ade1d4820..84802688d 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -58,7 +58,7 @@ pub async fn run() -> Result> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); + tracing_subscriber::fmt().with_max_level(filter).init(); info!("logging initialized."); } diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 323fca1b6..c780157f4 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -127,7 +127,7 @@ pub async fn run() -> anyhow::Result<()> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); + tracing_subscriber::fmt().with_max_level(filter).init(); info!("logging initialized."); } From a293373bb271d543bf0f03ef0ebb3807148b55d7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:36:14 +0100 Subject: [PATCH 107/217] chore(deps): add cargo dependency regex To parse cargo logs. --- Cargo.lock | 1 + Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1c10516b2..7a54063bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4067,6 +4067,7 @@ dependencies = [ "r2d2_mysql", "r2d2_sqlite", "rand", + "regex", "reqwest", "ringbuf", "serde", diff --git a/Cargo.toml b/Cargo.toml index 418bcb3ed..8b58154ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ r2d2 = "0" r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" +regex = "1.10.5" reqwest = { version = "0", features = ["json"] } ringbuf = "0" serde = { version = "1", features = ["derive"] } From eb928bcd041ba9942b777d150bf86b95b07829c8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:44:32 +0100 Subject: [PATCH 108/217] fix: [#893] enable color for logs It was disabled becuase parsing lgos to extract the services URLs was not working due to hidden color chars. This changes the parser to ignore color chars. --- src/console/ci/e2e/logs_parser.rs | 77 ++++++++++++++----------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index ff4028f80..a4024f29d 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,16 +1,11 @@ //! Utilities to parse Torrust Tracker logs. +use regex::Regex; use serde::{Deserialize, Serialize}; const INFO_LOG_LEVEL: &str = "INFO"; - -const UDP_TRACKER_TARGET: &str = "UDP TRACKER"; -const UDP_TRACKER_SOCKET_ADDR_START_PATTERN: &str = "Started on: udp://"; - -const HTTP_TRACKER_TARGET: &str = "HTTP TRACKER"; -const HTTP_TRACKER_URL_START_PATTERN: &str = "Started on: "; - -const HEALTH_CHECK_TARGET: &str = "HEALTH CHECK API"; -const HEALTH_CHECK_URL_START_PATTERN: &str = "Started on: "; +const UDP_TRACKER_LOG_TARGET: &str = "UDP TRACKER"; +const HTTP_TRACKER_LOG_TARGET: &str = "HTTP TRACKER"; +const HEALTH_CHECK_API_LOG_TARGET: &str = "HEALTH CHECK API"; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -59,19 +54,43 @@ impl RunningServices { /// /// NOTICE: Using colors in the console output could affect this method /// due to the hidden control chars. + /// + /// # Panics + /// + /// Will panic is the regular expression to parse the services can't be compiled. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { let mut udp_trackers: Vec = Vec::new(); let mut http_trackers: Vec = Vec::new(); let mut health_checks: Vec = Vec::new(); + let udp_re = Regex::new(r"Started on: udp://([0-9.]+:[0-9]+)").unwrap(); + let http_re = Regex::new(r"Started on: (https?://[0-9.]+:[0-9]+)").unwrap(); // DevSkim: ignore DS137138 + let health_re = Regex::new(r"Started on: (https?://[0-9.]+:[0-9]+)").unwrap(); // DevSkim: ignore DS137138 + let ansi_escape_re = Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + for line in logs.lines() { - if let Some(address) = Self::extract_udp_tracker_url(line) { - udp_trackers.push(address); - } else if let Some(address) = Self::extract_http_tracker_url(line) { - http_trackers.push(address); - } else if let Some(address) = Self::extract_health_check_api_url(line) { - health_checks.push(format!("{address}/health_check")); + let clean_line = ansi_escape_re.replace_all(line, ""); + + if !line.contains(INFO_LOG_LEVEL) { + continue; + }; + + if line.contains(UDP_TRACKER_LOG_TARGET) { + if let Some(captures) = udp_re.captures(&clean_line) { + let address = Self::replace_wildcard_ip_with_localhost(&captures[1]); + udp_trackers.push(address); + } + } else if line.contains(HTTP_TRACKER_LOG_TARGET) { + if let Some(captures) = http_re.captures(&clean_line) { + let address = Self::replace_wildcard_ip_with_localhost(&captures[1]); + http_trackers.push(address); + } + } else if line.contains(HEALTH_CHECK_API_LOG_TARGET) { + if let Some(captures) = health_re.captures(&clean_line) { + let address = format!("{}/health_check", Self::replace_wildcard_ip_with_localhost(&captures[1])); + health_checks.push(address); + } } } @@ -82,34 +101,6 @@ impl RunningServices { } } - fn extract_udp_tracker_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(UDP_TRACKER_TARGET) { - return None; - }; - - line.find(UDP_TRACKER_SOCKET_ADDR_START_PATTERN).map(|start| { - Self::replace_wildcard_ip_with_localhost(line[start + UDP_TRACKER_SOCKET_ADDR_START_PATTERN.len()..].trim()) - }) - } - - fn extract_http_tracker_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(HTTP_TRACKER_TARGET) { - return None; - }; - - line.find(HTTP_TRACKER_URL_START_PATTERN) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HTTP_TRACKER_URL_START_PATTERN.len()..].trim())) - } - - fn extract_health_check_api_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(HEALTH_CHECK_TARGET) { - return None; - }; - - line.find(HEALTH_CHECK_URL_START_PATTERN) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HEALTH_CHECK_URL_START_PATTERN.len()..].trim())) - } - fn replace_wildcard_ip_with_localhost(address: &str) -> String { address.replace("0.0.0.0", "127.0.0.1") } From 3c715fbbf7fd4d1e934361064972ae676e94ee95 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 10:24:25 +0100 Subject: [PATCH 109/217] fix: [#898] docker build error: failed to load bitcode of module criterion Command: ``` docker build --target release --tag torrust-tracker:release --file Containerfile . ``` Error: ```s => ERROR [build 3/3] RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive- 56.8s ------ > [build 3/3] RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release: 0.674 Compiling torrust-tracker-located-error v3.0.0-alpha.12-develop (/build/src/packages/located-error) 0.675 Compiling torrust-tracker-primitives v3.0.0-alpha.12-develop (/build/src/packages/primitives) 0.679 Compiling torrust-tracker-contrib-bencode v3.0.0-alpha.12-develop (/build/src/contrib/bencode) 0.763 Compiling torrust-tracker-configuration v3.0.0-alpha.12-develop (/build/src/packages/configuration) 0.763 Compiling torrust-tracker-clock v3.0.0-alpha.12-develop (/build/src/packages/clock) 0.936 Compiling torrust-tracker-torrent-repository v3.0.0-alpha.12-develop (/build/src/packages/torrent-repository) 0.936 Compiling torrust-tracker-test-helpers v3.0.0-alpha.12-develop (/build/src/packages/test-helpers) 1.181 Compiling torrust-tracker v3.0.0-alpha.12-develop (/build/src) 1.891 warning: Invalid value (Producer: 'LLVM18.1.7-rust-1.79.0-stable' Reader: 'LLVM 18.1.7-rust-1.79.0-stable') 1.891 1.891 error: failed to load bitcode of module "criterion-af9a3f7183f1573d.criterion.b69900c842eb33fa-cgu.08.rcgu.o": 1.891 1.991 warning: `torrust-tracker-contrib-bencode` (bench "bencode_benchmark") generated 1 warning 1.991 error: could not compile `torrust-tracker-contrib-bencode` (bench "bencode_benchmark") due to 1 previous error; 1 warning emitted 1.991 warning: build failed, waiting for other jobs to finish... 3.936 warning: `torrust-tracker-torrent-repository` (bench "repository_benchmark") generated 1 warning (1 duplicate) 3.936 error: could not compile `torrust-tracker-torrent-repository` (bench "repository_benchmark") due to 1 previous error; 1 warning emitted 56.80 error: command `/usr/local/rustup/toolchains/1.79.0-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --examples --tests --benches --all-targets --all-features --release` exited with code 101 ------ Containerfile:61 -------------------- 59 | WORKDIR /build/src 60 | COPY . /build/src 61 | >>> RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release 62 | 63 | -------------------- ERROR: failed to solve: process "/bin/sh -c cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release" did not complete successfully: exit code: 101 ``` - Docker: version 25.0.2, build 29cf629 - Rust: nightly-x86_64-unknown-linux-gnu (default). rustc 1.81.0-nightly (d7f6ebace 2024-06-16) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8b58154ec..072a21a7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ members = [ [profile.dev] debug = 1 -lto = "thin" +lto = "fat" opt-level = 1 [profile.release] From ef9461a5b98d59f081935185074245da483d3f2b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 09:48:17 +0100 Subject: [PATCH 110/217] feat!: [#878] extract logging and core section in toml config files --- Containerfile | 4 +-- compose.yaml | 2 +- docs/benchmarking.md | 2 ++ docs/containers.md | 2 +- packages/configuration/src/v1/core.rs | 19 ++++++------- packages/configuration/src/v1/logging.rs | 27 +++++++++++++++++++ packages/configuration/src/v1/mod.rs | 21 +++++++++++++-- packages/test-helpers/src/configuration.rs | 2 +- share/container/entry_script_sh | 10 +++---- .../config/tracker.container.mysql.toml | 1 + .../config/tracker.container.sqlite3.toml | 1 + .../config/tracker.e2e.container.sqlite3.toml | 1 + .../config/tracker.udp.benchmarking.toml | 3 +++ src/bootstrap/logging.rs | 2 +- src/core/mod.rs | 3 +++ src/lib.rs | 5 +++- 16 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 packages/configuration/src/v1/logging.rs diff --git a/Containerfile b/Containerfile index 79fae692f..cdd70e337 100644 --- a/Containerfile +++ b/Containerfile @@ -96,7 +96,7 @@ RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/ COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER="Sqlite3" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 @@ -104,7 +104,7 @@ ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} -ENV TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/compose.yaml b/compose.yaml index 1d425c743..a02302a26 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER:-MySQL} - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 1758e0de4..67b680fdc 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -29,6 +29,7 @@ cargo build --release -p aquatic_udp_load_test Run the tracker with UDP service enabled and other services disabled and set log level to `error`. ```toml +[logging] log_level = "error" [[udp_trackers]] @@ -163,6 +164,7 @@ Announce responses per info hash: Run the tracker with UDP service enabled and other services disabled and set log level to `error`. ```toml +[logging] log_level = "error" [[udp_trackers]] diff --git a/docs/containers.md b/docs/containers.md index a0ba59d4b..ff15cd7cc 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -149,7 +149,7 @@ The following environmental variables can be set: - `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). - `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. - `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 5d00c67ab..ae66f54fa 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -3,15 +3,11 @@ use std::net::{IpAddr, Ipv4Addr}; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; -use crate::{AnnouncePolicy, LogLevel}; +use crate::AnnouncePolicy; #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Core { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - #[serde(default = "Core::default_log_level")] - pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. #[serde(default = "Core::default_mode")] pub mode: TrackerMode, @@ -20,6 +16,7 @@ pub struct Core { /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. #[serde(default = "Core::default_db_driver")] pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. /// For `Sqlite3`, the format is `path/to/database.db`, for example: /// `./storage/tracker/lib/database/sqlite3.db`. @@ -35,11 +32,13 @@ pub struct Core { /// See [`AnnouncePolicy::interval_min`] #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header /// sent from the proxy will be used to get the client's IP address. #[serde(default = "Core::default_on_reverse_proxy")] pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a /// loopback IP address, this IP address will be used instead. If the peer /// is using a loopback IP address, the tracker assumes that the peer is @@ -47,6 +46,7 @@ pub struct Core { /// address instead. #[serde(default = "Core::default_external_ip")] pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -54,6 +54,7 @@ pub struct Core { /// information about the collected metrics. #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. /// That's how many times a torrent has been downloaded completely. #[serde(default = "Core::default_persistent_torrent_completed_stat")] @@ -65,10 +66,12 @@ pub struct Core { /// time, it will be removed from the torrent peer list. #[serde(default = "Core::default_max_peer_timeout")] pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. /// The clean up torrent job runs every `inactive_peer_cleanup_interval` /// seconds and it removes inactive peers. Eventually, the peer list of a @@ -83,7 +86,6 @@ impl Default for Core { let announce_policy = AnnouncePolicy::default(); Self { - log_level: Self::default_log_level(), mode: Self::default_mode(), db_driver: Self::default_db_driver(), db_path: Self::default_db_path(), @@ -101,11 +103,6 @@ impl Default for Core { } impl Core { - #[allow(clippy::unnecessary_wraps)] - fn default_log_level() -> Option { - Some(LogLevel::Info) - } - fn default_mode() -> TrackerMode { TrackerMode::Public } diff --git a/packages/configuration/src/v1/logging.rs b/packages/configuration/src/v1/logging.rs new file mode 100644 index 000000000..c85564a05 --- /dev/null +++ b/packages/configuration/src/v1/logging.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::LogLevel; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Logging { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + #[serde(default = "Logging::default_log_level")] + pub log_level: Option, +} + +impl Default for Logging { + fn default() -> Self { + Self { + log_level: Self::default_log_level(), + } + } +} + +impl Logging { + #[allow(clippy::unnecessary_wraps)] + fn default_log_level() -> Option { + Some(LogLevel::Info) + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8d45270b8..809970506 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -193,7 +193,10 @@ //! The default configuration is: //! //! ```toml +//! [logging] //! log_level = "info" +//! +//! [core] //! mode = "public" //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" @@ -233,6 +236,7 @@ pub mod core; pub mod health_check_api; pub mod http_tracker; +pub mod logging; pub mod tracker_api; pub mod udp_tracker; @@ -241,6 +245,7 @@ use std::net::IpAddr; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; +use logging::Logging; use serde::{Deserialize, Serialize}; use self::core::Core; @@ -258,19 +263,25 @@ const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; /// Core configuration for the tracker. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { + /// Logging configuration + pub logging: Logging, + /// Core configuration. - #[serde(flatten)] pub core: Core, + /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker /// represents a HTTP server that the tracker is running and it has its own /// configuration. pub http_trackers: Vec, + /// The HTTP API configuration. pub http_api: HttpApi, + /// The Health Check API configuration. pub health_check_api: HealthCheckApi, } @@ -278,6 +289,7 @@ pub struct Configuration { impl Default for Configuration { fn default() -> Self { Self { + logging: Logging::default(), core: Core::default(), udp_trackers: vec![UdpTracker::default()], http_trackers: vec![HttpTracker::default()], @@ -365,7 +377,10 @@ mod tests { #[cfg(test)] fn default_config_toml() -> String { - let config = r#"log_level = "info" + let config = r#"[logging] + log_level = "info" + + [core] mode = "public" db_driver = "Sqlite3" db_path = "./storage/tracker/lib/database/sqlite3.db" @@ -475,6 +490,7 @@ mod tests { fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { let config_toml = r#" + [core] db_path = "OVERWRITTEN DEFAULT DB PATH" "# .to_string(); @@ -498,6 +514,7 @@ mod tests { jail.create_file( "tracker.toml", r#" + [core] db_path = "OVERWRITTEN DEFAULT DB PATH" "#, )?; diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 15ecd5280..c35d0a851 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -29,7 +29,7 @@ pub fn ephemeral() -> Configuration { let mut config = Configuration::default(); - config.core.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging + config.logging.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 8c704ea67..51df717c6 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,8 +26,8 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "Sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "Sqlite3"; then # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" @@ -35,7 +35,7 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "MySQL"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "MySQL"; then # (no database file needed for MySQL) @@ -43,12 +43,12 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\"." echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 7678327ab..617450562 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,3 +1,4 @@ +[core] db_driver = "MySQL" db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index da8259286..01ca655c3 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,3 +1,4 @@ +[core] db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[http_trackers]] diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 767b56116..60d7a798a 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,3 +1,4 @@ +[core] db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 00f62628b..cd193c40a 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -1,4 +1,7 @@ +[logging] log_level = "error" + +[core] remove_peerless_torrents = false tracker_usage_statistics = false diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 14756565f..f6868602d 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -20,7 +20,7 @@ static INIT: Once = Once::new(); /// It redirects the log info to the standard output with the log level defined in the configuration pub fn setup(cfg: &Configuration) { - let tracing_level = config_level_or_default(&cfg.core.log_level); + let tracing_level = config_level_or_default(&cfg.logging.log_level); if tracing_level == LevelFilter::OFF { return; diff --git a/src/core/mod.rs b/src/core/mod.rs index 6af28199f..1b60ad6f9 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -312,7 +312,10 @@ //! You can control the behavior of this module with the module settings: //! //! ```toml +//! [logging] //! log_level = "debug" +//! +//! [core] //! mode = "public" //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" diff --git a/src/lib.rs b/src/lib.rs index 39d0b5b3d..2ed88a68b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,12 +167,15 @@ //! The default configuration is: //! //! ```toml +//! [logging] +//! log_level = "info" +//! +//! [core] //! announce_interval = 120 //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" //! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 -//! log_level = "info" //! max_peer_timeout = 900 //! min_announce_interval = 120 //! mode = "public" From 77dd938f807cbf7d2e423f2ee584ca7b75287f10 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:08:23 +0100 Subject: [PATCH 111/217] feat!: [#878] make log_level config value mandatory Althought, it has a default value `info` so you can omit it in the TOML config file. --- packages/configuration/src/v1/logging.rs | 7 +++---- packages/test-helpers/src/configuration.rs | 2 +- src/bootstrap/logging.rs | 19 ++++++++----------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/configuration/src/v1/logging.rs b/packages/configuration/src/v1/logging.rs index c85564a05..e33522db4 100644 --- a/packages/configuration/src/v1/logging.rs +++ b/packages/configuration/src/v1/logging.rs @@ -8,7 +8,7 @@ pub struct Logging { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. #[serde(default = "Logging::default_log_level")] - pub log_level: Option, + pub log_level: LogLevel, } impl Default for Logging { @@ -20,8 +20,7 @@ impl Default for Logging { } impl Logging { - #[allow(clippy::unnecessary_wraps)] - fn default_log_level() -> Option { - Some(LogLevel::Info) + fn default_log_level() -> LogLevel { + LogLevel::Info } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c35d0a851..f70bebcf7 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -29,7 +29,7 @@ pub fn ephemeral() -> Configuration { let mut config = Configuration::default(); - config.logging.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging + config.logging.log_level = LogLevel::Off; // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index f6868602d..649495dc7 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -20,7 +20,7 @@ static INIT: Once = Once::new(); /// It redirects the log info to the standard output with the log level defined in the configuration pub fn setup(cfg: &Configuration) { - let tracing_level = config_level_or_default(&cfg.logging.log_level); + let tracing_level = map_to_tracing_level_filter(&cfg.logging.log_level); if tracing_level == LevelFilter::OFF { return; @@ -31,17 +31,14 @@ pub fn setup(cfg: &Configuration) { }); } -fn config_level_or_default(log_level: &Option) -> LevelFilter { +fn map_to_tracing_level_filter(log_level: &LogLevel) -> LevelFilter { match log_level { - None => LevelFilter::INFO, - Some(level) => match level { - LogLevel::Off => LevelFilter::OFF, - LogLevel::Error => LevelFilter::ERROR, - LogLevel::Warn => LevelFilter::WARN, - LogLevel::Info => LevelFilter::INFO, - LogLevel::Debug => LevelFilter::DEBUG, - LogLevel::Trace => LevelFilter::TRACE, - }, + LogLevel::Off => LevelFilter::OFF, + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::TRACE, } } From 2f94f6caa5673b898d7873b4570f4c87a26be15f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:39:01 +0100 Subject: [PATCH 112/217] feat!: [#878] extract database section in core config section --- Containerfile | 4 +- compose.yaml | 2 +- docs/containers.md | 7 ++-- packages/configuration/src/v1/core.rs | 29 ++++---------- packages/configuration/src/v1/database.rs | 38 +++++++++++++++++++ packages/configuration/src/v1/mod.rs | 25 +++++++----- packages/test-helpers/src/configuration.rs | 2 +- share/container/entry_script_sh | 10 ++--- .../config/tracker.container.mysql.toml | 6 +-- .../config/tracker.container.sqlite3.toml | 4 +- .../config/tracker.e2e.container.sqlite3.toml | 4 +- src/console/ci/e2e/logs_parser.rs | 18 +-------- src/core/mod.rs | 8 ++-- src/lib.rs | 6 ++- 14 files changed, 92 insertions(+), 71 deletions(-) create mode 100644 packages/configuration/src/v1/database.rs diff --git a/Containerfile b/Containerfile index cdd70e337..d55d2f300 100644 --- a/Containerfile +++ b/Containerfile @@ -96,7 +96,7 @@ RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/ COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER="Sqlite3" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 @@ -104,7 +104,7 @@ ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} -ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/compose.yaml b/compose.yaml index a02302a26..cab5c6d5e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER:-MySQL} - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side diff --git a/docs/containers.md b/docs/containers.md index ff15cd7cc..1a1ea2f0d 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -149,7 +149,7 @@ The following environmental variables can be set: - `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). - `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. - `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). @@ -243,8 +243,9 @@ podman run -it \ The docker-compose configuration includes the MySQL service configuration. If you want to use MySQL instead of SQLite you should verify the `/etc/torrust/tracker/tracker.toml` (i.e `./storage/tracker/etc/tracker.toml`) configuration: ```toml -db_driver = "MySQL" -db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" +[core.database] +driver = "MySQL" +path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" ``` ### Build and Run: diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index ae66f54fa..17ac36ee0 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -1,8 +1,9 @@ use std::net::{IpAddr, Ipv4Addr}; use serde::{Deserialize, Serialize}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +use torrust_tracker_primitives::TrackerMode; +use crate::v1::database::Database; use crate::AnnouncePolicy; #[allow(clippy::struct_excessive_bools)] @@ -12,18 +13,9 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - #[serde(default = "Core::default_db_driver")] - pub db_driver: DatabaseDriver, - - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - #[serde(default = "Core::default_db_path")] - pub db_path: String, + // Database configuration. + #[serde(default = "Core::default_database")] + pub database: Database, /// See [`AnnouncePolicy::interval`] #[serde(default = "AnnouncePolicy::default_interval")] @@ -87,8 +79,7 @@ impl Default for Core { Self { mode: Self::default_mode(), - db_driver: Self::default_db_driver(), - db_path: Self::default_db_path(), + database: Self::default_database(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), @@ -107,12 +98,8 @@ impl Core { TrackerMode::Public } - fn default_db_driver() -> DatabaseDriver { - DatabaseDriver::Sqlite3 - } - - fn default_db_path() -> String { - String::from("./storage/tracker/lib/database/sqlite3.db") + fn default_database() -> Database { + Database::default() } fn default_on_reverse_proxy() -> bool { diff --git a/packages/configuration/src/v1/database.rs b/packages/configuration/src/v1/database.rs new file mode 100644 index 000000000..b029175ce --- /dev/null +++ b/packages/configuration/src/v1/database.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::DatabaseDriver; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Database { + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + #[serde(default = "Database::default_driver")] + pub driver: DatabaseDriver, + + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + #[serde(default = "Database::default_path")] + pub path: String, +} + +impl Default for Database { + fn default() -> Self { + Self { + driver: Self::default_driver(), + path: Self::default_path(), + } + } +} + +impl Database { + fn default_driver() -> DatabaseDriver { + DatabaseDriver::Sqlite3 + } + + fn default_path() -> String { + String::from("./storage/tracker/lib/database/sqlite3.db") + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 809970506..5b3cad3ea 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -198,8 +198,6 @@ //! //! [core] //! mode = "public" -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! announce_interval = 120 //! min_announce_interval = 120 //! on_reverse_proxy = false @@ -210,6 +208,10 @@ //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! //! [[udp_trackers]] //! enabled = false //! bind_address = "0.0.0.0:6969" @@ -234,6 +236,7 @@ //! bind_address = "127.0.0.1:1313" //!``` pub mod core; +pub mod database; pub mod health_check_api; pub mod http_tracker; pub mod logging; @@ -382,8 +385,6 @@ mod tests { [core] mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" announce_interval = 120 min_announce_interval = 120 on_reverse_proxy = false @@ -394,6 +395,10 @@ mod tests { inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true + [core.database] + driver = "Sqlite3" + path = "./storage/tracker/lib/database/sqlite3.db" + [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" @@ -490,8 +495,8 @@ mod tests { fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { let config_toml = r#" - [core] - db_path = "OVERWRITTEN DEFAULT DB PATH" + [core.database] + path = "OVERWRITTEN DEFAULT DB PATH" "# .to_string(); @@ -502,7 +507,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); @@ -514,8 +519,8 @@ mod tests { jail.create_file( "tracker.toml", r#" - [core] - db_path = "OVERWRITTEN DEFAULT DB PATH" + [core.database] + path = "OVERWRITTEN DEFAULT DB PATH" "#, )?; @@ -526,7 +531,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index f70bebcf7..fe05407d9 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -54,7 +54,7 @@ pub fn ephemeral() -> Configuration { let temp_directory = env::temp_dir(); let random_db_id = random::string(16); let temp_file = temp_directory.join(format!("data_{random_db_id}.db")); - temp_file.to_str().unwrap().clone_into(&mut config.core.db_path); + temp_file.to_str().unwrap().clone_into(&mut config.core.database.path); config } diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 51df717c6..0668114fd 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,8 +26,8 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "Sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" "Sqlite3"; then # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" @@ -35,7 +35,7 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "MySQL"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" "MySQL"; then # (no database file needed for MySQL) @@ -43,12 +43,12 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER\"." echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 617450562..75cc57b64 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,6 +1,6 @@ -[core] -db_driver = "MySQL" -db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" +[core.database] +driver = "MySQL" +path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" [[http_trackers]] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 01ca655c3..433e36127 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,5 +1,5 @@ -[core] -db_path = "/var/lib/torrust/tracker/database/sqlite3.db" +[core.database] +path = "/var/lib/torrust/tracker/database/sqlite3.db" [[http_trackers]] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 60d7a798a..b8adedefb 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,5 +1,5 @@ -[core] -db_path = "/var/lib/torrust/tracker/database/sqlite3.db" +[core.database] +path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] enabled = true diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index a4024f29d..4886786de 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -112,21 +112,7 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let logs = r#" - Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" - - [[udp_trackers]] - enabled = true - - [[http_trackers]] - enabled = true - ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" - ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - - [http_api] - ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" - ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - + let logs = r" Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 @@ -139,7 +125,7 @@ mod tests { 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 - "#; + "; let running_services = RunningServices::parse_from_logs(logs); diff --git a/src/core/mod.rs b/src/core/mod.rs index 1b60ad6f9..c5171ab58 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -317,8 +317,6 @@ //! //! [core] //! mode = "public" -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! announce_interval = 120 //! min_announce_interval = 120 //! max_peer_timeout = 900 @@ -328,6 +326,10 @@ //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false +//! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" //! ``` //! //! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. @@ -548,7 +550,7 @@ impl Tracker { stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); + let database = Arc::new(databases::driver::build(&config.database.driver, &config.database.path)?); let mode = config.mode.clone(); diff --git a/src/lib.rs b/src/lib.rs index 2ed88a68b..b94da4717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,8 +172,6 @@ //! //! [core] //! announce_interval = 120 -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 @@ -184,6 +182,10 @@ //! remove_peerless_torrents = true //! tracker_usage_statistics = true //! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" //! enabled = false From edc706cc146035d586e86741da7b1df1db4bd08d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:59:42 +0100 Subject: [PATCH 113/217] feat!: [#878] extract net section in core config section --- packages/configuration/src/v1/core.rs | 55 ++++++++-------------- packages/configuration/src/v1/mod.rs | 34 ++++++++----- packages/configuration/src/v1/network.rs | 41 ++++++++++++++++ packages/test-helpers/src/configuration.rs | 6 +-- src/core/mod.rs | 16 ++++--- src/lib.rs | 6 ++- src/servers/http/v1/services/announce.rs | 2 +- src/servers/udp/handlers.rs | 2 +- 8 files changed, 99 insertions(+), 63 deletions(-) create mode 100644 packages/configuration/src/v1/network.rs diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 17ac36ee0..5d8ab0b33 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -1,8 +1,7 @@ -use std::net::{IpAddr, Ipv4Addr}; - use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::TrackerMode; +use super::network::Network; use crate::v1::database::Database; use crate::AnnouncePolicy; @@ -13,10 +12,6 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - // Database configuration. - #[serde(default = "Core::default_database")] - pub database: Database, - /// See [`AnnouncePolicy::interval`] #[serde(default = "AnnouncePolicy::default_interval")] pub announce_interval: u32, @@ -25,20 +20,6 @@ pub struct Core { #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - #[serde(default = "Core::default_on_reverse_proxy")] - pub on_reverse_proxy: bool, - - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - #[serde(default = "Core::default_external_ip")] - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -71,6 +52,14 @@ pub struct Core { /// enabled. #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, + + // Database configuration. + #[serde(default = "Core::default_database")] + pub database: Database, + + // Network configuration. + #[serde(default = "Core::default_network")] + pub net: Network, } impl Default for Core { @@ -79,16 +68,15 @@ impl Default for Core { Self { mode: Self::default_mode(), - database: Self::default_database(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), - on_reverse_proxy: Self::default_on_reverse_proxy(), - external_ip: Self::default_external_ip(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), remove_peerless_torrents: Self::default_remove_peerless_torrents(), + database: Self::default_database(), + net: Self::default_network(), } } } @@ -98,19 +86,6 @@ impl Core { TrackerMode::Public } - fn default_database() -> Database { - Database::default() - } - - fn default_on_reverse_proxy() -> bool { - false - } - - #[allow(clippy::unnecessary_wraps)] - fn default_external_ip() -> Option { - Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) - } - fn default_tracker_usage_statistics() -> bool { true } @@ -130,4 +105,12 @@ impl Core { fn default_remove_peerless_torrents() -> bool { true } + + fn default_database() -> Database { + Database::default() + } + + fn default_network() -> Network { + Network::default() + } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 5b3cad3ea..d96e1335c 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -200,17 +200,19 @@ //! mode = "public" //! announce_interval = 120 //! min_announce_interval = 120 -//! on_reverse_proxy = false -//! external_ip = "0.0.0.0" //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = false //! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false //! //! [[udp_trackers]] //! enabled = false @@ -240,6 +242,7 @@ pub mod database; pub mod health_check_api; pub mod http_tracker; pub mod logging; +pub mod network; pub mod tracker_api; pub mod udp_tracker; @@ -307,7 +310,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - self.core.external_ip.as_ref().map(|external_ip| *external_ip) + self.core.net.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -387,18 +390,20 @@ mod tests { mode = "public" announce_interval = 120 min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" tracker_usage_statistics = true persistent_torrent_completed_stat = false max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true - [core.database] - driver = "Sqlite3" - path = "./storage/tracker/lib/database/sqlite3.db" - + [core.database] + driver = "Sqlite3" + path = "./storage/tracker/lib/database/sqlite3.db" + + [core.net] + external_ip = "0.0.0.0" + on_reverse_proxy = false + [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" @@ -443,7 +448,10 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.core.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); + assert_eq!( + configuration.core.net.external_ip, + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + ); } #[test] diff --git a/packages/configuration/src/v1/network.rs b/packages/configuration/src/v1/network.rs new file mode 100644 index 000000000..8e53d419c --- /dev/null +++ b/packages/configuration/src/v1/network.rs @@ -0,0 +1,41 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use serde::{Deserialize, Serialize}; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Network { + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + #[serde(default = "Network::default_external_ip")] + pub external_ip: Option, + + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + #[serde(default = "Network::default_on_reverse_proxy")] + pub on_reverse_proxy: bool, +} + +impl Default for Network { + fn default() -> Self { + Self { + external_ip: Self::default_external_ip(), + on_reverse_proxy: Self::default_on_reverse_proxy(), + } + } +} + +impl Network { + #[allow(clippy::unnecessary_wraps)] + fn default_external_ip() -> Option { + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + } + + fn default_on_reverse_proxy() -> bool { + false + } +} diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index fe05407d9..9c6c0fe11 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -64,7 +64,7 @@ pub fn ephemeral() -> Configuration { pub fn ephemeral_with_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.core.on_reverse_proxy = true; + cfg.core.net.on_reverse_proxy = true; cfg } @@ -74,7 +74,7 @@ pub fn ephemeral_with_reverse_proxy() -> Configuration { pub fn ephemeral_without_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.core.on_reverse_proxy = false; + cfg.core.net.on_reverse_proxy = false; cfg } @@ -124,7 +124,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.core.external_ip = Some(ip); + cfg.core.net.external_ip = Some(ip); cfg } diff --git a/src/core/mod.rs b/src/core/mod.rs index c5171ab58..f4eb2c335 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -320,16 +320,18 @@ //! announce_interval = 120 //! min_announce_interval = 120 //! max_peer_timeout = 900 -//! on_reverse_proxy = false -//! external_ip = "2.137.87.41" //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! on_reverse_proxy = false +//! external_ip = "2.137.87.41" //! ``` //! //! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. @@ -564,13 +566,13 @@ impl Tracker { stats_event_sender, stats_repository, database, - external_ip: config.external_ip, + external_ip: config.net.external_ip, policy: TrackerPolicy::new( config.remove_peerless_torrents, config.max_peer_timeout, config.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.on_reverse_proxy, + on_reverse_proxy: config.net.on_reverse_proxy, }) } diff --git a/src/lib.rs b/src/lib.rs index b94da4717..bf9257123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,12 +172,10 @@ //! //! [core] //! announce_interval = 120 -//! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 //! min_announce_interval = 120 //! mode = "public" -//! on_reverse_proxy = false //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! tracker_usage_statistics = true @@ -186,6 +184,10 @@ //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" //! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false +//! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" //! enabled = false diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 253140fbc..eee5e4688 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -151,7 +151,7 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.core.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + configuration.core.net.external_ip = Some(IpAddr::V6(Ipv6Addr::new( 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index 858d6606c..36825f084 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -425,7 +425,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.core.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); + self.configuration.core.net.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From fc046e0441302c3af8db3a1b1173d38e4383369e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 17:21:03 +0100 Subject: [PATCH 114/217] feat!: [#878] extract announce_policy section in core config section --- packages/configuration/src/lib.rs | 4 +++- packages/configuration/src/v1/core.rs | 21 +++++++++------------ packages/configuration/src/v1/mod.rs | 12 ++++++++---- src/core/mod.rs | 14 ++++++++------ src/lib.rs | 18 ++++++++++-------- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 46ece96ab..3b719f742 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -86,7 +86,7 @@ impl Info { } /// Announce policy -#[derive(PartialEq, Eq, Debug, Clone, Copy, Constructor)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, Constructor)] pub struct AnnouncePolicy { /// Interval in seconds that the client should wait between sending regular /// announce requests to the tracker. @@ -99,6 +99,7 @@ pub struct AnnouncePolicy { /// client's initial request. It serves as a guideline for clients to know /// how often they should contact the tracker for updates on the peer list, /// while ensuring that the tracker is not overwhelmed with requests. + #[serde(default = "AnnouncePolicy::default_interval")] pub interval: u32, /// Minimum announce interval. Clients must not reannounce more frequently @@ -112,6 +113,7 @@ pub struct AnnouncePolicy { /// value to prevent sending too many requests in a short period, which /// could lead to excessive load on the tracker or even getting banned by /// the tracker for not adhering to the rules. + #[serde(default = "AnnouncePolicy::default_interval_min")] pub interval_min: u32, } diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 5d8ab0b33..266da21ed 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -12,14 +12,6 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - /// See [`AnnouncePolicy::interval`] - #[serde(default = "AnnouncePolicy::default_interval")] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - #[serde(default = "AnnouncePolicy::default_interval_min")] - pub min_announce_interval: u32, - /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -53,6 +45,10 @@ pub struct Core { #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, + // Announce policy configuration. + #[serde(default = "Core::default_announce_policy")] + pub announce_policy: AnnouncePolicy, + // Database configuration. #[serde(default = "Core::default_database")] pub database: Database, @@ -64,17 +60,14 @@ pub struct Core { impl Default for Core { fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - Self { mode: Self::default_mode(), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), remove_peerless_torrents: Self::default_remove_peerless_torrents(), + announce_policy: Self::default_announce_policy(), database: Self::default_database(), net: Self::default_network(), } @@ -106,6 +99,10 @@ impl Core { true } + fn default_announce_policy() -> AnnouncePolicy { + AnnouncePolicy::default() + } + fn default_database() -> Database { Database::default() } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d96e1335c..d2f2a3012 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -198,14 +198,16 @@ //! //! [core] //! mode = "public" -//! announce_interval = 120 -//! min_announce_interval = 120 //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = false //! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 +//! //! [core.database] //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" @@ -388,14 +390,16 @@ mod tests { [core] mode = "public" - announce_interval = 120 - min_announce_interval = 120 tracker_usage_statistics = true persistent_torrent_completed_stat = false max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true + [core.announce_policy] + interval = 120 + interval_min = 120 + [core.database] driver = "Sqlite3" path = "./storage/tracker/lib/database/sqlite3.db" diff --git a/src/core/mod.rs b/src/core/mod.rs index f4eb2c335..77f7099af 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -113,10 +113,10 @@ //! } //! //! // Core tracker configuration -//! pub struct Configuration { +//! pub struct AnnounceInterval { //! // ... -//! pub announce_interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker -//! pub min_announce_interval: u32, // Minimum announce interval. Clients must not reannounce more frequently than this +//! pub interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker +//! pub interval_min: u32, // Minimum announce interval. Clients must not reannounce more frequently than this //! // ... //! } //! ``` @@ -317,14 +317,16 @@ //! //! [core] //! mode = "public" -//! announce_interval = 120 -//! min_announce_interval = 120 //! max_peer_timeout = 900 //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false //! +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 +//! //! [core.database] //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" @@ -558,7 +560,7 @@ impl Tracker { Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), + announce_policy: config.announce_policy, mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), diff --git a/src/lib.rs b/src/lib.rs index bf9257123..5c0fd4b56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,22 +171,24 @@ //! log_level = "info" //! //! [core] -//! announce_interval = 120 //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 -//! min_announce_interval = 120 //! mode = "public" //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! tracker_usage_statistics = true //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 //! -//! [core.net] -//! external_ip = "0.0.0.0" -//! on_reverse_proxy = false +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false //! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" From 7b2f75724494c883c8e0d6faae9153c4ab47a562 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 18:19:57 +0100 Subject: [PATCH 115/217] feat!: [#878] extract tracker_policy section in core config section --- packages/configuration/src/lib.rs | 44 ++++++++++++++++++- packages/configuration/src/v1/core.rs | 40 +++-------------- packages/configuration/src/v1/mod.rs | 12 +++-- .../torrent-repository/tests/entry/mod.rs | 8 ++-- .../tests/repository/mod.rs | 8 ++-- src/core/mod.rs | 16 +++---- src/lib.rs | 6 ++- 7 files changed, 76 insertions(+), 58 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 3b719f742..594a283db 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -37,11 +37,51 @@ pub type HealthCheckApi = v1::health_check_api::HealthCheckApi; pub type AccessTokens = HashMap; -#[derive(Copy, Clone, Debug, PartialEq, Constructor)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Constructor)] pub struct TrackerPolicy { - pub remove_peerless_torrents: bool, + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + #[serde(default = "TrackerPolicy::default_max_peer_timeout")] pub max_peer_timeout: u32, + + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + #[serde(default = "TrackerPolicy::default_persistent_torrent_completed_stat")] pub persistent_torrent_completed_stat: bool, + + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + #[serde(default = "TrackerPolicy::default_remove_peerless_torrents")] + pub remove_peerless_torrents: bool, +} + +impl Default for TrackerPolicy { + fn default() -> Self { + Self { + max_peer_timeout: Self::default_max_peer_timeout(), + persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), + remove_peerless_torrents: Self::default_remove_peerless_torrents(), + } + } +} + +impl TrackerPolicy { + fn default_max_peer_timeout() -> u32 { + 900 + } + + fn default_persistent_torrent_completed_stat() -> bool { + false + } + + fn default_remove_peerless_torrents() -> bool { + true + } } /// Information required for loading config diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 266da21ed..49fdf2a80 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -3,7 +3,7 @@ use torrust_tracker_primitives::TrackerMode; use super::network::Network; use crate::v1::database::Database; -use crate::AnnouncePolicy; +use crate::{AnnouncePolicy, TrackerPolicy}; #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -20,30 +20,14 @@ pub struct Core { #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - #[serde(default = "Core::default_persistent_torrent_completed_stat")] - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - #[serde(default = "Core::default_max_peer_timeout")] - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - #[serde(default = "Core::default_remove_peerless_torrents")] - pub remove_peerless_torrents: bool, + // Tracker policy configuration. + #[serde(default = "Core::default_tracker_policy")] + pub tracker_policy: TrackerPolicy, // Announce policy configuration. #[serde(default = "Core::default_announce_policy")] @@ -62,11 +46,9 @@ impl Default for Core { fn default() -> Self { Self { mode: Self::default_mode(), - max_peer_timeout: Self::default_max_peer_timeout(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), - persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), - remove_peerless_torrents: Self::default_remove_peerless_torrents(), + tracker_policy: Self::default_tracker_policy(), announce_policy: Self::default_announce_policy(), database: Self::default_database(), net: Self::default_network(), @@ -83,20 +65,12 @@ impl Core { true } - fn default_persistent_torrent_completed_stat() -> bool { - false - } - - fn default_max_peer_timeout() -> u32 { - 900 - } - fn default_inactive_peer_cleanup_interval() -> u64 { 600 } - fn default_remove_peerless_torrents() -> bool { - true + fn default_tracker_policy() -> TrackerPolicy { + TrackerPolicy::default() } fn default_announce_policy() -> AnnouncePolicy { diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d2f2a3012..19499cd4a 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -199,9 +199,11 @@ //! [core] //! mode = "public" //! tracker_usage_statistics = true -//! persistent_torrent_completed_stat = false -//! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 +//! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! //! [core.announce_policy] @@ -391,9 +393,11 @@ mod tests { [core] mode = "public" tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 + + [core.tracker_policy] + max_peer_timeout = 900 + persistent_torrent_completed_stat = false remove_peerless_torrents = true [core.announce_policy] diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index 3b9f3e3ad..fdbe211b3 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -43,22 +43,22 @@ fn rw_lock_parking_lot() -> Torrent { #[fixture] fn policy_none() -> TrackerPolicy { - TrackerPolicy::new(false, 0, false) + TrackerPolicy::new(0, false, false) } #[fixture] fn policy_persist() -> TrackerPolicy { - TrackerPolicy::new(false, 0, true) + TrackerPolicy::new(0, true, false) } #[fixture] fn policy_remove() -> TrackerPolicy { - TrackerPolicy::new(true, 0, false) + TrackerPolicy::new(0, false, true) } #[fixture] fn policy_remove_persist() -> TrackerPolicy { - TrackerPolicy::new(true, 0, true) + TrackerPolicy::new(0, true, true) } pub enum Makes { diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index dd9893cc9..b10f4a64a 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -220,22 +220,22 @@ fn paginated_limit_one_offset_one() -> Pagination { #[fixture] fn policy_none() -> TrackerPolicy { - TrackerPolicy::new(false, 0, false) + TrackerPolicy::new(0, false, false) } #[fixture] fn policy_persist() -> TrackerPolicy { - TrackerPolicy::new(false, 0, true) + TrackerPolicy::new(0, true, false) } #[fixture] fn policy_remove() -> TrackerPolicy { - TrackerPolicy::new(true, 0, false) + TrackerPolicy::new(0, false, true) } #[fixture] fn policy_remove_persist() -> TrackerPolicy { - TrackerPolicy::new(true, 0, true) + TrackerPolicy::new(0, true, true) } #[rstest] diff --git a/src/core/mod.rs b/src/core/mod.rs index 77f7099af..bd7ce4883 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -317,11 +317,13 @@ //! //! [core] //! mode = "public" -//! max_peer_timeout = 900 //! tracker_usage_statistics = true -//! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 -//! remove_peerless_torrents = false +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 +//! persistent_torrent_completed_stat = false +//! remove_peerless_torrents = true //! //! [core.announce_policy] //! interval = 120 @@ -569,11 +571,7 @@ impl Tracker { stats_repository, database, external_ip: config.net.external_ip, - policy: TrackerPolicy::new( - config.remove_peerless_torrents, - config.max_peer_timeout, - config.persistent_torrent_completed_stat, - ), + policy: config.tracker_policy.clone(), on_reverse_proxy: config.net.on_reverse_proxy, }) } @@ -1043,7 +1041,7 @@ mod tests { pub fn tracker_persisting_torrents_in_database() -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.core.persistent_torrent_completed_stat = true; + configuration.core.tracker_policy.persistent_torrent_completed_stat = true; tracker_factory(&configuration) } diff --git a/src/lib.rs b/src/lib.rs index 5c0fd4b56..c059f6df7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,11 +172,13 @@ //! //! [core] //! inactive_peer_cleanup_interval = 600 -//! max_peer_timeout = 900 //! mode = "public" +//! tracker_usage_statistics = true +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true -//! tracker_usage_statistics = true //! //! [core.announce_policy] //! interval = 120 From c5cc9fd6a461ad439fb81b405ecbe652d82fd4fb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 19:34:56 +0100 Subject: [PATCH 116/217] feat: [#878] extract tsl_config in toml config TSL configuration for HTTP trackers and the Tracker API is still optional. However, when it's provided is enabled. The `ssl_enabled` field was removed. You can remove the whole `tsl_config` to disable TSL. If you want to kee a copy in the TOML file you can just comment the lines. ```toml [[http_trackers]] ... [http_trackers.tsl_config] ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" ssl_key_path = "./storage/tracker/lib/tls/localhost.key" [http_api] ... [http_api.tsl_config] ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" ssl_key_path = "./storage/tracker/lib/tls/localhost.key" ``` --- Cargo.toml | 2 +- docs/containers.md | 23 +++--- packages/configuration/Cargo.toml | 2 +- packages/configuration/src/lib.rs | 17 +++-- packages/configuration/src/v1/http_tracker.rs | 19 +++-- packages/configuration/src/v1/mod.rs | 20 ++---- packages/configuration/src/v1/tracker_api.rs | 19 +++-- .../config/tracker.container.mysql.toml | 8 +-- .../config/tracker.container.sqlite3.toml | 8 --- .../config/tracker.e2e.container.sqlite3.toml | 6 -- src/bootstrap/jobs/http_tracker.rs | 2 +- src/bootstrap/jobs/mod.rs | 70 +++++++++---------- src/bootstrap/jobs/tracker_apis.rs | 2 +- src/lib.rs | 9 --- src/servers/apis/mod.rs | 12 ++-- src/servers/apis/server.rs | 2 +- src/servers/http/server.rs | 2 +- tests/servers/api/environment.rs | 2 +- tests/servers/http/environment.rs | 2 +- 19 files changed, 92 insertions(+), 135 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 072a21a7e..c22c3dd45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ axum = { version = "0", features = ["macros"] } axum-client-ip = "0" axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } -camino = { version = "1.1.6", features = ["serde"] } +camino = { version = "1.1.6", features = ["serde", "serde1"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" diff --git a/docs/containers.md b/docs/containers.md index 1a1ea2f0d..82c67c26e 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -330,24 +330,23 @@ The storage folder must contain your certificates: ```s storage/tracker/lib/tls - ├── localhost.crt - └── localhost.key + ├── localhost.crt + └── localhost.key +storage/http_api/lib/tls + ├── localhost.crt + └── localhost.key ``` You have not enabled it in your `tracker.toml` file: ```toml +[http_trackers.tsl_config] +ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -[[http_trackers]] -# ... -ssl_enabled = true -# ... - -[http_api] -# ... -ssl_enabled = true -# ... - +[http_api.tsl_config] +ssl_cert_path = "./storage/http_api/lib/tls/localhost.crt" +ssl_key_path = "./storage/http_api/lib/tls/localhost.key" ``` > NOTE: you can enable it independently for each HTTP tracker or the API. diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index bac2132d5..53e4e4cfa 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,7 +15,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -camino = { version = "1.1.6", features = ["serde"] } +camino = { version = "1.1.6", features = ["serde", "serde1"] } derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 594a283db..c8c91443a 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use camino::Utf8PathBuf; use derive_more::Constructor; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; @@ -215,24 +215,23 @@ impl From for Error { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] pub struct TslConfig { /// Path to the SSL certificate file. - #[serde_as(as = "NoneAsEmptyString")] #[serde(default = "TslConfig::default_ssl_cert_path")] - pub ssl_cert_path: Option, + pub ssl_cert_path: Utf8PathBuf, + /// Path to the SSL key file. - #[serde_as(as = "NoneAsEmptyString")] #[serde(default = "TslConfig::default_ssl_key_path")] - pub ssl_key_path: Option, + pub ssl_key_path: Utf8PathBuf, } impl TslConfig { #[allow(clippy::unnecessary_wraps)] - fn default_ssl_cert_path() -> Option { - Some(Utf8PathBuf::new()) + fn default_ssl_cert_path() -> Utf8PathBuf { + Utf8PathBuf::new() } #[allow(clippy::unnecessary_wraps)] - fn default_ssl_key_path() -> Option { - Some(Utf8PathBuf::new()) + fn default_ssl_key_path() -> Utf8PathBuf { + Utf8PathBuf::new() } } diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index b1fe1437b..fed2282a5 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -12,19 +12,17 @@ pub struct HttpTracker { /// Weather the HTTP tracker is enabled or not. #[serde(default = "HttpTracker::default_enabled")] pub enabled: bool, + /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. #[serde(default = "HttpTracker::default_bind_address")] pub bind_address: SocketAddr, - /// Weather the HTTP tracker will use SSL or not. - #[serde(default = "HttpTracker::default_ssl_enabled")] - pub ssl_enabled: bool, - /// TSL config. Only used if `ssl_enabled` is true. - #[serde(flatten)] - #[serde(default = "TslConfig::default")] - pub tsl_config: TslConfig, + + /// TSL config. + #[serde(default = "HttpTracker::default_tsl_config")] + pub tsl_config: Option, } impl Default for HttpTracker { @@ -32,8 +30,7 @@ impl Default for HttpTracker { Self { enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), - ssl_enabled: Self::default_ssl_enabled(), - tsl_config: TslConfig::default(), + tsl_config: Self::default_tsl_config(), } } } @@ -47,7 +44,7 @@ impl HttpTracker { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) } - fn default_ssl_enabled() -> bool { - false + fn default_tsl_config() -> Option { + None } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 19499cd4a..603be85d2 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -176,14 +176,16 @@ //! //! ```s //! [[http_trackers]] -//! enabled = true //! ... +//! +//! [http_trackers.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! //! [http_api] -//! enabled = true //! ... +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! ``` @@ -225,16 +227,10 @@ //! [[http_trackers]] //! enabled = false //! bind_address = "0.0.0.0:7070" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! //! [http_api] //! enabled = true //! bind_address = "127.0.0.1:1212" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! //! [http_api.access_tokens] //! admin = "MyAccessToken" @@ -419,16 +415,10 @@ mod tests { [[http_trackers]] enabled = false bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - + [http_api] enabled = true bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" [http_api.access_tokens] admin = "MyAccessToken" diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index c2e3e5ee9..42794ad18 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -15,19 +15,18 @@ pub struct HttpApi { /// Weather the HTTP API is enabled or not. #[serde(default = "HttpApi::default_enabled")] pub enabled: bool, + /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. #[serde(default = "HttpApi::default_bind_address")] pub bind_address: SocketAddr, - /// Weather the HTTP API will use SSL or not. - #[serde(default = "HttpApi::default_ssl_enabled")] - pub ssl_enabled: bool, + /// TSL config. Only used if `ssl_enabled` is true. - #[serde(flatten)] - #[serde(default = "TslConfig::default")] - pub tsl_config: TslConfig, + #[serde(default = "HttpApi::default_tsl_config")] + pub tsl_config: Option, + /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have @@ -41,8 +40,7 @@ impl Default for HttpApi { Self { enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), - ssl_enabled: Self::default_ssl_enabled(), - tsl_config: TslConfig::default(), + tsl_config: Self::default_tsl_config(), access_tokens: Self::default_access_tokens(), } } @@ -57,8 +55,9 @@ impl HttpApi { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) } - fn default_ssl_enabled() -> bool { - false + #[allow(clippy::unnecessary_wraps)] + fn default_tsl_config() -> Option { + None } fn default_access_tokens() -> AccessTokens { diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 75cc57b64..70ee8b500 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -3,9 +3,5 @@ driver = "MySQL" path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" [[http_trackers]] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" +bind_address = "0.0.0.0:7070" +enabled = true diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 433e36127..f7bb6b8bb 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,10 +1,2 @@ [core.database] path = "/var/lib/torrust/tracker/database/sqlite3.db" - -[[http_trackers]] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index b8adedefb..744d267fd 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -6,12 +6,6 @@ enabled = true [[http_trackers]] enabled = true -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [health_check_api] # Must be bound to wildcard IP to be accessible from outside the container diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index e9eb6bc16..05bfe2341 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -42,7 +42,7 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + let tls = make_rust_tls(&config.tsl_config) .await .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 316e5746c..f42a843f4 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -20,27 +20,33 @@ pub struct Started { pub address: std::net::SocketAddr, } -pub async fn make_rust_tls(enabled: bool, tsl_config: &TslConfig) -> Option> { - if !enabled { - info!("TLS not enabled"); - return None; - } +pub async fn make_rust_tls(opt_tsl_config: &Option) -> Option> { + match opt_tsl_config { + Some(tsl_config) => { + let cert = tsl_config.ssl_cert_path.clone(); + let key = tsl_config.ssl_key_path.clone(); - if let (Some(cert), Some(key)) = (tsl_config.ssl_cert_path.clone(), tsl_config.ssl_key_path.clone()) { - info!("Using https: cert path: {cert}."); - info!("Using https: key path: {key}."); - - Some( - RustlsConfig::from_pem_file(cert, key) - .await - .map_err(|err| Error::BadTlsConfig { - source: (Arc::new(err) as DynError).into(), - }), - ) - } else { - Some(Err(Error::MissingTlsConfig { - location: Location::caller(), - })) + if !cert.exists() || !key.exists() { + return Some(Err(Error::MissingTlsConfig { + location: Location::caller(), + })); + } + + info!("Using https: cert path: {cert}."); + info!("Using https: key path: {key}."); + + Some( + RustlsConfig::from_pem_file(cert, key) + .await + .map_err(|err| Error::BadTlsConfig { + source: (Arc::new(err) as DynError).into(), + }), + ) + } + None => { + info!("TLS not enabled"); + None + } } } @@ -54,29 +60,23 @@ mod tests { #[tokio::test] async fn it_should_error_on_bad_tls_config() { - let err = make_rust_tls( - true, - &TslConfig { - ssl_cert_path: Some(Utf8PathBuf::from("bad cert path")), - ssl_key_path: Some(Utf8PathBuf::from("bad key path")), - }, - ) + let err = make_rust_tls(&Some(TslConfig { + ssl_cert_path: Utf8PathBuf::from("bad cert path"), + ssl_key_path: Utf8PathBuf::from("bad key path"), + })) .await .expect("tls_was_enabled") .expect_err("bad_cert_and_key_files"); - assert!(matches!(err, Error::BadTlsConfig { source: _ })); + assert!(matches!(err, Error::MissingTlsConfig { location: _ })); } #[tokio::test] async fn it_should_error_on_missing_cert_or_key_paths() { - let err = make_rust_tls( - true, - &TslConfig { - ssl_cert_path: None, - ssl_key_path: None, - }, - ) + let err = make_rust_tls(&Some(TslConfig { + ssl_cert_path: Utf8PathBuf::from(""), + ssl_key_path: Utf8PathBuf::from(""), + })) .await .expect("tls_was_enabled") .expect_err("missing_config"); diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 3c1f13255..c3b12d7a1 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -63,7 +63,7 @@ pub async fn start_job( if config.enabled { let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + let tls = make_rust_tls(&config.tsl_config) .await .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); diff --git a/src/lib.rs b/src/lib.rs index c059f6df7..7f8c70a47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,16 +199,10 @@ //! [[http_trackers]] //! bind_address = "0.0.0.0:7070" //! enabled = false -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" //! //! [http_api] //! bind_address = "127.0.0.1:1212" //! enabled = true -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" //! //! [http_api.access_tokens] //! admin = "MyAccessToken" @@ -261,9 +255,6 @@ //! [http_api] //! enabled = true //! bind_address = "127.0.0.1:1212" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! ``` //! //! By default it's enabled on port `1212`. You also need to add access tokens in the configuration: diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 47d40c654..02b93efa6 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -27,7 +27,8 @@ //! [http_api] //! enabled = true //! bind_address = "0.0.0.0:1212" -//! ssl_enabled = false +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! @@ -106,16 +107,15 @@ //! //! # Setup SSL (optional) //! -//! The API server supports SSL. You can enable it by setting the -//! [`ssl_enabled`](torrust_tracker_configuration::HttpApi::ssl_enabled) option -//! to `true` in the configuration file -//! ([`http_api`](torrust_tracker_configuration::HttpApi) section). +//! The API server supports SSL. You can enable it by adding the `tsl_config` +//! section to the configuration. //! //! ```toml //! [http_api] //! enabled = true //! bind_address = "0.0.0.0:1212" -//! ssl_enabled = true +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 7c5b8983b..74dc89692 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -280,7 +280,7 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + let tls = make_rust_tls(&config.tsl_config) .await .map(|tls| tls.expect("tls config failed")); diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 5c33fc8fa..bbe0c3cc1 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -239,7 +239,7 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + let tls = make_rust_tls(&config.tsl_config) .await .map(|tls| tls.expect("tls config failed")); diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index cacde8af9..8f84620dd 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -35,7 +35,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(&config.tsl_config)).map(|tls| tls.expect("tls config failed")); let server = ApiServer::new(Launcher::new(bind_to, tls)); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 61837c40f..6e80569ec 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -33,7 +33,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(&config.tsl_config)).map(|tls| tls.expect("tls config failed")); let server = HttpServer::new(Launcher::new(bind_to, tls)); From 50bef25af092414b46d13ce393dacc22b4f9a2cf Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 18 Jun 2024 08:36:57 +0100 Subject: [PATCH 117/217] feat: remove ambiguous log entry It remvoes this line: ``` 2024-06-17T18:52:49.196708Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled ``` becuase it doesn't specifu which service the TSl is not enabled for. On the other hand, the output already indicates whether the service is runnig on HTTP or HTTPs: ``` 2024-06-18T07:37:58.595692Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 ``` --- src/bootstrap/jobs/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index f42a843f4..87a607720 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -44,7 +44,6 @@ pub async fn make_rust_tls(opt_tsl_config: &Option) -> Option { - info!("TLS not enabled"); None } } From 06ad5dabe82ebef947b8b54c19ac9a74eca33335 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 18 Jun 2024 10:11:11 +0100 Subject: [PATCH 118/217] feat!: [#878] remove enabled fields in config By default all services are disabled. If the service section is missing in the TOML config file it means the service is disabled. From: ```toml [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" ``` To: ```toml ``` The `http_api` section has been disabled by default becuase there is no way to override it to disable it, if it's enabled by default. You nned to explicitly enabled the API now. --- docs/benchmarking.md | 4 +- packages/configuration/src/v1/http_tracker.rs | 9 --- packages/configuration/src/v1/mod.rs | 47 ++--------- packages/configuration/src/v1/tracker_api.rs | 9 --- packages/configuration/src/v1/udp_tracker.rs | 8 -- packages/test-helpers/src/configuration.rs | 39 ++++++--- .../config/tracker.container.mysql.toml | 16 +++- .../config/tracker.container.sqlite3.toml | 14 ++++ .../config/tracker.development.sqlite3.toml | 10 ++- .../config/tracker.e2e.container.sqlite3.toml | 10 ++- .../config/tracker.udp.benchmarking.toml | 5 +- src/app.rs | 81 ++++++++++--------- src/bootstrap/jobs/http_tracker.rs | 21 ++--- src/bootstrap/jobs/mod.rs | 4 +- src/bootstrap/jobs/tracker_apis.rs | 22 ++--- src/lib.rs | 21 +---- src/servers/apis/mod.rs | 2 - src/servers/apis/server.rs | 2 +- src/servers/http/server.rs | 5 +- src/servers/udp/server.rs | 3 +- tests/servers/api/environment.rs | 2 +- tests/servers/http/environment.rs | 7 +- tests/servers/udp/environment.rs | 4 +- 23 files changed, 156 insertions(+), 189 deletions(-) diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 67b680fdc..ce3b69057 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -33,7 +33,7 @@ Run the tracker with UDP service enabled and other services disabled and set log log_level = "error" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" ``` Build and run the tracker: @@ -168,7 +168,7 @@ Run the tracker with UDP service enabled and other services disabled and set log log_level = "error" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" ``` ```console diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index fed2282a5..42ec02bf2 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -9,10 +9,6 @@ use crate::TslConfig; #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { - /// Weather the HTTP tracker is enabled or not. - #[serde(default = "HttpTracker::default_enabled")] - pub enabled: bool, - /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -28,7 +24,6 @@ pub struct HttpTracker { impl Default for HttpTracker { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), tsl_config: Self::default_tsl_config(), } @@ -36,10 +31,6 @@ impl Default for HttpTracker { } impl HttpTracker { - fn default_enabled() -> bool { - false - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 603be85d2..546f55b6e 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -220,16 +220,7 @@ //! external_ip = "0.0.0.0" //! on_reverse_proxy = false //! -//! [[udp_trackers]] -//! enabled = false -//! bind_address = "0.0.0.0:6969" -//! -//! [[http_trackers]] -//! enabled = false -//! bind_address = "0.0.0.0:7070" -//! //! [http_api] -//! enabled = true //! bind_address = "127.0.0.1:1212" //! //! [http_api.access_tokens] @@ -267,7 +258,7 @@ const CONFIG_OVERRIDE_PREFIX: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_"; const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; /// Core configuration for the tracker. -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default)] pub struct Configuration { /// Logging configuration pub logging: Logging, @@ -278,33 +269,20 @@ pub struct Configuration { /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. - pub udp_trackers: Vec, + pub udp_trackers: Option>, /// The list of HTTP trackers the tracker is running. Each HTTP tracker /// represents a HTTP server that the tracker is running and it has its own /// configuration. - pub http_trackers: Vec, + pub http_trackers: Option>, /// The HTTP API configuration. - pub http_api: HttpApi, + pub http_api: Option, /// The Health Check API configuration. pub health_check_api: HealthCheckApi, } -impl Default for Configuration { - fn default() -> Self { - Self { - logging: Logging::default(), - core: Core::default(), - udp_trackers: vec![UdpTracker::default()], - http_trackers: vec![HttpTracker::default()], - http_api: HttpApi::default(), - health_check_api: HealthCheckApi::default(), - } - } -} - impl Configuration { /// Returns the tracker public IP address id defined in the configuration, /// and `None` otherwise. @@ -408,21 +386,6 @@ mod tests { external_ip = "0.0.0.0" on_reverse_proxy = false - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - - [http_api.access_tokens] - admin = "MyAccessToken" - [health_check_api] bind_address = "127.0.0.1:1313" "# @@ -556,7 +519,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!( - configuration.http_api.access_tokens.get("admin"), + configuration.http_api.unwrap().access_tokens.get("admin"), Some("NewToken".to_owned()).as_ref() ); diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 42794ad18..302a4ee95 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -12,10 +12,6 @@ pub type AccessTokens = HashMap; #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpApi { - /// Weather the HTTP API is enabled or not. - #[serde(default = "HttpApi::default_enabled")] - pub enabled: bool, - /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -38,7 +34,6 @@ pub struct HttpApi { impl Default for HttpApi { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), tsl_config: Self::default_tsl_config(), access_tokens: Self::default_access_tokens(), @@ -47,10 +42,6 @@ impl Default for HttpApi { } impl HttpApi { - fn default_enabled() -> bool { - true - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index f8387202e..b3d420d72 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -4,9 +4,6 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { - /// Weather the UDP tracker is enabled or not. - #[serde(default = "UdpTracker::default_enabled")] - pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -17,17 +14,12 @@ pub struct UdpTracker { impl Default for UdpTracker { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), } } } impl UdpTracker { - fn default_enabled() -> bool { - false - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969) } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 9c6c0fe11..646617b32 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -2,7 +2,7 @@ use std::env; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use torrust_tracker_configuration::{Configuration, LogLevel}; +use torrust_tracker_configuration::{Configuration, HttpApi, HttpTracker, LogLevel, UdpTracker}; use torrust_tracker_primitives::TrackerMode; use crate::random; @@ -33,8 +33,10 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for API let api_port = 0u16; - config.http_api.enabled = true; - config.http_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port); + config.http_api = Some(HttpApi { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port), + ..Default::default() + }); // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; @@ -42,13 +44,16 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for UDP tracker let udp_port = 0u16; - config.udp_trackers[0].enabled = true; - config.udp_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port); + config.udp_trackers = Some(vec![UdpTracker { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port), + }]); // Ephemeral socket address for HTTP tracker let http_port = 0u16; - config.http_trackers[0].enabled = true; - config.http_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port); + config.http_trackers = Some(vec![HttpTracker { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port), + tsl_config: None, + }]); // Ephemeral sqlite database let temp_directory = env::temp_dir(); @@ -137,9 +142,17 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6); - cfg.http_trackers[0].bind_address.clone_from(&ipv6); - cfg.udp_trackers[0].bind_address = ipv6; + if let Some(ref mut http_api) = cfg.http_api { + http_api.bind_address.clone_from(&ipv6); + }; + + if let Some(ref mut http_trackers) = cfg.http_trackers { + http_trackers[0].bind_address.clone_from(&ipv6); + } + + if let Some(ref mut udp_trackers) = cfg.udp_trackers { + udp_trackers[0].bind_address.clone_from(&ipv6); + } cfg } @@ -149,9 +162,9 @@ pub fn ephemeral_ipv6() -> Configuration { pub fn ephemeral_with_no_services() -> Configuration { let mut cfg = ephemeral(); - cfg.http_api.enabled = false; - cfg.http_trackers[0].enabled = false; - cfg.udp_trackers[0].enabled = false; + cfg.http_api = None; + cfg.http_trackers = None; + cfg.udp_trackers = None; cfg } diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 70ee8b500..68cc8db8a 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -2,6 +2,16 @@ driver = "MySQL" path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" -[[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = true +# Uncomment to enable services + +#[[udp_trackers]] +#bind_address = "0.0.0.0:6969" + +#[[http_trackers]] +#bind_address = "0.0.0.0:7070" + +#[http_api] +#bind_address = "0.0.0.0:1212" + +#[http_api.access_tokens] +#admin = "MyAccessToken" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index f7bb6b8bb..63e169a70 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,2 +1,16 @@ [core.database] path = "/var/lib/torrust/tracker/database/sqlite3.db" + +# Uncomment to enable services + +#[[udp_trackers]] +#bind_address = "0.0.0.0:6969" + +#[[http_trackers]] +#bind_address = "0.0.0.0:7070" + +#[http_api] +#bind_address = "0.0.0.0:1212" + +#[http_api.access_tokens] +#admin = "MyAccessToken" diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index bf6478492..84754794e 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -1,5 +1,11 @@ [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" [[http_trackers]] -enabled = true +bind_address = "0.0.0.0:7070" + +[http_api] +bind_address = "0.0.0.0:1212" + +[http_api.access_tokens] +admin = "MyAccessToken" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 744d267fd..fb33a8e32 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -2,10 +2,16 @@ path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" [[http_trackers]] -enabled = true +bind_address = "0.0.0.0:7070" + +[http_api] +bind_address = "0.0.0.0:1212" + +[http_api.access_tokens] +admin = "MyAccessToken" [health_check_api] # Must be bound to wildcard IP to be accessible from outside the container diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index cd193c40a..d9361cf10 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -6,7 +6,4 @@ remove_peerless_torrents = false tracker_usage_statistics = false [[udp_trackers]] -enabled = true - -[http_api] -enabled = false +bind_address = "0.0.0.0:6969" diff --git a/src/app.rs b/src/app.rs index b41f4098e..f6a909002 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,7 +25,7 @@ use std::sync::Arc; use tokio::task::JoinHandle; use torrust_tracker_configuration::Configuration; -use tracing::warn; +use tracing::{info, warn}; use crate::bootstrap::jobs::{health_check_api, http_tracker, torrent_cleanup, tracker_apis, udp_tracker}; use crate::servers::registar::Registar; @@ -59,51 +59,56 @@ pub async fn start(config: &Configuration, tracker: Arc) -> Vec { + for udp_tracker_config in udp_trackers { + if tracker.is_private() { + warn!( + "Could not start UDP tracker on: {} while in {:?}. UDP is not safe for private trackers!", + udp_tracker_config.bind_address, config.core.mode + ); + } else { + jobs.push(udp_tracker::start_job(udp_tracker_config, tracker.clone(), registar.give_form()).await); + } + } } + None => info!("No UDP blocks in configuration"), } // Start the HTTP blocks - for http_tracker_config in &config.http_trackers { - if !http_tracker_config.enabled { - continue; + match &config.http_trackers { + Some(http_trackers) => { + for http_tracker_config in http_trackers { + if let Some(job) = http_tracker::start_job( + http_tracker_config, + tracker.clone(), + registar.give_form(), + servers::http::Version::V1, + ) + .await + { + jobs.push(job); + }; + } } - - if let Some(job) = http_tracker::start_job( - http_tracker_config, - tracker.clone(), - registar.give_form(), - servers::http::Version::V1, - ) - .await - { - jobs.push(job); - }; + None => info!("No HTTP blocks in configuration"), } // Start HTTP API - if config.http_api.enabled { - if let Some(job) = tracker_apis::start_job( - &config.http_api, - tracker.clone(), - registar.give_form(), - servers::apis::Version::V1, - ) - .await - { - jobs.push(job); - }; + match &config.http_api { + Some(http_api_config) => { + if let Some(job) = tracker_apis::start_job( + http_api_config, + tracker.clone(), + registar.give_form(), + servers::apis::Version::V1, + ) + .await + { + jobs.push(job); + }; + } + None => info!("No API block in configuration"), } // Start runners to remove torrents without peers, every interval diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 05bfe2341..fed4e5347 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -16,7 +16,6 @@ use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; -use tracing::info; use super::make_rust_tls; use crate::core; @@ -39,19 +38,14 @@ pub async fn start_job( form: ServiceRegistrationForm, version: Version, ) -> Option> { - if config.enabled { - let socket = config.bind_address; + let socket = config.bind_address; - let tls = make_rust_tls(&config.tsl_config) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let tls = make_rust_tls(&config.tsl_config) + .await + .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); - match version { - Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), - } - } else { - info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration."); - None + match version { + Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), } } @@ -93,7 +87,8 @@ mod tests { #[tokio::test] async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_trackers[0]; + let http_tracker = cfg.http_trackers.clone().expect("missing HTTP tracker configuration"); + let config = &http_tracker[0]; let tracker = initialize_with_configuration(&cfg); let version = Version::V1; diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 87a607720..79a4347ef 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -43,9 +43,7 @@ pub async fn make_rust_tls(opt_tsl_config: &Option) -> Option { - None - } + None => None, } } diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index c3b12d7a1..2067e5b0c 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -26,7 +26,6 @@ use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use tokio::task::JoinHandle; use torrust_tracker_configuration::{AccessTokens, HttpApi}; -use tracing::info; use super::make_rust_tls; use crate::core; @@ -60,21 +59,16 @@ pub async fn start_job( form: ServiceRegistrationForm, version: Version, ) -> Option> { - if config.enabled { - let bind_to = config.bind_address; + let bind_to = config.bind_address; - let tls = make_rust_tls(&config.tsl_config) - .await - .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); + let tls = make_rust_tls(&config.tsl_config) + .await + .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); - let access_tokens = Arc::new(config.access_tokens.clone()); + let access_tokens = Arc::new(config.access_tokens.clone()); - match version { - Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), form, access_tokens).await), - } - } else { - info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration."); - None + match version { + Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), form, access_tokens).await), } } @@ -110,7 +104,7 @@ mod tests { #[tokio::test] async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_api; + let config = &cfg.http_api.clone().unwrap(); let tracker = initialize_with_configuration(&cfg); let version = Version::V1; diff --git a/src/lib.rs b/src/lib.rs index 7f8c70a47..cf2834418 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,21 +192,6 @@ //! external_ip = "0.0.0.0" //! on_reverse_proxy = false //! -//! [[udp_trackers]] -//! bind_address = "0.0.0.0:6969" -//! enabled = false -//! -//! [[http_trackers]] -//! bind_address = "0.0.0.0:7070" -//! enabled = false -//! -//! [http_api] -//! bind_address = "127.0.0.1:1212" -//! enabled = true -//! -//! [http_api.access_tokens] -//! admin = "MyAccessToken" -//! //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` @@ -253,8 +238,10 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "127.0.0.1:1212" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" //! ``` //! //! By default it's enabled on port `1212`. You also need to add access tokens in the configuration: @@ -310,7 +297,6 @@ //! //! ```toml //! [[http_trackers]] -//! enabled = true //! bind_address = "0.0.0.0:7070" //! ``` //! @@ -405,7 +391,6 @@ //! //! ```toml //! [[udp_trackers]] -//! enabled = true //! bind_address = "0.0.0.0:6969" //! ``` //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 02b93efa6..6dae66c2d 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -25,7 +25,6 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "0.0.0.0:1212" //! //! [http_api.tsl_config] @@ -112,7 +111,6 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "0.0.0.0:1212" //! //! [http_api.tsl_config] diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 74dc89692..246660ab1 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -274,7 +274,7 @@ mod tests { #[tokio::test] async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_api; + let config = &cfg.http_api.clone().unwrap(); let tracker = initialize_with_configuration(&cfg); diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index bbe0c3cc1..5798f7c10 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -206,7 +206,7 @@ impl HttpServer { /// Or if the request returns an error. #[must_use] pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { - let url = format!("http://{binding}/health_check"); + let url = format!("http://{binding}/health_check"); // DevSkim: ignore DS137138 let info = format!("checking http tracker health check at: {url}"); @@ -235,7 +235,8 @@ mod tests { async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); let tracker = initialize_with_configuration(&cfg); - let config = &cfg.http_trackers[0]; + let http_trackers = cfg.http_trackers.clone().expect("missing HTTP trackers configuration"); + let config = &http_trackers[0]; let bind_to = config.bind_address; diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index b2c72258d..f36f7df45 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -460,7 +460,8 @@ mod tests { async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); let tracker = initialize_with_configuration(&cfg); - let config = &cfg.udp_trackers[0]; + let udp_trackers = cfg.udp_trackers.clone().expect("missing UDP trackers configuration"); + let config = &udp_trackers[0]; let bind_to = config.bind_address; let register = &Registar::default(); diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 8f84620dd..dc2f70a76 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -31,7 +31,7 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = initialize_with_configuration(configuration); - let config = Arc::new(configuration.http_api.clone()); + let config = Arc::new(configuration.http_api.clone().expect("missing API configuration")); let bind_to = config.bind_address; diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 6e80569ec..2133ed6d0 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -29,7 +29,12 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = initialize_with_configuration(configuration); - let config = Arc::new(configuration.http_trackers[0].clone()); + let http_tracker = configuration + .http_trackers + .clone() + .expect("missing HTTP tracker configuration"); + + let config = Arc::new(http_tracker[0].clone()); let bind_to = config.bind_address; diff --git a/tests/servers/udp/environment.rs b/tests/servers/udp/environment.rs index c1fecbdd3..1ba038c70 100644 --- a/tests/servers/udp/environment.rs +++ b/tests/servers/udp/environment.rs @@ -29,7 +29,9 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = initialize_with_configuration(configuration); - let config = Arc::new(configuration.udp_trackers[0].clone()); + let udp_tracker = configuration.udp_trackers.clone().expect("missing UDP tracker configuration"); + + let config = Arc::new(udp_tracker[0].clone()); let bind_to = config.bind_address; From 0bcca80fe6b570d874c2fd49d763221778b70912 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 19 Jun 2024 10:51:26 +0100 Subject: [PATCH 119/217] chore(deps): update dependencies ```console cargo update Updating crates.io index Locking 18 packages to latest compatible versions Updating bytemuck v1.16.0 -> v1.16.1 Updating derive_more v0.99.17 -> v0.99.18 Removing displaydoc v0.2.4 Adding hermit-abi v0.4.0 Updating httparse v1.9.3 -> v1.9.4 Adding hyper-rustls v0.27.2 Removing icu_collections v1.5.0 Removing icu_locid v1.5.0 Removing icu_locid_transform v1.5.0 Removing icu_locid_transform_data v1.5.0 Removing icu_normalizer v1.5.0 Removing icu_normalizer_data v1.5.0 Removing icu_properties v1.5.0 Removing icu_properties_data v1.5.0 Removing icu_provider v1.5.0 Removing icu_provider_macros v1.5.0 Downgrading idna v1.0.0 -> v0.5.0 (latest: v1.0.1) Removing litemap v0.7.3 Updating miniz_oxide v0.7.3 -> v0.7.4 Updating polling v3.7.1 -> v3.7.2 Updating reqwest v0.12.4 -> v0.12.5 Adding rustls v0.23.10 Adding rustls-webpki v0.102.4 Removing stable_deref_trait v1.2.0 Adding subtle v2.5.0 Removing synstructure v0.13.1 Removing tinystr v0.7.6 Adding tokio-rustls v0.26.0 Adding unicode-bidi v0.3.15 Adding unicode-normalization v0.1.23 Updating url v2.5.1 -> v2.5.2 Removing utf16_iter v1.0.5 Removing utf8_iter v1.0.4 Removing write16 v1.0.0 Removing writeable v0.5.5 Removing yoke v0.7.4 Removing yoke-derive v0.7.4 Removing zerofrom v0.1.4 Removing zerofrom-derive v0.1.4 Adding zeroize v1.8.1 Removing zerovec v0.10.2 Removing zerovec-derive v0.10.2 Updating zstd-sys v2.0.10+zstd.1.5.6 -> v2.0.11+zstd.1.5.6 ``` --- Cargo.lock | 397 +++++++++++++++-------------------------------------- 1 file changed, 114 insertions(+), 283 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a54063bf..94fbe7d87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,7 +288,7 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.7.1", + "polling 3.7.2", "rustix 0.38.34", "slab", "tracing", @@ -497,10 +497,10 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rustls", + "rustls 0.21.12", "rustls-pemfile", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower", "tower-service", ] @@ -696,9 +696,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -1116,15 +1116,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -1148,17 +1148,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "downcast" version = "0.11.0" @@ -1652,6 +1641,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1700,9 +1695,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1731,6 +1726,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1790,124 +1802,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1916,14 +1810,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -1978,7 +1870,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -1995,7 +1887,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.52.0", ] @@ -2192,12 +2084,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "local-ip-address" version = "0.6.1" @@ -2264,9 +2150,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -2517,7 +2403,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -2793,13 +2679,13 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" +checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix 0.38.34", "tracing", @@ -3092,9 +2978,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", @@ -3106,6 +2992,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -3120,7 +3007,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -3301,10 +3188,23 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -3331,6 +3231,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -3662,12 +3573,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -3690,6 +3595,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -3736,17 +3647,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -3879,16 +3779,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -3959,7 +3849,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.10", + "rustls-pki-types", "tokio", ] @@ -4337,12 +4238,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -4351,27 +4267,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -4726,18 +4630,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "wyz" version = "0.5.1" @@ -4753,30 +4645,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -4799,47 +4667,10 @@ dependencies = [ ] [[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" +name = "zeroize" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zstd" @@ -4861,9 +4692,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", From cdd544f2e3a273c1612c0c5c64b17da1036ee415 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Sat, 18 May 2024 16:44:08 +0200 Subject: [PATCH 120/217] scratch --- .gitignore | 3 +- packages/configuration/src/lib.rs | 7 +- packages/primitives/src/example/mod.rs | 26 ++++++ packages/test-helpers/src/configuration.rs | 2 +- src/bootstrap/app.rs | 15 ++-- src/servers/udp/server.rs | 99 +--------------------- 6 files changed, 46 insertions(+), 106 deletions(-) create mode 100644 packages/primitives/src/example/mod.rs diff --git a/.gitignore b/.gitignore index c1abad7e0..e2e39beed 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ /tracker.* /tracker.toml callgrind.out -perf.data* \ No newline at end of file +perf.data* +*.code-workspace diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 355d9b1d1..1f39fc48f 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -466,13 +466,16 @@ impl Default for AnnouncePolicy { pub struct Configuration { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, + #[serde(alias = "log_level")] + pub trace_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. pub mode: TrackerMode, // Database configuration /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. /// For `Sqlite3`, the format is `path/to/database.db`, for example: /// `./storage/tracker/lib/database/sqlite3.db`. @@ -573,7 +576,7 @@ impl Default for Configuration { let announce_policy = AnnouncePolicy::default(); let mut configuration = Configuration { - log_level: Option::from(String::from("info")), + trace_level: Option::from(String::from("info")), mode: TrackerMode::Public, db_driver: DatabaseDriver::Sqlite3, db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), diff --git a/packages/primitives/src/example/mod.rs b/packages/primitives/src/example/mod.rs new file mode 100644 index 000000000..68516b766 --- /dev/null +++ b/packages/primitives/src/example/mod.rs @@ -0,0 +1,26 @@ +//! Example File +//! +//! + +#[allow(dead_code)] +enum EnumExample { + Time, + Look, + Life(usize), + Go, +} + +#[test] +fn example() { + let a_enum = EnumExample::Time; + let _ = EnumExample::Look; + + let it_is_time = match a_enum { + EnumExample::Time => true, + EnumExample::Look | EnumExample::Life(_) | EnumExample::Go => false, + }; + + assert!(it_is_time); + + // panic!(); +} diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 49cfdd390..42812923b 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -28,7 +28,7 @@ pub fn ephemeral() -> Configuration { // For example: a test for the UDP tracker should disable the API and HTTP tracker. let mut config = Configuration { - log_level: Some("off".to_owned()), // Change to `debug` for tests debugging + trace_level: Some("off".to_owned()), // Change to `debug` for tests debugging ..Default::default() }; diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index 203c9b7fb..40ed4d0dd 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -15,7 +15,6 @@ use std::sync::Arc; use torrust_tracker_clock::static_time; use torrust_tracker_configuration::Configuration; -use tracing::metadata::ParseLevelError; use tracing::Level; use super::config::initialize_configuration; @@ -23,7 +22,7 @@ use crate::core::services::tracker_factory; use crate::core::Tracker; use crate::shared::crypto::ephemeral_instance_keys; -/// It loads the configuration from the environment and setups up tracing (logging). +/// It loads the configuration from the environment gets trace level /// /// # Panics /// @@ -31,7 +30,13 @@ use crate::shared::crypto::ephemeral_instance_keys; #[must_use] pub fn config() -> (Configuration, Level) { let config = initialize_configuration(); - let level = parse_level_or_default(&config.log_level).expect("its should provide a valid value for the log level"); + + let level: Level = config + .trace_level + .as_deref() + .unwrap_or("info") + .parse() + .expect("its should provide a valid value for the log level"); (config, level) } @@ -59,7 +64,3 @@ fn initialize_static() { // Initialize the Ephemeral Instance Random Seed lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); } - -fn parse_level_or_default(level: &Option) -> Result { - level.as_deref().unwrap_or("info").parse() -} diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 074d48ea1..d877b1feb 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -19,7 +19,6 @@ //! In production, the `Udp` launcher is used directly. use std::net::SocketAddr; use std::sync::Arc; -use std::time::Duration; use futures::future::BoxFuture; use futures::FutureExt; @@ -33,7 +32,6 @@ use tracing::{debug, error, info, instrument, trace, warn, Level}; use super::v0::UdpRequest; use super::Error; use crate::core::Tracker; -use crate::servers::signals::{global_interrupt_signal, global_shutdown_signal, global_terminate_signal}; use crate::servers::udp::v0::handlers; use crate::shared::handle::{Handle, Watcher}; @@ -70,16 +68,9 @@ pub(super) struct Udp { } enum Task { - BeforeListening, - Terminate, - Interrupt, Shutdown, Graceful, Listening(Option), - Remaining, - Finished, - // Notified(Result), - // Ready(Result), Readable(std::io::Result<()>), } @@ -151,7 +142,7 @@ impl Udp { addr: self.addr, err: e.into(), })?, - _ => unreachable!(), + Task::Listening(_) => unreachable!(), }; } @@ -186,7 +177,8 @@ impl Udp { let () = self.handle.wait_connections_end().await; } Task::Readable(_) => unreachable!("it should not return readable"), - _ => unreachable!(), + + Task::Listening(_) => unreachable!(), }; Ok(()) @@ -233,7 +225,7 @@ impl Udp { Err(Error::UnableToGetListeningAddress {}) } } - _ => unreachable!(), + Task::Readable(_) => unreachable!(), } } @@ -334,89 +326,6 @@ fn get_response_payload(response: &aquatic_udp_protocol::Response) -> Vec { payload } -#[instrument(ret)] -pub fn graceful_udp_shutdown(handle: &Handle, message: String) { - let handle = handle.clone(); - - tokio::task::spawn(async move { - { - let end = tokio::select! { - maybe = handle.listening() => { Task::Listening(maybe) }, - () = global_shutdown_signal() => { Task::BeforeListening } - }; - - match end { - Task::BeforeListening => { - warn!("Shutdown Before Listening"); - handle.shutdown(); - return; - } - Task::Listening(maybe) => { - if let Some(addr) = maybe { - info!("Listening to: {addr}"); - } else { - warn!("Failed to start Listening"); - handle.shutdown(); - return; - } - } - - _ => unreachable!(), - } - }; - - loop { - let end = tokio::select! { - biased; - () = global_terminate_signal() => { Task::Terminate} - () = global_interrupt_signal() => { Task::Interrupt} - () = handle.wait_shutdown() => { Task::Shutdown} - () = handle.wait_graceful_shutdown() => { Task::Graceful} - }; - - match end { - Task::Interrupt => { - if let Ok(()) = tokio::time::timeout(Duration::from_millis(100), handle.wait_graceful_shutdown()).await { - info! {"Got Second Interrupt: Exiting Now"}; - handle.shutdown(); - return; - } - info! {"Got Interrupt: Gracefully Exiting over the next 90 seconds..."}; - handle.graceful_shutdown(Some(Duration::from_secs(90))); - } - Task::Terminate => { - warn!("Got Terminate: Exiting Now!"); - handle.shutdown(); - return; - } - Task::Shutdown => { - info!("Received Shutdown, Closing Service."); - return; - } - Task::Graceful => loop { - let wait = tokio::select! { - biased; - () = handle.wait_connections_end() => Task::Finished, - () = tokio::time::sleep(Duration::from_secs(1)) => Task::Remaining, - }; - - match wait { - Task::Remaining => { - info!("Remaining Connections: {}", handle.connection_count()); - } - Task::Finished => { - info!("No More Connections, Closing Service"); - return; - } - _ => unreachable!(), - }; - }, - _ => unreachable!(), - } - } - }); -} - // match handle.listening().await { // Some(addr) => { From 615eaf68a040bc571e580d075b64682a988e6531 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 10:05:48 +0100 Subject: [PATCH 121/217] chore(deps): bump aquatic_udp_protocol from 0.8.0 to 0.9.0 --- Cargo.lock | 841 +++++++++++-------- Cargo.toml | 1 + cSpell.json | 3 +- src/console/clients/checker/checks/udp.rs | 7 +- src/console/clients/udp/app.rs | 9 +- src/console/clients/udp/checker.rs | 19 +- src/console/clients/udp/responses.rs | 38 +- src/servers/udp/peer_builder.rs | 17 +- src/servers/udp/request.rs | 2 +- src/servers/udp/server.rs | 7 +- src/servers/udp/v0/cookie.rs | 8 +- src/servers/udp/v0/handlers.rs | 148 ++-- src/shared/bit_torrent/tracker/udp/client.rs | 18 +- tests/servers/udp/contract.rs | 38 +- 14 files changed, 662 insertions(+), 494 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d27b019f..58842a2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -64,12 +64,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -93,47 +87,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -141,18 +136,34 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "aquatic_peer_id" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f0732a73df221dcb25713849c6ebaf57b85355f669716652a7466f688cc06f25" +dependencies = [ + "compact_str", + "hex", + "quickcheck", + "regex", + "serde", + "zerocopy", +] [[package]] name = "aquatic_udp_protocol" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2919b480121f7d20d247524da62bad1b6b7928bc3f50898f624b5c592727341" +checksum = "0af90e5162f5fcbde33524128f08dc52a779f32512d5f8692eadd4b55c89389e" dependencies = [ + "aquatic_peer_id", "byteorder", "either", + "zerocopy", ] [[package]] @@ -169,9 +180,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "brotli", "flate2", @@ -185,20 +196,26 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -227,7 +244,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.0", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -237,9 +254,9 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e7c467bdcd2bd982ce5c8742a1a178aba7b03db399fd18f5d5d438f5aa91cb4" +checksum = "72188bed20deb981f3a4a9fe674e5980fd9e9c2bd880baa94715ad5d60d64c67" dependencies = [ "axum", "forwarded-header-value", @@ -299,7 +316,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -317,19 +334,19 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rustls", - "rustls-pemfile 2.1.1", + "rustls 0.21.12", + "rustls-pemfile", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tower", "tower-service", ] [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -346,6 +363,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -380,7 +403,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -421,9 +444,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.3.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", "cfg_aliases", @@ -431,23 +454,23 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.3.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", "syn_derive", ] [[package]] name = "brotli" -version = "3.5.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -456,9 +479,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -472,9 +495,9 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -516,14 +539,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" -version = "1.0.90" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -543,21 +576,21 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -589,9 +622,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -600,9 +633,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -610,33 +643,33 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim", ] [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "cmake" @@ -649,9 +682,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "colored" @@ -663,6 +696,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "config" version = "0.14.0" @@ -745,9 +791,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -805,9 +851,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -842,9 +888,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -864,9 +910,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -874,27 +920,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.55", + "strsim", + "syn 2.0.66", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -909,15 +955,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -928,7 +974,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -958,19 +1004,29 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -979,9 +1035,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1011,15 +1067,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "libz-sys", @@ -1097,7 +1153,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -1109,7 +1165,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -1121,7 +1177,7 @@ dependencies = [ "frunk_core", "frunk_proc_macro_helpers", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -1186,7 +1242,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -1237,9 +1293,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1248,9 +1304,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1260,15 +1316,15 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap 2.2.6", "slab", @@ -1279,9 +1335,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1307,21 +1363,20 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", - "allocator-api2", ] [[package]] name = "hashlink" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1377,12 +1432,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -1390,9 +1445,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1402,9 +1457,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -1421,6 +1476,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1439,9 +1511,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -1514,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -1544,6 +1616,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -1564,15 +1642,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -1684,9 +1762,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1695,7 +1773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1711,9 +1789,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "pkg-config", @@ -1728,9 +1806,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "local-ip-address" @@ -1746,9 +1824,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1777,9 +1855,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1795,9 +1873,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -1837,7 +1915,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -1888,7 +1966,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", "termcolor", "thiserror", ] @@ -1899,7 +1977,7 @@ version = "0.30.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57349d5a326b437989b6ee4dc8f2f34b0cc131202748414712a8e7d98952fc8c" dependencies = [ - "base64", + "base64 0.21.7", "bigdecimal", "bindgen", "bitflags 2.5.0", @@ -1942,11 +2020,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2011,11 +2088,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2037,9 +2113,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -2056,9 +2132,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -2098,7 +2174,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -2109,9 +2185,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2137,9 +2213,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2147,15 +2223,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -2170,7 +2246,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" dependencies = [ - "base64", + "base64 0.21.7", "serde", ] @@ -2182,9 +2258,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", @@ -2193,9 +2269,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" dependencies = [ "pest", "pest_generator", @@ -2203,22 +2279,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] name = "pest_meta" -version = "2.7.8" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" dependencies = [ "once_cell", "pest", @@ -2280,14 +2356,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2303,9 +2379,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -2316,15 +2392,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] @@ -2412,9 +2488,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -2439,11 +2515,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2538,18 +2625,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2559,9 +2646,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -2570,15 +2657,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "relative-path" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rend" @@ -2591,11 +2678,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.2" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2605,6 +2692,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -2615,11 +2703,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -2648,9 +2736,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +checksum = "5c65e4c865bc3d2e3294493dff0acf7e6c259d066e34e22059fa9c39645c3636" dependencies = [ "crossbeam-utils", ] @@ -2690,7 +2778,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.5.0", "serde", "serde_derive", @@ -2698,9 +2786,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.18.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" dependencies = [ "futures", "futures-timer", @@ -2710,18 +2798,19 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" dependencies = [ "cfg-if", "glob", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", - "syn 2.0.55", + "syn 2.0.66", "unicode-ident", ] @@ -2751,9 +2840,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.3" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" dependencies = [ "arrayvec", "borsh", @@ -2767,9 +2856,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -2788,9 +2877,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2801,40 +2890,44 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", - "rustls-webpki", + "rustls-webpki 0.101.7", "sct", ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ - "base64", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -2846,17 +2939,28 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.102.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2915,11 +3019,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -2928,9 +3032,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2938,15 +3042,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -2972,20 +3076,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] name = "serde_html_form" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50437e6a58912eecc08865e35ea2e8d365fbb2db0debb1c8bb43bf1faf055f25" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" dependencies = [ "form_urlencoded", "indexmap 2.2.6", @@ -2996,9 +3100,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -3017,20 +3121,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -3049,11 +3153,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -3067,14 +3171,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -3116,9 +3220,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3152,9 +3256,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3174,15 +3278,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subprocess" @@ -3194,6 +3292,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -3207,9 +3311,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -3225,7 +3329,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -3236,9 +3340,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "system-configuration" @@ -3313,22 +3417,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -3343,9 +3447,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3364,9 +3468,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3408,9 +3512,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -3427,13 +3531,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -3452,41 +3556,51 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls", + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.10", + "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.14", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -3515,15 +3629,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] @@ -3576,6 +3690,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "zerocopy", ] [[package]] @@ -3722,7 +3837,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] [[package]] @@ -3824,9 +3939,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -3835,9 +3950,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -3913,7 +4028,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -3947,7 +4062,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3986,11 +4101,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4005,7 +4120,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4023,7 +4138,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -4043,17 +4158,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -4064,9 +4180,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -4076,9 +4192,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -4088,9 +4204,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -4100,9 +4222,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -4112,9 +4234,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -4124,9 +4246,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -4136,9 +4258,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -4151,18 +4273,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -4188,47 +4310,54 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.66", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zstd" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 86960d9e0..3c89a36e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ tracing-subscriber = "0" # tracing-test = "0" url = "2" uuid = { version = "1", features = ["v4"] } +zerocopy = "0.7.33" [package.metadata.cargo-machete] ignored = ["serde_bytes"] diff --git a/cSpell.json b/cSpell.json index dbe0ca430..415459d27 100644 --- a/cSpell.json +++ b/cSpell.json @@ -163,7 +163,8 @@ "Xtorrent", "Xunlei", "xxxxxxxxxxxxxxxxxxxxd", - "yyyyyyyyyyyyyyyyyyyyd" + "yyyyyyyyyyyyyyyyyyyyd", + "zerocopy" ], "enableFiletypes": [ "dockerfile", diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 4279c3180..0db08020e 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -1,4 +1,5 @@ use std::net::SocketAddr; +use std::num::NonZeroU16; use std::time::Duration; use colored::Colorize; @@ -106,7 +107,7 @@ async fn setup_connection( ) -> Result<(Client, aquatic_udp_protocol::ConnectResponse), Error> { let client = checker::Client::bind_and_connect(addr, timeout).await?; - let transaction_id = aquatic_udp_protocol::TransactionId(rand::Rng::gen(&mut rand::thread_rng())); + let transaction_id = aquatic_udp_protocol::TransactionId::new(rand::Rng::gen(&mut rand::thread_rng())); let ctx = client.send_connection_request(transaction_id).await?; @@ -118,8 +119,10 @@ async fn check_udp_announce( ctx: &aquatic_udp_protocol::ConnectResponse, info_hash: InfoHash, ) -> Result { + let port = NonZeroU16::new(client.local_addr()?.port()).expect("it should be non-zero"); + client - .send_announce_request(ctx, info_hash, aquatic_udp_protocol::Port(client.local_addr()?.port())) + .send_announce_request(ctx, info_hash, aquatic_udp_protocol::Port::new(port)) .await } diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 4cc0b24f8..3633555c0 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -57,6 +57,7 @@ //! //! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`. use std::net::SocketAddr; +use std::num::NonZeroU16; use std::time::Duration; use anyhow::Context; @@ -119,7 +120,7 @@ pub async fn run() -> anyhow::Result<()> { } async fn handle_announce(addr: &SocketAddr, timeout: &Duration, info_hash: &InfoHash) -> anyhow::Result { - let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); + let transaction_id = TransactionId::new(RANDOM_TRANSACTION_ID); let client = checker::Client::bind_and_connect(addr, timeout).await?; @@ -127,14 +128,16 @@ async fn handle_announce(addr: &SocketAddr, timeout: &Duration, info_hash: &Info let ctx = client.send_connection_request(transaction_id).await?; + let port = NonZeroU16::new(bound_to.port()).expect("it should be non-zero"); + client - .send_announce_request(&ctx, *info_hash, Port(bound_to.port())) + .send_announce_request(&ctx, *info_hash, Port::new(port)) .await .context("failed to handle announce") } async fn handle_scrape(addr: &SocketAddr, timeout: &Duration, info_hashes: &[InfoHash]) -> anyhow::Result { - let transaction_id = TransactionId(RANDOM_TRANSACTION_ID); + let transaction_id = TransactionId::new(RANDOM_TRANSACTION_ID); let client = checker::Client::bind_and_connect(addr, timeout).await?; diff --git a/src/console/clients/udp/checker.rs b/src/console/clients/udp/checker.rs index 054bca609..2aadefe9a 100644 --- a/src/console/clients/udp/checker.rs +++ b/src/console/clients/udp/checker.rs @@ -2,8 +2,8 @@ use std::net::{Ipv4Addr, SocketAddr}; use std::time::Duration; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectResponse, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, - ScrapeRequest, TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectResponse, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, + Port, Response, ScrapeRequest, TransactionId, }; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::debug; @@ -76,14 +76,15 @@ impl Client { transaction_id: ctx.transaction_id, info_hash: aquatic_udp_protocol::InfoHash(info_hash.bytes()), peer_id: PeerId(*b"-qB00000000000000001"), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4Addr::new(0, 0, 0, 0).into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers(1i32.into()), port: client_port, + action_placeholder: AnnounceActionPlaceholder::default(), }; let _ = self diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 2fbc38f5f..8ea1a978b 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,7 +1,7 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use aquatic_udp_protocol::{AnnounceResponse, ScrapeResponse}; +use aquatic_udp_protocol::{AnnounceResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; #[derive(Serialize)] @@ -13,33 +13,33 @@ pub struct AnnounceResponseDto { peers: Vec, } -impl From> for AnnounceResponseDto { - fn from(announce: AnnounceResponse) -> Self { +impl From> for AnnounceResponseDto { + fn from(announce: AnnounceResponse) -> Self { Self { - transaction_id: announce.transaction_id.0, - announce_interval: announce.announce_interval.0, - leechers: announce.leechers.0, - seeders: announce.seeders.0, + transaction_id: announce.fixed.transaction_id.0.into(), + announce_interval: announce.fixed.announce_interval.0.into(), + leechers: announce.fixed.leechers.0.into(), + seeders: announce.fixed.seeders.0.into(), peers: announce .peers .iter() - .map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)) + .map(|peer| format!("{}:{}", Ipv4Addr::from(peer.ip_address), peer.port.0)) .collect::>(), } } } -impl From> for AnnounceResponseDto { - fn from(announce: AnnounceResponse) -> Self { +impl From> for AnnounceResponseDto { + fn from(announce: AnnounceResponse) -> Self { Self { - transaction_id: announce.transaction_id.0, - announce_interval: announce.announce_interval.0, - leechers: announce.leechers.0, - seeders: announce.seeders.0, + transaction_id: announce.fixed.transaction_id.0.into(), + announce_interval: announce.fixed.announce_interval.0.into(), + leechers: announce.fixed.leechers.0.into(), + seeders: announce.fixed.seeders.0.into(), peers: announce .peers .iter() - .map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)) + .map(|peer| format!("{}:{}", Ipv6Addr::from(peer.ip_address), peer.port.0)) .collect::>(), } } @@ -54,14 +54,14 @@ pub struct ScrapeResponseDto { impl From for ScrapeResponseDto { fn from(scrape: ScrapeResponse) -> Self { Self { - transaction_id: scrape.transaction_id.0, + transaction_id: scrape.transaction_id.0.into(), torrent_stats: scrape .torrent_stats .iter() .map(|torrent_scrape_statistics| TorrentStats { - seeders: torrent_scrape_statistics.seeders.0, - completed: torrent_scrape_statistics.completed.0, - leechers: torrent_scrape_statistics.leechers.0, + seeders: torrent_scrape_statistics.seeders.0.into(), + completed: torrent_scrape_statistics.completed.0.into(), + leechers: torrent_scrape_statistics.leechers.0.into(), }) .collect::>(), } diff --git a/src/servers/udp/peer_builder.rs b/src/servers/udp/peer_builder.rs index f7eb935a0..104f42a73 100644 --- a/src/servers/udp/peer_builder.rs +++ b/src/servers/udp/peer_builder.rs @@ -18,13 +18,20 @@ use crate::CurrentClock; /// request. #[must_use] pub fn from_request(announce_wrapper: &AnnounceWrapper, peer_ip: &IpAddr) -> peer::Peer { + let announce_event = match aquatic_udp_protocol::AnnounceEvent::from(announce_wrapper.announce_request.event) { + aquatic_udp_protocol::AnnounceEvent::Started => AnnounceEvent::Started, + aquatic_udp_protocol::AnnounceEvent::Stopped => AnnounceEvent::Stopped, + aquatic_udp_protocol::AnnounceEvent::Completed => AnnounceEvent::Completed, + aquatic_udp_protocol::AnnounceEvent::None => AnnounceEvent::None, + }; + peer::Peer { peer_id: peer::Id(announce_wrapper.announce_request.peer_id.0), - peer_addr: SocketAddr::new(*peer_ip, announce_wrapper.announce_request.port.0), + peer_addr: SocketAddr::new(*peer_ip, announce_wrapper.announce_request.port.0.into()), updated: CurrentClock::now(), - uploaded: NumberOfBytes(announce_wrapper.announce_request.bytes_uploaded.0), - downloaded: NumberOfBytes(announce_wrapper.announce_request.bytes_downloaded.0), - left: NumberOfBytes(announce_wrapper.announce_request.bytes_left.0), - event: AnnounceEvent::from_i32(announce_wrapper.announce_request.event.to_i32()), + uploaded: NumberOfBytes(announce_wrapper.announce_request.bytes_uploaded.0.into()), + downloaded: NumberOfBytes(announce_wrapper.announce_request.bytes_downloaded.0.into()), + left: NumberOfBytes(announce_wrapper.announce_request.bytes_left.0.into()), + event: announce_event, } } diff --git a/src/servers/udp/request.rs b/src/servers/udp/request.rs index e172e03b1..f95fec07a 100644 --- a/src/servers/udp/request.rs +++ b/src/servers/udp/request.rs @@ -21,7 +21,7 @@ impl AnnounceWrapper { #[must_use] pub fn new(announce_request: &AnnounceRequest) -> Self { AnnounceWrapper { - announce_request: announce_request.clone(), + announce_request: *announce_request, info_hash: InfoHash(announce_request.info_hash.0), } } diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index d877b1feb..264afce54 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -22,7 +22,8 @@ use std::sync::Arc; use futures::future::BoxFuture; use futures::FutureExt; -use ringbuf::{Rb, StaticRb}; +use ringbuf::traits::{Consumer as _, Observer as _, RingBuffer as _}; +use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::select; use tokio::task::AbortHandle; @@ -316,7 +317,9 @@ fn get_response_payload(response: &aquatic_udp_protocol::Response) -> Vec { let buffer = vec![0u8; MAX_PACKET_SIZE]; let mut cursor = std::io::Cursor::new(buffer); - let () = response.write(&mut cursor).expect("it should be able to write to buffer"); + let () = response + .write_bytes(&mut cursor) + .expect("it should be able to write to buffer"); #[allow(clippy::cast_possible_truncation)] let len = cursor.position() as usize; diff --git a/src/servers/udp/v0/cookie.rs b/src/servers/udp/v0/cookie.rs index a005a969d..bbfc139ef 100644 --- a/src/servers/udp/v0/cookie.rs +++ b/src/servers/udp/v0/cookie.rs @@ -71,6 +71,8 @@ use std::panic::Location; use aquatic_udp_protocol::ConnectionId; use torrust_tracker_clock::time_extent::{Extent, TimeExtent}; +use zerocopy::network_endian::I64; +use zerocopy::AsBytes; use super::error::Error; @@ -83,13 +85,15 @@ pub const COOKIE_LIFETIME: TimeExtent = TimeExtent::from_sec(2, &60); /// Converts a connection ID into a connection cookie. #[must_use] pub fn from_connection_id(connection_id: &ConnectionId) -> Cookie { - connection_id.0.to_le_bytes() + let mut cookie = [0u8; 8]; + connection_id.write_to(&mut cookie); + cookie } /// Converts a connection cookie into a connection ID. #[must_use] pub fn into_connection_id(connection_cookie: &Cookie) -> ConnectionId { - ConnectionId(i64::from_le_bytes(*connection_cookie)) + ConnectionId(I64::new(i64::from_be_bytes(*connection_cookie))) } /// Generates a new connection cookie. diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index 98bede053..0430b02a9 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -1,18 +1,20 @@ //! Handlers for the UDP server. use std::fmt; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, SocketAddr}; use std::panic::Location; use std::sync::Arc; use std::time::Instant; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceRequest, AnnounceResponse, ConnectRequest, ConnectResponse, ErrorResponse, NumberOfDownloads, - NumberOfPeers, Port, Request, Response, ResponsePeer, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, + AnnounceInterval, AnnounceRequest, AnnounceResponse, AnnounceResponseFixedData, ConnectRequest, ConnectResponse, + ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, NumberOfDownloads, NumberOfPeers, Port, Request, Response, ResponsePeer, + ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; use torrust_tracker_located_error::DynError; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::debug; use uuid::Uuid; +use zerocopy::network_endian::I32; use super::cookie::{check, from_connection_id, into_connection_id, make}; use super::UdpRequest; @@ -41,7 +43,7 @@ pub(crate) async fn handle_packet(tracker: &Arc, addr: SocketAddr, udp_ let request_id = RequestId::make(udp_request); - match Request::from_bytes(&udp_request.payload[..udp_request.payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| { + match Request::parse_bytes(&udp_request.payload[..udp_request.payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| { Error::InternalServer { message: format!("{e:?}"), location: Location::caller(), @@ -74,7 +76,7 @@ pub(crate) async fn handle_packet(tracker: &Arc, addr: SocketAddr, udp_ &Error::BadRequest { source: (Arc::new(e) as DynError).into(), }, - TransactionId(0), + TransactionId(I32::new(0)), ); log_error_response(&request_id); @@ -179,18 +181,20 @@ pub async fn handle_announce( #[allow(clippy::cast_possible_truncation)] if target.is_ipv4() { let announce_response = AnnounceResponse { - transaction_id: wrapped_announce_request.announce_request.transaction_id, - announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32), - leechers: NumberOfPeers(i64::from(response.stats.incomplete) as i32), - seeders: NumberOfPeers(i64::from(response.stats.complete) as i32), + fixed: AnnounceResponseFixedData { + transaction_id: wrapped_announce_request.announce_request.transaction_id, + announce_interval: AnnounceInterval(I32::new(i64::from(tracker.get_announce_policy().interval) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(response.stats.incomplete) as i32)), + seeders: NumberOfPeers(I32::new(i64::from(response.stats.complete) as i32)), + }, peers: response .peers .iter() .filter_map(|peer| { if let IpAddr::V4(ip) = peer.peer_addr.ip() { - Some(ResponsePeer:: { - ip_address: ip, - port: Port(peer.peer_addr.port()), + Some(ResponsePeer:: { + ip_address: ip.into(), + port: Port(peer.peer_addr.port().into()), }) } else { None @@ -204,18 +208,20 @@ pub async fn handle_announce( Ok(Response::from(announce_response)) } else { let announce_response = AnnounceResponse { - transaction_id: wrapped_announce_request.announce_request.transaction_id, - announce_interval: AnnounceInterval(i64::from(tracker.get_announce_policy().interval) as i32), - leechers: NumberOfPeers(i64::from(response.stats.incomplete) as i32), - seeders: NumberOfPeers(i64::from(response.stats.complete) as i32), + fixed: AnnounceResponseFixedData { + transaction_id: wrapped_announce_request.announce_request.transaction_id, + announce_interval: AnnounceInterval(I32::new(i64::from(tracker.get_announce_policy().interval) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(response.stats.incomplete) as i32)), + seeders: NumberOfPeers(I32::new(i64::from(response.stats.complete) as i32)), + }, peers: response .peers .iter() .filter_map(|peer| { if let IpAddr::V6(ip) = peer.peer_addr.ip() { - Some(ResponsePeer:: { - ip_address: ip, - port: Port(peer.peer_addr.port()), + Some(ResponsePeer:: { + ip_address: ip.into(), + port: Port(peer.peer_addr.port().into()), }) } else { None @@ -259,9 +265,9 @@ pub async fn handle_scrape(tracker: &Tracker, request: &ScrapeRequest, target: S #[allow(clippy::cast_possible_truncation)] let scrape_entry = { TorrentScrapeStatistics { - seeders: NumberOfPeers(i64::from(swarm_metadata.complete) as i32), - completed: NumberOfDownloads(i64::from(swarm_metadata.downloaded) as i32), - leechers: NumberOfPeers(i64::from(swarm_metadata.incomplete) as i32), + seeders: NumberOfPeers(I32::new(i64::from(swarm_metadata.complete) as i32)), + completed: NumberOfDownloads(I32::new(i64::from(swarm_metadata.downloaded) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(swarm_metadata.incomplete) as i32)), } }; @@ -445,14 +451,14 @@ mod tests { fn sample_connect_request() -> ConnectRequest { ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), } } #[tokio::test] async fn a_connect_response_should_contain_the_same_transaction_id_as_the_connect_request() { let request = ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), }; let response = handle_connect(&public_tracker(), &request, sample_ipv4_target()) @@ -471,7 +477,7 @@ mod tests { #[tokio::test] async fn a_connect_response_should_contain_a_new_connection_id() { let request = ConnectRequest { - transaction_id: TransactionId(0i32), + transaction_id: TransactionId(0i32.into()), }; let response = handle_connect(&public_tracker(), &request, sample_ipv4_target()) @@ -527,10 +533,11 @@ mod tests { mod announce_request { use std::net::Ipv4Addr; + use std::num::NonZeroU16; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId as AquaticPeerId, PeerKey, Port, - TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectionId, NumberOfBytes, NumberOfPeers, + PeerId as AquaticPeerId, PeerKey, Port, TransactionId, }; use super::sample_ipv4_target; @@ -548,17 +555,18 @@ mod tests { let default_request = AnnounceRequest { connection_id: into_connection_id(&make(&sample_ipv4_target())), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId::new(0i32), info_hash: info_hash_aquatic, peer_id: AquaticPeerId([255u8; 20]), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(client_ip), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port: Port(client_port), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: client_ip.into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers::new(1i32), + port: Port::new(NonZeroU16::new(client_port).expect("a non-zero client port")), + action_placeholder: AnnounceActionPlaceholder::default(), }; AnnounceRequestBuilder { request: default_request, @@ -581,12 +589,12 @@ mod tests { } pub fn with_ip_address(mut self, ip_address: Ipv4Addr) -> Self { - self.request.ip_address = Some(ip_address); + self.request.ip_address = ip_address.into(); self } pub fn with_port(mut self, port: u16) -> Self { - self.request.port = Port(port); + self.request.port = Port(port.into()); self } @@ -598,12 +606,12 @@ mod tests { mod using_ipv4 { use std::future; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, NumberOfPeers, PeerId as AquaticPeerId, - Response, ResponsePeer, + AnnounceInterval, AnnounceResponse, AnnounceResponseFixedData, InfoHash as AquaticInfoHash, Ipv4AddrBytes, + Ipv6AddrBytes, NumberOfPeers, PeerId as AquaticPeerId, Response, ResponsePeer, }; use mockall::predicate::eq; use torrust_tracker_primitives::peer; @@ -657,14 +665,16 @@ mod tests { let response = handle_announce(&public_tracker(), &request, target).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(120i32), - leechers: NumberOfPeers(0i32), - seeders: NumberOfPeers(1i32), + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval(120i32.into()), + leechers: NumberOfPeers(0i32.into()), + seeders: NumberOfPeers(1i32.into()), + }, peers: empty_peer_vector }) ); @@ -739,7 +749,7 @@ mod tests { let response = announce_a_new_peer_using_ipv4(tracker.clone()).await; // The response should not contain the peer using IPV6 - let peers: Option>> = match response { + let peers: Option>> = match response { Response::AnnounceIpv6(announce_response) => Some(announce_response.peers), _ => None, }; @@ -820,12 +830,12 @@ mod tests { mod using_ipv6 { use std::future; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::Arc; use aquatic_udp_protocol::{ - AnnounceInterval, AnnounceResponse, InfoHash as AquaticInfoHash, NumberOfPeers, PeerId as AquaticPeerId, - Response, ResponsePeer, + AnnounceInterval, AnnounceResponse, AnnounceResponseFixedData, InfoHash as AquaticInfoHash, Ipv4AddrBytes, + Ipv6AddrBytes, NumberOfPeers, PeerId as AquaticPeerId, Response, ResponsePeer, }; use mockall::predicate::eq; use torrust_tracker_primitives::peer; @@ -883,14 +893,16 @@ mod tests { let response = handle_announce(&public_tracker(), &request, target).await.unwrap(); - let empty_peer_vector: Vec> = vec![]; + let empty_peer_vector: Vec> = vec![]; assert_eq!( response, Response::from(AnnounceResponse { - transaction_id: request.transaction_id, - announce_interval: AnnounceInterval(120i32), - leechers: NumberOfPeers(0i32), - seeders: NumberOfPeers(1i32), + fixed: AnnounceResponseFixedData { + transaction_id: request.transaction_id, + announce_interval: AnnounceInterval(120i32.into()), + leechers: NumberOfPeers(0i32.into()), + seeders: NumberOfPeers(1i32.into()), + }, peers: empty_peer_vector }) ); @@ -968,7 +980,7 @@ mod tests { let response = announce_a_new_peer_using_ipv6(tracker.clone()).await; // The response should not contain the peer using IPV4 - let peers: Option>> = match response { + let peers: Option>> = match response { Response::AnnounceIpv4(announce_response) => Some(announce_response.peers), _ => None, }; @@ -1076,9 +1088,9 @@ mod tests { fn zeroed_torrent_statistics() -> TorrentScrapeStatistics { TorrentScrapeStatistics { - seeders: NumberOfPeers(0), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(0.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), } } @@ -1091,7 +1103,7 @@ mod tests { let request = ScrapeRequest { connection_id: into_connection_id(&make(&target)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId::new(0i32), info_hashes, }; @@ -1127,7 +1139,7 @@ mod tests { ScrapeRequest { connection_id: into_connection_id(&make(target)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId::new(0i32), info_hashes, } } @@ -1165,9 +1177,9 @@ mod tests { let torrent_stats = match_scrape_response(add_a_sample_seeder_and_scrape(tracker.clone()).await); let expected_torrent_stats = vec![TorrentScrapeStatistics { - seeders: NumberOfPeers(1), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(1.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), }]; assert_eq!(torrent_stats.unwrap().torrent_stats, expected_torrent_stats); @@ -1238,9 +1250,9 @@ mod tests { let torrent_stats = match_scrape_response(handle_scrape(&tracker, &request, target).await.unwrap()).unwrap(); let expected_torrent_stats = vec![TorrentScrapeStatistics { - seeders: NumberOfPeers(1), - completed: NumberOfDownloads(0), - leechers: NumberOfPeers(0), + seeders: NumberOfPeers(1.into()), + completed: NumberOfDownloads(0.into()), + leechers: NumberOfPeers(0.into()), }]; assert_eq!(torrent_stats.torrent_stats, expected_torrent_stats); @@ -1271,7 +1283,7 @@ mod tests { ScrapeRequest { connection_id: into_connection_id(&make(target)), - transaction_id: TransactionId(0i32), + transaction_id: TransactionId::new(0i32), info_hashes, } } diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 574fea0d6..729ff0451 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -204,7 +204,7 @@ impl Client { let mut cursor = Cursor::new(request_buffer); request - .write(&mut cursor) + .write_bytes(&mut cursor) .map_err(|e| Error::UnableToWriteToRequestBuffer { err: e.into() })?; let request_data = { @@ -228,7 +228,7 @@ impl Client { debug!(target: "UDP tracker client", "received {payload_size} bytes. Response {response_buffer:?}"); - Response::from_bytes(&response_buffer[..payload_size], true) + Response::parse_bytes(&response_buffer[..payload_size], true) .map_err(|e| Error::UnableToGetResponseFromBuffer { err: e.into() }) } @@ -247,13 +247,13 @@ impl Client { let connect_request = ConnectRequest { transaction_id }; - let _ = self.send_request(connect_request.clone().into()).await?; + let _ = self.send_request(connect_request.into()).await?; let response = self.receive_response().await?; debug!("connection request response:\n{response:#?}"); - check_connect_response(&response, &connect_request) + check_connect_response(&response, connect_request) } /// Helper Function to Check if a UDP Service is Connectable @@ -265,13 +265,13 @@ impl Client { /// # Panics pub async fn check(self) -> Result { let connect_request = ConnectRequest { - transaction_id: TransactionId(rand::Rng::gen(&mut rand::thread_rng())), + transaction_id: TransactionId::new(rand::Rng::gen(&mut rand::thread_rng())), }; - let _ = self.send_request(connect_request.clone().into()).await?; + let _ = self.send_request(connect_request.into()).await?; let process = move |response: Result, connect_request: ConnectRequest| -> Result { - check_connect_response(&response?, &connect_request).map(|id| { + check_connect_response(&response?, connect_request).map(|id| { format!( "Connected with, transaction_id: {} and connection_id: {}", id.transaction_id.0, id.connection_id.0 @@ -300,11 +300,11 @@ impl Client { /// If the [`Response`] is not a [`ConnectResponse`] /// or if the [`TransactionId`] dose not match. /// -pub fn check_connect_response(response: &Response, connect_request: &ConnectRequest) -> Result { +pub fn check_connect_response(response: &Response, connect_request: ConnectRequest) -> Result { match response { Response::Connect(connect) => { if connect.transaction_id == connect_request.transaction_id { - Ok(connect.clone()) + Ok(*connect) } else { Err(Error::UnexpectedTransactionId { expected: connect.transaction_id, diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index c9e1827bf..10b090725 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -32,7 +32,7 @@ async fn should_return_a_bad_request_response_when_the_client_sends_an_empty_req let mut buffer = empty_buffer(); client.receive(&mut buffer).await.expect("it should receive a response"); - let response = Response::from_bytes(&buffer, true).expect("it should parse a response"); + let response = Response::parse_bytes(&buffer, true).expect("it should parse a response"); assert!(is_error_response(&response, "bad request")); @@ -57,7 +57,7 @@ mod receiving_a_connection_request { .expect("it should connect"); let connect_request = ConnectRequest { - transaction_id: TransactionId(123), + transaction_id: TransactionId::new(123), }; client @@ -67,7 +67,7 @@ mod receiving_a_connection_request { let response = client.receive_response().await.expect("it should get response"); - assert!(is_connect_response(&response, TransactionId(123))); + assert!(is_connect_response(&response, TransactionId::new(123))); env.stop().await; } @@ -75,10 +75,11 @@ mod receiving_a_connection_request { mod receiving_an_announce_request { use std::net::Ipv4Addr; + use std::num::NonZeroU16; use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, - TransactionId, + AnnounceActionPlaceholder, AnnounceEvent, AnnounceRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, + PeerKey, Port, TransactionId, }; use torrust_tracker::shared::bit_torrent::tracker::udp::Client; use torrust_tracker_configuration::CLIENT_TIMEOUT_DEFAULT; @@ -96,25 +97,28 @@ mod receiving_an_announce_request { .expect("it should connect"); let ctx = client - .do_connection_request(TransactionId(123)) + .do_connection_request(TransactionId::new(123)) .await .expect("it should do connection"); // Send announce request + let port = NonZeroU16::new(client.local_addr().unwrap().port()).expect("the port should be non-zero"); + let announce_request = AnnounceRequest { connection_id: ConnectionId(ctx.connection_id.0), - transaction_id: TransactionId(123i32), + action_placeholder: AnnounceActionPlaceholder::default(), + transaction_id: TransactionId::new(123i32), info_hash: InfoHash([0u8; 20]), peer_id: PeerId([255u8; 20]), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port: Port(client.local_addr().expect("it should get the local address").port()), + bytes_downloaded: NumberOfBytes(0i64.into()), + bytes_uploaded: NumberOfBytes(0i64.into()), + bytes_left: NumberOfBytes(0i64.into()), + event: AnnounceEvent::Started.into(), + ip_address: Ipv4Addr::new(0, 0, 0, 0).into(), + key: PeerKey::new(0i32), + peers_wanted: NumberOfPeers(1i32.into()), + port: Port::new(port), }; client @@ -150,7 +154,7 @@ mod receiving_an_scrape_request { .expect("it should connect"); let ctx = client - .do_connection_request(TransactionId(123)) + .do_connection_request(TransactionId::new(123)) .await .expect("it should connect"); @@ -162,7 +166,7 @@ mod receiving_an_scrape_request { let scrape_request = ScrapeRequest { connection_id: ConnectionId(ctx.connection_id.0), - transaction_id: TransactionId(123i32), + transaction_id: TransactionId::new(123i32), info_hashes, }; From ddfc1ee343924094fb8a170a7584661200ca703e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 1 Apr 2024 13:35:12 +0100 Subject: [PATCH 122/217] docs: [#768] udpate profiling docs --- .cargo/config.toml | 1 + Cargo.toml | 6 +- cSpell.json | 6 + docs/media/flamegraph.svg | 4 +- .../flamegraph_generated_withput_sudo.svg | 491 ++++++++++++++++++ docs/profiling.md | 75 ++- flamegraph_generated_withput_sudo.svg | 491 ++++++++++++++++++ 7 files changed, 1068 insertions(+), 6 deletions(-) create mode 100644 docs/media/flamegraph_generated_withput_sudo.svg create mode 100644 flamegraph_generated_withput_sudo.svg diff --git a/.cargo/config.toml b/.cargo/config.toml index a88db5f38..34d6230b9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -23,3 +23,4 @@ rustflags = [ "-D", "unused", ] + diff --git a/Cargo.toml b/Cargo.toml index 3c89a36e8..ccfe4f5ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,6 @@ debug = 1 lto = "fat" opt-level = 3 -[target.x86_64-unknown-linux-gnu] -linker = "/usr/bin/clang" -rustflags = ["-Clink-arg=-Wl,--no-rosegment", "-Clink-arg=-fuse-ld=lld"] +[profile.release-debug] +inherits = "release" +debug = true diff --git a/cSpell.json b/cSpell.json index 415459d27..b3b7a6ea0 100644 --- a/cSpell.json +++ b/cSpell.json @@ -21,6 +21,7 @@ "bools", "Bragilevsky", "bufs", + "buildid", "Buildx", "byteorder", "callgrind", @@ -71,8 +72,11 @@ "Intermodal", "intervali", "Joakim", + "kallsyms", "kcachegrind", + "kexec", "keyout", + "kptr", "lcov", "leecher", "leechers", @@ -84,6 +88,7 @@ "matchmakes", "metainfo", "middlewares", + "misresolved", "mockall", "multimap", "myacicontext", @@ -153,6 +158,7 @@ "Vagaa", "valgrind", "Vitaly", + "vmlinux", "Vuze", "Weidendorfer", "Werror", diff --git a/docs/media/flamegraph.svg b/docs/media/flamegraph.svg index 34e7146f9..58387ee06 100644 --- a/docs/media/flamegraph.svg +++ b/docs/media/flamegraph.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/docs/media/flamegraph_generated_withput_sudo.svg b/docs/media/flamegraph_generated_withput_sudo.svg new file mode 100644 index 000000000..84c00ffe3 --- /dev/null +++ b/docs/media/flamegraph_generated_withput_sudo.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file diff --git a/docs/profiling.md b/docs/profiling.md index 7c28367ce..26e5b786e 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -2,12 +2,85 @@ ## Using flamegraph +### Requirements + +You need to install some dependencies. For Ubuntu you can run: + ```console -TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" cargo flamegraph --bin=profiling -- 60 +sudo apt-get install clang lld +``` + +You also need to uncomment these lines in the cargo [config.toml](./../.cargo/config.toml) file. + +```toml +[target.x86_64-unknown-linux-gnu] +linker = "/usr/bin/clang" +rustflags = ["-Clink-arg=-fuse-ld=lld", "-Clink-arg=-Wl,--no-rosegment"] +``` + +Follow the [flamegraph](https://github.com/flamegraph-rs/flamegraph) instructions for installation. + +Apart from running the tracker you will need to run some request if you want to profile services while they are processing requests. + +You can use the aquatic [UDP load test](https://github.com/greatest-ape/aquatic/tree/master/crates/udp_load_test) script. + +### Generate flamegraph + +To generate the graph you will need to: + +1. Build the tracker for profiling. +2. Run the aquatic UDP load test. +3. Run the tracker with flamegraph and profiling configuration. + +```console +cargo build --profile=release-debug --bin=profiling +./target/release/aquatic_udp_load_test -c "load-test-config.toml" +sudo TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 +``` + +__NOTICE__: You need to install the `aquatic_udp_load_test` program. + +The output should be like the following: + +```output +Loading configuration file: `./share/default/config/tracker.udp.benchmarking.toml` ... +Torrust successfully shutdown. +[ perf record: Woken up 23377 times to write data ] +Warning: +Processed 533730 events and lost 3 chunks! + +Check IO/CPU overload! + +[ perf record: Captured and wrote 5899.806 MB perf.data (373239 samples) ] +writing flamegraph to "flamegraph.svg" ``` ![flamegraph](./media/flamegraph.svg) +__NOTICE__: You need to provide the absolute path for the installed `flamegraph` app if you use sudo. Replace `/home/USER/.cargo/bin/flamegraph` with the location of your installed `flamegraph` app. You can run it without sudo but you can get a warning message like the following: + +```output +WARNING: Kernel address maps (/proc/{kallsyms,modules}) are restricted, +check /proc/sys/kernel/kptr_restrict and /proc/sys/kernel/perf_event_paranoid. + +Samples in kernel functions may not be resolved if a suitable vmlinux +file is not found in the buildid cache or in the vmlinux path. + +Samples in kernel modules won't be resolved at all. + +If some relocation was applied (e.g. kexec) symbols may be misresolved +even with a suitable vmlinux or kallsyms file. + +Couldn't record kernel reference relocation symbol +Symbol resolution may be skewed if relocation was used (e.g. kexec). +Check /proc/kallsyms permission or run as root. +Loading configuration file: `./share/default/config/tracker.udp.benchmarking.toml` ... +``` + +And some bars in the graph will have the `unknown` label. + +![flamegraph generated without sudo](./media/flamegraph_generated_withput_sudo.svg) + ## Using valgrind and kcachegrind You need to: diff --git a/flamegraph_generated_withput_sudo.svg b/flamegraph_generated_withput_sudo.svg new file mode 100644 index 000000000..84c00ffe3 --- /dev/null +++ b/flamegraph_generated_withput_sudo.svg @@ -0,0 +1,491 @@ +Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file From f21bd14b7a8c2a92e3c43737d3beae58eda98be3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 4 Apr 2024 17:12:43 +0100 Subject: [PATCH 123/217] chore: add new cargo dependency: crossbeam-skiplist It will be used to create a new torrent repository implementation that allos adding torrents in parallel. That could be potencially faster than BTreeMap in a write intensive context. --- Cargo.lock | 11 +++++++++++ Cargo.toml | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 58842a2f9..da747aa1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -886,6 +886,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-skiplist" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df29de440c58ca2cc6e587ec3d22347551a32435fbde9d2bff64e78a9ffa151b" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -3655,6 +3665,7 @@ dependencies = [ "clap", "colored", "config", + "crossbeam-skiplist", "derive_more", "futures", "hex-literal", diff --git a/Cargo.toml b/Cargo.toml index ccfe4f5ff..ddd1d2dab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } colored = "2" config = "0" +crossbeam-skiplist = "0.1" derive_more = "0" futures = "0" hex-literal = "0" @@ -60,7 +61,7 @@ serde_bytes = "0" serde_json = "1" serde_repr = "0" thiserror = "1" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "sync", "tracing"] } +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "tracing"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "packages/clock" } torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "packages/configuration" } torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = "contrib/bencode" } From 1c609874e463b301896a3a09649829161aada78c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 4 Apr 2024 17:14:43 +0100 Subject: [PATCH 124/217] feat: new torrent repository using crossbeam_skiplist::SkipMap SkipMap is an ordered map based on a lock-free skip list. It's al alternative to BTreeMap which supports concurrent access across multiple threads. One of the performance problems with the current solution is we can only add one torrent at the time because threads need to lock the whole BTreeMap. The SkipMap should avoid that problem. More info about SkiMap: https://docs.rs/crossbeam-skiplist/latest/crossbeam_skiplist/struct.SkipMap.html#method.remove The aquatic UDP load test was executed with the current implementation and the new one: Current Implementation: Requests out: 397287.37/second Responses in: 357549.15/second - Connect responses: 177073.94 - Announce responses: 176905.36 - Scrape responses: 3569.85 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 287 - p100: 371 New Implementation: Requests out: 396788.68/second Responses in: 357105.27/second - Connect responses: 176662.91 - Announce responses: 176863.44 - Scrape responses: 3578.91 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 105 - p99.9: 287 - p100: 351 The result is pretty similar but the benchmarking for the repository using criterios shows that this implementations is a litle bit better than the current one. --- Cargo.lock | 1 + cSpell.json | 1 + packages/torrent-repository/Cargo.toml | 3 +- .../benches/repository_benchmark.rs | 21 +++- packages/torrent-repository/src/lib.rs | 3 + .../torrent-repository/src/repository/mod.rs | 1 + .../src/repository/skip_map_mutex_std.rs | 106 ++++++++++++++++++ src/core/torrent/mod.rs | 5 +- 8 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 packages/torrent-repository/src/repository/skip_map_mutex_std.rs diff --git a/Cargo.lock b/Cargo.lock index da747aa1a..ffab5f8e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3770,6 +3770,7 @@ name = "torrust-tracker-torrent-repository" version = "3.0.0-alpha.12-develop" dependencies = [ "criterion", + "crossbeam-skiplist", "futures", "rstest", "tokio", diff --git a/cSpell.json b/cSpell.json index b3b7a6ea0..050ca762c 100644 --- a/cSpell.json +++ b/cSpell.json @@ -136,6 +136,7 @@ "Shareaza", "sharktorrent", "SHLVL", + "skiplist", "socketaddr", "sqllite", "subsec", diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 70f385b02..17c7c53d4 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -16,8 +16,9 @@ rust-version.workspace = true version.workspace = true [dependencies] +crossbeam-skiplist = "0.1" futures = "0.3.29" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "../primitives" } diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index a3684c8e2..65608c86c 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,7 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, - TorrentsRwLockTokioMutexTokio, + TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; use crate::helpers::{asyn, sync}; @@ -45,6 +45,10 @@ fn add_one_torrent(c: &mut Criterion) { .iter_custom(asyn::add_one_torrent::); }); + group.bench_function("SkipMapMutexStd", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.finish(); } @@ -89,6 +93,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| asyn::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -133,6 +142,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| asyn::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -178,6 +192,11 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { }); }); + group.bench_function("SkipMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 8bb1b6def..f7c19624c 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_clock::clock; pub mod entry; @@ -16,6 +17,8 @@ pub type TorrentsRwLockTokio = repository::RwLockTokio; pub type TorrentsRwLockTokioMutexStd = repository::RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = repository::RwLockTokio; +pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; + /// This code needs to be copied into each crate. /// Working version, for production. #[cfg(not(test))] diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 494040c9d..7ede1f87a 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -11,6 +11,7 @@ pub mod rw_lock_std_mutex_tokio; pub mod rw_lock_tokio; pub mod rw_lock_tokio_mutex_std; pub mod rw_lock_tokio_mutex_tokio; +pub mod skip_map_mutex_std; use std::fmt::Debug; diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs new file mode 100644 index 000000000..aa1f43826 --- /dev/null +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use crossbeam_skiplist::SkipMap; +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; + +use super::Repository; +use crate::entry::{Entry, EntrySync}; +use crate::{EntryMutexStd, EntrySingle}; + +#[derive(Default, Debug)] +pub struct CrossbeamSkipList { + torrents: SkipMap, +} + +impl Repository for CrossbeamSkipList +where + EntryMutexStd: EntrySync, + EntrySingle: Entry, +{ + fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().insert_or_update_peer_and_get_stats(peer) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().expect("it should get a lock").get_stats(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexStd::new( + EntrySingle { + peers: BTreeMap::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index ab78de683..5d42e8b4d 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -26,6 +26,7 @@ //! Peer that don not have a full copy of the torrent data are called "leechers". //! -use torrust_tracker_torrent_repository::TorrentsRwLockStdMutexStd; +use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; -pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used +//pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used +pub type Torrents = TorrentsSkipMapMutexStd; // Currently Used From 9a135db9877cf004255aafae0c06c198307590bf Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 5 Apr 2024 18:27:17 +0100 Subject: [PATCH 125/217] chore: ignore crossbeam-skiplist crate in cargo-machete It's been used in the src/packages/torrent-repository package. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ddd1d2dab..162db9354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes"] +ignored = ["serde_bytes", "crossbeam-skiplist"] [dev-dependencies] local-ip-address = "0" From d17c881e6828add8c3641203d84e3489ae8f5800 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 5 Apr 2024 18:29:24 +0100 Subject: [PATCH 126/217] refactor: separate torrent repository trait from implementations There are now more implementations. --- packages/torrent-repository/src/lib.rs | 14 ++++---- .../torrent-repository/src/repository/mod.rs | 34 ------------------- .../src/repository/rw_lock_std.rs | 16 +++++++++ .../src/repository/rw_lock_tokio.rs | 18 ++++++++++ .../tests/repository/mod.rs | 3 +- src/core/torrent/mod.rs | 2 -- 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index f7c19624c..ccaf579e3 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use repository::rw_lock_std::RwLockStd; +use repository::rw_lock_tokio::RwLockTokio; use repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_clock::clock; @@ -10,12 +12,12 @@ pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; -pub type TorrentsRwLockStd = repository::RwLockStd; -pub type TorrentsRwLockStdMutexStd = repository::RwLockStd; -pub type TorrentsRwLockStdMutexTokio = repository::RwLockStd; -pub type TorrentsRwLockTokio = repository::RwLockTokio; -pub type TorrentsRwLockTokioMutexStd = repository::RwLockTokio; -pub type TorrentsRwLockTokioMutexTokio = repository::RwLockTokio; +pub type TorrentsRwLockStd = RwLockStd; +pub type TorrentsRwLockStdMutexStd = RwLockStd; +pub type TorrentsRwLockStdMutexTokio = RwLockStd; +pub type TorrentsRwLockTokio = RwLockTokio; +pub type TorrentsRwLockTokioMutexStd = RwLockTokio; +pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 7ede1f87a..975a876d8 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -41,37 +41,3 @@ pub trait RepositoryAsync: Debug + Default + Sized + 'static { peer: &peer::Peer, ) -> impl std::future::Future + Send; } - -#[derive(Default, Debug)] -pub struct RwLockStd { - torrents: std::sync::RwLock>, -} - -#[derive(Default, Debug)] -pub struct RwLockTokio { - torrents: tokio::sync::RwLock>, -} - -impl RwLockStd { - /// # Panics - /// - /// Panics if unable to get a lock. - pub fn write( - &self, - ) -> std::sync::RwLockWriteGuard<'_, std::collections::BTreeMap> { - self.torrents.write().expect("it should get lock") - } -} - -impl RwLockTokio { - pub fn write( - &self, - ) -> impl std::future::Future< - Output = tokio::sync::RwLockWriteGuard< - '_, - std::collections::BTreeMap, - >, - > { - self.torrents.write() - } -} diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 9d7f29416..e9074a271 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -11,6 +11,22 @@ use super::Repository; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockStd}; +#[derive(Default, Debug)] +pub struct RwLockStd { + pub(crate) torrents: std::sync::RwLock>, +} + +impl RwLockStd { + /// # Panics + /// + /// Panics if unable to get a lock. + pub fn write( + &self, + ) -> std::sync::RwLockWriteGuard<'_, std::collections::BTreeMap> { + self.torrents.write().expect("it should get lock") + } +} + impl TorrentsRwLockStd { fn get_torrents<'a>(&'a self) -> std::sync::RwLockReadGuard<'a, std::collections::BTreeMap> where diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index fa84e2451..d84074eaf 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -11,6 +11,24 @@ use super::RepositoryAsync; use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockTokio}; +#[derive(Default, Debug)] +pub struct RwLockTokio { + pub(crate) torrents: tokio::sync::RwLock>, +} + +impl RwLockTokio { + pub fn write( + &self, + ) -> impl std::future::Future< + Output = tokio::sync::RwLockWriteGuard< + '_, + std::collections::BTreeMap, + >, + > { + self.torrents.write() + } +} + impl TorrentsRwLockTokio { async fn get_torrents<'a>(&'a self) -> tokio::sync::RwLockReadGuard<'a, std::collections::BTreeMap> where diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index ed48c062f..fbd783486 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; -use torrust_tracker_torrent_repository::repository::{RwLockStd, RwLockTokio}; +use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; +use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; use torrust_tracker_torrent_repository::EntrySingle; use crate::common::repo::Repo; diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index 5d42e8b4d..286a7e047 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -25,8 +25,6 @@ //! - The number of peers that have NOT completed downloading the torrent and are still active, that means they are actively participating in the network. //! Peer that don not have a full copy of the torrent data are called "leechers". //! - use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; -//pub type Torrents = TorrentsRwLockStdMutexStd; // Currently Used pub type Torrents = TorrentsSkipMapMutexStd; // Currently Used From f06891a9124db5764d73c7d1560c7ddcefe4f3f0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 8 Apr 2024 14:07:15 +0100 Subject: [PATCH 127/217] test: add tests for new torrent repository using SkipMap --- .../src/repository/skip_map_mutex_std.rs | 2 +- .../torrent-repository/tests/common/repo.rs | 165 +++++++++++------- .../tests/repository/mod.rs | 108 ++++++++++-- 3 files changed, 193 insertions(+), 82 deletions(-) diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index aa1f43826..0c0127b15 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -15,7 +15,7 @@ use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { - torrents: SkipMap, + pub torrents: SkipMap, } impl Repository for CrossbeamSkipList diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 3a4b53d2f..5a86aa3cf 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -7,49 +7,54 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; #[derive(Debug)] pub(crate) enum Repo { - Std(TorrentsRwLockStd), - StdMutexStd(TorrentsRwLockStdMutexStd), - StdMutexTokio(TorrentsRwLockStdMutexTokio), - Tokio(TorrentsRwLockTokio), - TokioMutexStd(TorrentsRwLockTokioMutexStd), - TokioMutexTokio(TorrentsRwLockTokioMutexTokio), + RwLockStd(TorrentsRwLockStd), + RwLockStdMutexStd(TorrentsRwLockStdMutexStd), + RwLockStdMutexTokio(TorrentsRwLockStdMutexTokio), + RwLockTokio(TorrentsRwLockTokio), + RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), + RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), + SkipMapMutexStd(TorrentsSkipMapMutexStd), } impl Repo { pub(crate) async fn get(&self, key: &InfoHash) -> Option { match self { - Repo::Std(repo) => repo.get(key), - Repo::StdMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), - Repo::Tokio(repo) => repo.get(key).await, - Repo::TokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::RwLockStd(repo) => repo.get(key), + Repo::RwLockStdMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::RwLockStdMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::RwLockTokio(repo) => repo.get(key).await, + Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), + Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), + Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } + pub(crate) async fn get_metrics(&self) -> TorrentsMetrics { match self { - Repo::Std(repo) => repo.get_metrics(), - Repo::StdMutexStd(repo) => repo.get_metrics(), - Repo::StdMutexTokio(repo) => repo.get_metrics().await, - Repo::Tokio(repo) => repo.get_metrics().await, - Repo::TokioMutexStd(repo) => repo.get_metrics().await, - Repo::TokioMutexTokio(repo) => repo.get_metrics().await, + Repo::RwLockStd(repo) => repo.get_metrics(), + Repo::RwLockStdMutexStd(repo) => repo.get_metrics(), + Repo::RwLockStdMutexTokio(repo) => repo.get_metrics().await, + Repo::RwLockTokio(repo) => repo.get_metrics().await, + Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, + Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, + Repo::SkipMapMutexStd(repo) => repo.get_metrics(), } } + pub(crate) async fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntrySingle)> { match self { - Repo::Std(repo) => repo.get_paginated(pagination), - Repo::StdMutexStd(repo) => repo + Repo::RwLockStd(repo) => repo.get_paginated(pagination), + Repo::RwLockStdMutexStd(repo) => repo .get_paginated(pagination) .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), - Repo::StdMutexTokio(repo) => { + Repo::RwLockStdMutexTokio(repo) => { let mut v: Vec<(InfoHash, EntrySingle)> = vec![]; for (i, t) in repo.get_paginated(pagination).await { @@ -57,14 +62,14 @@ impl Repo { } v } - Repo::Tokio(repo) => repo.get_paginated(pagination).await, - Repo::TokioMutexStd(repo) => repo + Repo::RwLockTokio(repo) => repo.get_paginated(pagination).await, + Repo::RwLockTokioMutexStd(repo) => repo .get_paginated(pagination) .await .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), - Repo::TokioMutexTokio(repo) => { + Repo::RwLockTokioMutexTokio(repo) => { let mut v: Vec<(InfoHash, EntrySingle)> = vec![]; for (i, t) in repo.get_paginated(pagination).await { @@ -72,76 +77,102 @@ impl Repo { } v } + Repo::SkipMapMutexStd(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) + .collect(), } } + pub(crate) async fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { match self { - Repo::Std(repo) => repo.import_persistent(persistent_torrents), - Repo::StdMutexStd(repo) => repo.import_persistent(persistent_torrents), - Repo::StdMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, - Repo::Tokio(repo) => repo.import_persistent(persistent_torrents).await, - Repo::TokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, - Repo::TokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockStd(repo) => repo.import_persistent(persistent_torrents), + Repo::RwLockStdMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::RwLockStdMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, + Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, + Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } + pub(crate) async fn remove(&self, key: &InfoHash) -> Option { match self { - Repo::Std(repo) => repo.remove(key), - Repo::StdMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), - Repo::Tokio(repo) => repo.remove(key).await, - Repo::TokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::RwLockStd(repo) => repo.remove(key), + Repo::RwLockStdMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::RwLockStdMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::RwLockTokio(repo) => repo.remove(key).await, + Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), + Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), + Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } + pub(crate) async fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { match self { - Repo::Std(repo) => repo.remove_inactive_peers(current_cutoff), - Repo::StdMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), - Repo::StdMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::Tokio(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::TokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, - Repo::TokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::RwLockStdMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::RwLockStdMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, + Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } + pub(crate) async fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { match self { - Repo::Std(repo) => repo.remove_peerless_torrents(policy), - Repo::StdMutexStd(repo) => repo.remove_peerless_torrents(policy), - Repo::StdMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, - Repo::Tokio(repo) => repo.remove_peerless_torrents(policy).await, - Repo::TokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, - Repo::TokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockStd(repo) => repo.remove_peerless_torrents(policy), + Repo::RwLockStdMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::RwLockStdMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, + Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, + Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } + pub(crate) async fn update_torrent_with_peer_and_get_stats( &self, info_hash: &InfoHash, peer: &peer::Peer, ) -> (bool, SwarmMetadata) { match self { - Repo::Std(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::StdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::StdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::Tokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::TokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::TokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::RwLockStdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::RwLockStdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, + Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), } } + pub(crate) async fn insert(&self, info_hash: &InfoHash, torrent: EntrySingle) -> Option { match self { - Repo::Std(repo) => repo.write().insert(*info_hash, torrent), - Repo::StdMutexStd(repo) => Some(repo.write().insert(*info_hash, torrent.into())?.lock().unwrap().clone()), - Repo::StdMutexTokio(repo) => { - let r = repo.write().insert(*info_hash, torrent.into()); - match r { - Some(t) => Some(t.lock().await.clone()), - None => None, - } + Repo::RwLockStd(repo) => { + repo.write().insert(*info_hash, torrent); } - Repo::Tokio(repo) => repo.write().await.insert(*info_hash, torrent), - Repo::TokioMutexStd(repo) => Some(repo.write().await.insert(*info_hash, torrent.into())?.lock().unwrap().clone()), - Repo::TokioMutexTokio(repo) => Some(repo.write().await.insert(*info_hash, torrent.into())?.lock().await.clone()), - } + Repo::RwLockStdMutexStd(repo) => { + repo.write().insert(*info_hash, torrent.into()); + } + Repo::RwLockStdMutexTokio(repo) => { + repo.write().insert(*info_hash, torrent.into()); + } + Repo::RwLockTokio(repo) => { + repo.write().await.insert(*info_hash, torrent); + } + Repo::RwLockTokioMutexStd(repo) => { + repo.write().await.insert(*info_hash, torrent.into()); + } + Repo::RwLockTokioMutexTokio(repo) => { + repo.write().await.insert(*info_hash, torrent.into()); + } + Repo::SkipMapMutexStd(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } + }; + self.get(info_hash).await } } diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index fbd783486..0b8863e8a 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -10,6 +10,7 @@ use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; +use torrust_tracker_torrent_repository::repository::skip_map_mutex_std::CrossbeamSkipList; use torrust_tracker_torrent_repository::EntrySingle; use crate::common::repo::Repo; @@ -17,30 +18,37 @@ use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; #[fixture] fn standard() -> Repo { - Repo::Std(RwLockStd::default()) + Repo::RwLockStd(RwLockStd::default()) } + #[fixture] fn standard_mutex() -> Repo { - Repo::StdMutexStd(RwLockStd::default()) + Repo::RwLockStdMutexStd(RwLockStd::default()) } #[fixture] fn standard_tokio() -> Repo { - Repo::StdMutexTokio(RwLockStd::default()) + Repo::RwLockStdMutexTokio(RwLockStd::default()) } #[fixture] fn tokio_std() -> Repo { - Repo::Tokio(RwLockTokio::default()) + Repo::RwLockTokio(RwLockTokio::default()) } + #[fixture] fn tokio_mutex() -> Repo { - Repo::TokioMutexStd(RwLockTokio::default()) + Repo::RwLockTokioMutexStd(RwLockTokio::default()) } #[fixture] fn tokio_tokio() -> Repo { - Repo::TokioMutexTokio(RwLockTokio::default()) + Repo::RwLockTokioMutexTokio(RwLockTokio::default()) +} + +#[fixture] +fn skip_list_std() -> Repo { + Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } type Entries = Vec<(InfoHash, EntrySingle)>; @@ -224,7 +232,16 @@ fn policy_remove_persist() -> TrackerPolicy { #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_a_torrent_entry( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { make(&repo, &entries).await; @@ -247,7 +264,16 @@ async fn it_should_get_a_torrent_entry( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, many_out_of_order: Entries, ) { @@ -280,7 +306,16 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_paginated( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(paginated_limit_zero(), paginated_limit_one(), paginated_limit_one_offset_one())] paginated: Pagination, ) { @@ -328,7 +363,16 @@ async fn it_should_get_paginated( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_get_metrics( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; @@ -360,7 +404,16 @@ async fn it_should_get_metrics( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_import_persistent_torrents( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(persistent_empty(), persistent_single(), persistent_three())] persistent_torrents: PersistentTorrents, ) { @@ -389,7 +442,16 @@ async fn it_should_import_persistent_torrents( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_an_entry( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { make(&repo, &entries).await; @@ -416,7 +478,16 @@ async fn it_should_remove_an_entry( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_inactive_peers( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, ) { use std::ops::Sub as _; @@ -489,7 +560,16 @@ async fn it_should_remove_inactive_peers( #[case::in_order(many_hashed_in_order())] #[tokio::test] async fn it_should_remove_peerless_torrents( - #[values(standard(), standard_mutex(), standard_tokio(), tokio_std(), tokio_mutex(), tokio_tokio())] repo: Repo, + #[values( + standard(), + standard_mutex(), + standard_tokio(), + tokio_std(), + tokio_mutex(), + tokio_tokio(), + skip_list_std() + )] + repo: Repo, #[case] entries: Entries, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { From d8811dd2db9d54773f4ce98cfca61d179ce552f6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 13:24:41 +0100 Subject: [PATCH 128/217] chore(deps): add cargo dependency: dashmap It will be used to create a new torrent repository implementation, using a DashMap for the torrent list. DashMap crate: https://crates.io/crates/dashmap --- Cargo.lock | 15 +++++++++++++++ Cargo.toml | 1 + cSpell.json | 1 + packages/torrent-repository/Cargo.toml | 1 + 4 files changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ffab5f8e0..f5323cde8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -953,6 +953,19 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" @@ -3666,6 +3679,7 @@ dependencies = [ "colored", "config", "crossbeam-skiplist", + "dashmap", "derive_more", "futures", "hex-literal", @@ -3771,6 +3785,7 @@ version = "3.0.0-alpha.12-develop" dependencies = [ "criterion", "crossbeam-skiplist", + "dashmap", "futures", "rstest", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 162db9354..a509e50b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ clap = { version = "4", features = ["derive", "env"] } colored = "2" config = "0" crossbeam-skiplist = "0.1" +dashmap = "5.5.3" derive_more = "0" futures = "0" hex-literal = "0" diff --git a/cSpell.json b/cSpell.json index 050ca762c..b3b9a7237 100644 --- a/cSpell.json +++ b/cSpell.json @@ -164,6 +164,7 @@ "Weidendorfer", "Werror", "whitespaces", + "Xacrimon", "XBTT", "Xdebug", "Xeon", diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 17c7c53d4..5364a59a7 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -17,6 +17,7 @@ version.workspace = true [dependencies] crossbeam-skiplist = "0.1" +dashmap = "5.5.3" futures = "0.3.29" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } From 5ef0d3f99d9be4ea1c7be4b8975e9b097ad81f2b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 14:17:10 +0100 Subject: [PATCH 129/217] feat: [#565] new torrent repository implementation usind DashMap It's not enabled as the deafult repository becuase DashMap does not return the items in order. Some tests fail: ``` output failures: ---- core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash stdout ---- thread 'core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash' panicked at src/core/services/torrent.rs:303:13: assertion `left == right` failed left: [BasicInfo { info_hash: InfoHash([158, 2, 23, 208, 250, 113, 200, 115, 50, 205, 139, 249, 219, 234, 188, 178, 194, 207, 60, 77]), seeders: 1, completed: 0, leechers: 0 }, BasicInfo { info_hash: InfoHash([3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58]), seeders: 1, completed: 0, leechers: 0 }] right: [BasicInfo { info_hash: InfoHash([3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58]), seeders: 1, completed: 0, leechers: 0 }, BasicInfo { info_hash: InfoHash([158, 2, 23, 208, 250, 113, 200, 115, 50, 205, 139, 249, 219, 234, 188, 178, 194, 207, 60, 77]), seeders: 1, completed: 0, leechers: 0 }] note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: core::services::torrent::tests::searching_for_torrents::should_return_torrents_ordered_by_info_hash test result: FAILED. 212 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.18s error: test failed, to rerun pass `--lib` ``` On the other hand, to use it, a new data strcuture should be added to the repo: An Index with sorted torrents by Infohash The API uses pagination returning torrents in alphabetically order by InfoHash. Adding such an Index would probably decrease the performace of this repository implementation. And it's performace looks similar to the current SkipMap implementation. SkipMap performace with Aquatic UDP load test: ``` Requests out: 396788.68/second Responses in: 357105.27/second - Connect responses: 176662.91 - Announce responses: 176863.44 - Scrape responses: 3578.91 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 105 - p99.9: 287 - p100: 351 ``` DashMap performace with Aquatic UDP load test: ``` Requests out: 410658.38/second Responses in: 365892.86/second - Connect responses: 181258.91 - Announce responses: 181005.95 - Scrape responses: 3628.00 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 295 - p100: 363 ``` --- .../benches/repository_benchmark.rs | 23 +++- packages/torrent-repository/src/lib.rs | 2 + .../src/repository/dash_map_mutex_std.rs | 106 ++++++++++++++++++ .../torrent-repository/src/repository/mod.rs | 1 + .../torrent-repository/tests/common/repo.rs | 20 +++- .../tests/repository/mod.rs | 30 +++-- 6 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 packages/torrent-repository/src/repository/dash_map_mutex_std.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 65608c86c..58cd70d9a 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -4,8 +4,8 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ - TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, - TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; use crate::helpers::{asyn, sync}; @@ -49,6 +49,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("DashMapMutexStd", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.finish(); } @@ -98,6 +102,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -147,6 +156,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.finish(); } @@ -197,6 +211,11 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("DashMapMutexStd", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.finish(); } diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index ccaf579e3..7a6d209b9 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use repository::dash_map_mutex_std::XacrimonDashMap; use repository::rw_lock_std::RwLockStd; use repository::rw_lock_tokio::RwLockTokio; use repository::skip_map_mutex_std::CrossbeamSkipList; @@ -20,6 +21,7 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsDashMapMutexStd = XacrimonDashMap; /// This code needs to be copied into each crate. /// Working version, for production. diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs new file mode 100644 index 000000000..67c47973e --- /dev/null +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use dashmap::DashMap; +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::info_hash::InfoHash; +use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; + +use super::Repository; +use crate::entry::{Entry, EntrySync}; +use crate::{EntryMutexStd, EntrySingle}; + +#[derive(Default, Debug)] +pub struct XacrimonDashMap { + pub torrents: DashMap, +} + +impl Repository for XacrimonDashMap +where + EntryMutexStd: EntrySync, + EntrySingle: Entry, +{ + fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + if let Some(entry) = self.torrents.get(info_hash) { + entry.insert_or_update_peer_and_get_stats(peer) + } else { + let _unused = self.torrents.insert(*info_hash, Arc::default()); + + match self.torrents.get(info_hash) { + Some(entry) => entry.insert_or_update_peer_and_get_stats(peer), + None => (false, SwarmMetadata::zeroed()), + } + } + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().expect("it should get a lock").get_stats(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexStd::new( + EntrySingle { + peers: BTreeMap::default(), + downloaded: *completed, + } + .into(), + ); + + self.torrents.insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|(_key, value)| value.clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + self.torrents.retain(|_, entry| entry.is_good(policy)); + } +} diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index 975a876d8..c7c64c54a 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -5,6 +5,7 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; +pub mod dash_map_mutex_std; pub mod rw_lock_std; pub mod rw_lock_std_mutex_std; pub mod rw_lock_std_mutex_tokio; diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 5a86aa3cf..5a6eddf97 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -6,8 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ - EntrySingle, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, + TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, }; #[derive(Debug)] @@ -19,6 +19,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + DashMapMutexStd(TorrentsDashMapMutexStd), } impl Repo { @@ -31,6 +32,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } @@ -43,6 +45,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::DashMapMutexStd(repo) => repo.get_metrics(), } } @@ -82,6 +85,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::DashMapMutexStd(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) + .collect(), } } @@ -94,6 +102,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } @@ -106,6 +115,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } @@ -118,6 +128,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } @@ -130,6 +141,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } @@ -146,6 +158,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), + Repo::DashMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), } } @@ -172,6 +185,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::DashMapMutexStd(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } }; self.get(info_hash).await } diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 0b8863e8a..fad2155dc 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -8,6 +8,7 @@ use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; +use torrust_tracker_torrent_repository::repository::dash_map_mutex_std::XacrimonDashMap; use torrust_tracker_torrent_repository::repository::rw_lock_std::RwLockStd; use torrust_tracker_torrent_repository::repository::rw_lock_tokio::RwLockTokio; use torrust_tracker_torrent_repository::repository::skip_map_mutex_std::CrossbeamSkipList; @@ -51,6 +52,11 @@ fn skip_list_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn dash_map_std() -> Repo { + Repo::DashMapMutexStd(XacrimonDashMap::default()) +} + type Entries = Vec<(InfoHash, EntrySingle)>; #[fixture] @@ -239,7 +245,8 @@ async fn it_should_get_a_torrent_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -271,7 +278,8 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -313,7 +321,8 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -370,7 +379,8 @@ async fn it_should_get_metrics( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -411,7 +421,8 @@ async fn it_should_import_persistent_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -449,7 +460,8 @@ async fn it_should_remove_an_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -485,7 +497,8 @@ async fn it_should_remove_inactive_peers( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, @@ -567,7 +580,8 @@ async fn it_should_remove_peerless_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_std(), + dash_map_std() )] repo: Repo, #[case] entries: Entries, From 10bd7cc41d02ec04bfcd70d31ec278656dfa504b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 16:35:35 +0100 Subject: [PATCH 130/217] chore: add dashmap cargo dep to cargp machete It's only used for benchmarking. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a509e50b7..0f0b016ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist"] +ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap"] [dev-dependencies] local-ip-address = "0" From 8c080f1220b8e3c8c556e7bac6154fbe8b26946c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 9 Apr 2024 17:12:07 +0100 Subject: [PATCH 131/217] fix: torrent repository tests. DashMap is not ordered DashMap implementation does not support returning torrent ordered, so we have to skip those tests for this reporitoy implementation. --- packages/torrent-repository/tests/repository/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index fad2155dc..4bd9d69e5 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -278,8 +278,7 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), - dash_map_std() + skip_list_std() )] repo: Repo, #[case] entries: Entries, @@ -321,8 +320,7 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), - dash_map_std() + skip_list_std() )] repo: Repo, #[case] entries: Entries, From 409c57768b82ea118273466183aa41718dcce17a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 12 Apr 2024 15:54:49 +0100 Subject: [PATCH 132/217] refactor: segregate command and query for announce request This changes the API of the torrent repository. The method: ``` fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata); ``` is replaced with: ``` fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer); fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option; ``` The performance is not affected. Benchmaring is still using both methods in order to simulate `announce` requests. 1. The interface is simpler (command/query segregation. 2. In the long-term: - Returning swarm metadata in the announce request could be optional. The announce request process would be faster if the tracker does not have to mantain the swarm data. This is not likely to happen becuase the scrape request needs this metadata. - New repository performance improvements could be implemented. This allow decoupling peer lists from swarm metadata. The repository internally can have two data strcutures one for the peer list and another for the swarm metatada. Both using different locks. --- Cargo.lock | 2 +- .../benches/helpers/asyn.rs | 34 +++++------ .../benches/helpers/sync.rs | 22 +++++-- packages/torrent-repository/src/entry/mod.rs | 20 ++---- .../torrent-repository/src/entry/mutex_std.rs | 14 ++--- .../src/entry/mutex_tokio.rs | 12 ++-- .../torrent-repository/src/entry/single.rs | 10 +-- .../src/repository/dash_map_mutex_std.rs | 16 ++--- .../torrent-repository/src/repository/mod.rs | 10 ++- .../src/repository/rw_lock_std.rs | 10 ++- .../src/repository/rw_lock_std_mutex_std.rs | 12 +++- .../src/repository/rw_lock_std_mutex_tokio.rs | 16 ++++- .../src/repository/rw_lock_tokio.rs | 11 +++- .../src/repository/rw_lock_tokio_mutex_std.rs | 11 +++- .../repository/rw_lock_tokio_mutex_tokio.rs | 14 ++++- .../src/repository/skip_map_mutex_std.rs | 10 ++- .../torrent-repository/tests/common/repo.rs | 43 +++++++------ .../tests/common/torrent.rs | 22 +++---- .../torrent-repository/tests/entry/mod.rs | 41 +++++++------ .../tests/repository/mod.rs | 40 ++++++++---- src/core/mod.rs | 61 +++++++++++-------- src/core/services/torrent.rs | 38 ++++-------- src/servers/udp/v0/handlers.rs | 12 +--- tests/common/udp/environment.rs | 2 +- tests/servers/api/environment.rs | 2 +- tests/servers/http/environment.rs | 2 +- 26 files changed, 260 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5323cde8..28ad86cb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -960,7 +960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", diff --git a/packages/torrent-repository/benches/helpers/asyn.rs b/packages/torrent-repository/benches/helpers/asyn.rs index 80f70cdc2..1c6d9d915 100644 --- a/packages/torrent-repository/benches/helpers/asyn.rs +++ b/packages/torrent-repository/benches/helpers/asyn.rs @@ -18,9 +18,9 @@ where let info_hash = InfoHash([0; 20]); - torrent_repository - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(&info_hash, &DEFAULT_PEER).await; + + torrent_repository.get_swarm_metadata(&info_hash).await; } start.elapsed() @@ -37,9 +37,9 @@ where let handles = FuturesUnordered::new(); // Add the torrent/peer to the torrent repository - torrent_repository - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER).await; + + torrent_repository.get_swarm_metadata(info_hash).await; let start = Instant::now(); @@ -47,9 +47,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(info_hash, &DEFAULT_PEER).await; + + torrent_repository_clone.get_swarm_metadata(info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -87,9 +87,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER).await; + + torrent_repository_clone.get_swarm_metadata(&info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -123,9 +123,8 @@ where // Add the torrents/peers to the torrent repository for info_hash in &info_hashes { - torrent_repository - .update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER) - .await; + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER).await; + torrent_repository.get_swarm_metadata(info_hash).await; } let start = Instant::now(); @@ -134,9 +133,8 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone - .update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER) - .await; + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER).await; + torrent_repository_clone.get_swarm_metadata(&info_hash).await; if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); diff --git a/packages/torrent-repository/benches/helpers/sync.rs b/packages/torrent-repository/benches/helpers/sync.rs index 0523f4141..63fccfc77 100644 --- a/packages/torrent-repository/benches/helpers/sync.rs +++ b/packages/torrent-repository/benches/helpers/sync.rs @@ -20,7 +20,9 @@ where let info_hash = InfoHash([0; 20]); - torrent_repository.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(&info_hash, &DEFAULT_PEER); + + torrent_repository.get_swarm_metadata(&info_hash); } start.elapsed() @@ -37,7 +39,9 @@ where let handles = FuturesUnordered::new(); // Add the torrent/peer to the torrent repository - torrent_repository.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER); + + torrent_repository.get_swarm_metadata(info_hash); let start = Instant::now(); @@ -45,7 +49,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(info_hash, &DEFAULT_PEER); + + torrent_repository_clone.get_swarm_metadata(info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -83,7 +89,9 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER); + + torrent_repository_clone.get_swarm_metadata(&info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); @@ -117,7 +125,8 @@ where // Add the torrents/peers to the torrent repository for info_hash in &info_hashes { - torrent_repository.update_torrent_with_peer_and_get_stats(info_hash, &DEFAULT_PEER); + torrent_repository.upsert_peer(info_hash, &DEFAULT_PEER); + torrent_repository.get_swarm_metadata(info_hash); } let start = Instant::now(); @@ -126,7 +135,8 @@ where let torrent_repository_clone = torrent_repository.clone(); let handle = runtime.spawn(async move { - torrent_repository_clone.update_torrent_with_peer_and_get_stats(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.upsert_peer(&info_hash, &DEFAULT_PEER); + torrent_repository_clone.get_swarm_metadata(&info_hash); if let Some(sleep_time) = sleep { let start_time = std::time::Instant::now(); diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 4c39af829..d72ff254b 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -15,7 +15,7 @@ pub trait Entry { /// It returns the swarm metadata (statistics) as a struct: /// /// `(seeders, completed, leechers)` - fn get_stats(&self) -> SwarmMetadata; + fn get_swarm_metadata(&self) -> SwarmMetadata; /// Returns True if Still a Valid Entry according to the Tracker Policy fn is_good(&self, policy: &TrackerPolicy) -> bool; @@ -40,10 +40,7 @@ pub trait Entry { /// /// The number of peers that have complete downloading is synchronously updated when peers are updated. /// That's the total torrent downloads counter. - fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool; - - // It preforms a combined operation of `insert_or_update_peer` and `get_stats`. - fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&mut self, peer: &peer::Peer) -> bool; /// It removes peer from the swarm that have not been updated for more than `current_cutoff` seconds fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch); @@ -51,20 +48,19 @@ pub trait Entry { #[allow(clippy::module_name_repetitions)] pub trait EntrySync { - fn get_stats(&self) -> SwarmMetadata; + fn get_swarm_metadata(&self) -> SwarmMetadata; fn is_good(&self, policy: &TrackerPolicy) -> bool; fn peers_is_empty(&self) -> bool; fn get_peers_len(&self) -> usize; fn get_peers(&self, limit: Option) -> Vec>; fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec>; - fn insert_or_update_peer(&self, peer: &peer::Peer) -> bool; - fn insert_or_update_peer_and_get_stats(&self, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&self, peer: &peer::Peer) -> bool; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch); } #[allow(clippy::module_name_repetitions)] pub trait EntryAsync { - fn get_stats(&self) -> impl std::future::Future + Send; + fn get_swarm_metadata(&self) -> impl std::future::Future + Send; fn check_good(self, policy: &TrackerPolicy) -> impl std::future::Future + Send; fn peers_is_empty(&self) -> impl std::future::Future + Send; fn get_peers_len(&self) -> impl std::future::Future + Send; @@ -74,11 +70,7 @@ pub trait EntryAsync { client: &SocketAddr, limit: Option, ) -> impl std::future::Future>> + Send; - fn insert_or_update_peer(self, peer: &peer::Peer) -> impl std::future::Future + Send; - fn insert_or_update_peer_and_get_stats( - self, - peer: &peer::Peer, - ) -> impl std::future::Future + std::marker::Send; + fn upsert_peer(self, peer: &peer::Peer) -> impl std::future::Future + Send; fn remove_inactive_peers(self, current_cutoff: DurationSinceUnixEpoch) -> impl std::future::Future + Send; } diff --git a/packages/torrent-repository/src/entry/mutex_std.rs b/packages/torrent-repository/src/entry/mutex_std.rs index b4b823909..990d8ab76 100644 --- a/packages/torrent-repository/src/entry/mutex_std.rs +++ b/packages/torrent-repository/src/entry/mutex_std.rs @@ -9,8 +9,8 @@ use super::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; impl EntrySync for EntryMutexStd { - fn get_stats(&self) -> SwarmMetadata { - self.lock().expect("it should get a lock").get_stats() + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.lock().expect("it should get a lock").get_swarm_metadata() } fn is_good(&self, policy: &TrackerPolicy) -> bool { @@ -33,14 +33,8 @@ impl EntrySync for EntryMutexStd { self.lock().expect("it should get lock").get_peers_for_client(client, limit) } - fn insert_or_update_peer(&self, peer: &peer::Peer) -> bool { - self.lock().expect("it should lock the entry").insert_or_update_peer(peer) - } - - fn insert_or_update_peer_and_get_stats(&self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - self.lock() - .expect("it should lock the entry") - .insert_or_update_peer_and_get_stats(peer) + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.lock().expect("it should lock the entry").upsert_peer(peer) } fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { diff --git a/packages/torrent-repository/src/entry/mutex_tokio.rs b/packages/torrent-repository/src/entry/mutex_tokio.rs index 34f4a4e92..c5363e51a 100644 --- a/packages/torrent-repository/src/entry/mutex_tokio.rs +++ b/packages/torrent-repository/src/entry/mutex_tokio.rs @@ -9,8 +9,8 @@ use super::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle}; impl EntryAsync for EntryMutexTokio { - async fn get_stats(&self) -> SwarmMetadata { - self.lock().await.get_stats() + async fn get_swarm_metadata(&self) -> SwarmMetadata { + self.lock().await.get_swarm_metadata() } async fn check_good(self, policy: &TrackerPolicy) -> bool { @@ -33,12 +33,8 @@ impl EntryAsync for EntryMutexTokio { self.lock().await.get_peers_for_client(client, limit) } - async fn insert_or_update_peer(self, peer: &peer::Peer) -> bool { - self.lock().await.insert_or_update_peer(peer) - } - - async fn insert_or_update_peer_and_get_stats(self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - self.lock().await.insert_or_update_peer_and_get_stats(peer) + async fn upsert_peer(self, peer: &peer::Peer) -> bool { + self.lock().await.upsert_peer(peer) } async fn remove_inactive_peers(self, current_cutoff: DurationSinceUnixEpoch) { diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index c1041e9a2..a38b54023 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -12,7 +12,7 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] - fn get_stats(&self) -> SwarmMetadata { + fn get_swarm_metadata(&self) -> SwarmMetadata { let complete: u32 = self.peers.values().filter(|peer| peer.is_seeder()).count() as u32; let incomplete: u32 = self.peers.len() as u32 - complete; @@ -70,7 +70,7 @@ impl Entry for EntrySingle { } } - fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool { + fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { let mut downloaded_stats_updated: bool = false; match peer::ReadInfo::get_event(peer) { @@ -93,12 +93,6 @@ impl Entry for EntrySingle { downloaded_stats_updated } - fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - let changed = self.insert_or_update_peer(peer); - let stats = self.get_stats(); - (changed, stats) - } - fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { self.peers .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 67c47973e..b398b09dc 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -23,19 +23,21 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { if let Some(entry) = self.torrents.get(info_hash) { - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } else { let _unused = self.torrents.insert(*info_hash, Arc::default()); - - match self.torrents.get(info_hash) { - Some(entry) => entry.insert_or_update_peer_and_get_stats(peer), - None => (false, SwarmMetadata::zeroed()), + if let Some(entry) = self.torrents.get(info_hash) { + entry.upsert_peer(peer); } } } + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + fn get(&self, key: &InfoHash) -> Option { let maybe_entry = self.torrents.get(key); maybe_entry.map(|entry| entry.clone()) @@ -45,7 +47,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in &self.torrents { - let stats = entry.value().lock().expect("it should get a lock").get_stats(); + let stats = entry.value().lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/mod.rs b/packages/torrent-repository/src/repository/mod.rs index c7c64c54a..f198288f8 100644 --- a/packages/torrent-repository/src/repository/mod.rs +++ b/packages/torrent-repository/src/repository/mod.rs @@ -24,7 +24,8 @@ pub trait Repository: Debug + Default + Sized + 'static { fn remove(&self, key: &InfoHash) -> Option; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch); fn remove_peerless_torrents(&self, policy: &TrackerPolicy); - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata); + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer); + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option; } #[allow(clippy::module_name_repetitions)] @@ -36,9 +37,6 @@ pub trait RepositoryAsync: Debug + Default + Sized + 'static { fn remove(&self, key: &InfoHash) -> impl std::future::Future> + Send; fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) -> impl std::future::Future + Send; fn remove_peerless_torrents(&self, policy: &TrackerPolicy) -> impl std::future::Future + Send; - fn update_torrent_with_peer_and_get_stats( - &self, - info_hash: &InfoHash, - peer: &peer::Peer, - ) -> impl std::future::Future + Send; + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) -> impl std::future::Future + Send; + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> impl std::future::Future> + Send; } diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index e9074a271..af48428e4 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -47,12 +47,16 @@ impl Repository for TorrentsRwLockStd where EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let mut db = self.get_torrents_mut(); let entry = db.entry(*info_hash).or_insert(EntrySingle::default()); - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).map(|entry| entry.get_swarm_metadata()) } fn get(&self, key: &InfoHash) -> Option { @@ -64,7 +68,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 0b65234e3..74cdc4475 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -33,7 +33,7 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -44,7 +44,13 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get_torrents() + .get(info_hash) + .map(super::super::entry::EntrySync::get_swarm_metadata) } fn get(&self, key: &InfoHash) -> Option { @@ -56,7 +62,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().values() { - let stats = entry.lock().expect("it should get a lock").get_stats(); + let stats = entry.lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 7b038ad56..842a9bbfe 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -36,7 +36,7 @@ where EntryMutexTokio: EntryAsync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -47,8 +47,18 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer).await + entry.upsert_peer(peer).await; } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + let maybe_entry = self.get_torrents().get(info_hash).cloned(); + + match maybe_entry { + Some(entry) => Some(entry.get_swarm_metadata().await), + None => None, + } + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents(); db.get(key).cloned() @@ -74,7 +84,7 @@ where let entries: Vec<_> = self.get_torrents().values().cloned().collect(); for entry in entries { - let stats = entry.lock().await.get_stats(); + let stats = entry.lock().await.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index d84074eaf..b95f1e31e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -51,13 +51,18 @@ impl RepositoryAsync for TorrentsRwLockTokio where EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let mut db = self.get_torrents_mut().await; let entry = db.entry(*info_hash).or_insert(EntrySingle::default()); - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).await.map(|entry| entry.get_swarm_metadata()) + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -81,7 +86,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index fbbc51a09..bde959940 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -35,7 +35,7 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().await.get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -46,8 +46,13 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer) + entry.upsert_peer(peer); } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.get(info_hash).await.map(|entry| entry.get_swarm_metadata()) + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -71,7 +76,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats(); + let stats = entry.get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index bc7fd61e8..1d002e317 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -35,7 +35,7 @@ where EntryMutexTokio: EntryAsync, EntrySingle: Entry, { - async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let maybe_entry = self.get_torrents().await.get(info_hash).cloned(); let entry = if let Some(entry) = maybe_entry { @@ -46,8 +46,16 @@ where entry.clone() }; - entry.insert_or_update_peer_and_get_stats(peer).await + entry.upsert_peer(peer).await; } + + async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + match self.get(info_hash).await { + Some(entry) => Some(entry.get_swarm_metadata().await), + None => None, + } + } + async fn get(&self, key: &InfoHash) -> Option { let db = self.get_torrents().await; db.get(key).cloned() @@ -71,7 +79,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in self.get_torrents().await.values() { - let stats = entry.get_stats().await; + let stats = entry.get_swarm_metadata().await; metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 0c0127b15..ef3e7e478 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -23,9 +23,13 @@ where EntryMutexStd: EntrySync, EntrySingle: Entry, { - fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> (bool, SwarmMetadata) { + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); - entry.value().insert_or_update_peer_and_get_stats(peer) + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) } fn get(&self, key: &InfoHash) -> Option { @@ -37,7 +41,7 @@ where let mut metrics = TorrentsMetrics::default(); for entry in &self.torrents { - let stats = entry.value().lock().expect("it should get a lock").get_stats(); + let stats = entry.value().lock().expect("it should get a lock").get_swarm_metadata(); metrics.complete += u64::from(stats.complete); metrics.downloaded += u64::from(stats.downloaded); metrics.incomplete += u64::from(stats.incomplete); diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 5a6eddf97..7c245fe04 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -23,6 +23,32 @@ pub(crate) enum Repo { } impl Repo { + pub(crate) async fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + match self { + Repo::RwLockStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::RwLockStdMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::RwLockStdMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, + Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + } + } + + pub(crate) async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + match self { + Repo::RwLockStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::RwLockStdMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::RwLockStdMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, + Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + } + } + pub(crate) async fn get(&self, key: &InfoHash) -> Option { match self { Repo::RwLockStd(repo) => repo.get(key), @@ -145,23 +171,6 @@ impl Repo { } } - pub(crate) async fn update_torrent_with_peer_and_get_stats( - &self, - info_hash: &InfoHash, - peer: &peer::Peer, - ) -> (bool, SwarmMetadata) { - match self { - Repo::RwLockStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::RwLockStdMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::RwLockStdMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokioMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::RwLockTokioMutexTokio(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer).await, - Repo::SkipMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - Repo::DashMapMutexStd(repo) => repo.update_torrent_with_peer_and_get_stats(info_hash, peer), - } - } - pub(crate) async fn insert(&self, info_hash: &InfoHash, torrent: EntrySingle) -> Option { match self { Repo::RwLockStd(repo) => { diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index 33264c443..c0699479e 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -17,9 +17,9 @@ pub(crate) enum Torrent { impl Torrent { pub(crate) async fn get_stats(&self) -> SwarmMetadata { match self { - Torrent::Single(entry) => entry.get_stats(), - Torrent::MutexStd(entry) => entry.get_stats(), - Torrent::MutexTokio(entry) => entry.clone().get_stats().await, + Torrent::Single(entry) => entry.get_swarm_metadata(), + Torrent::MutexStd(entry) => entry.get_swarm_metadata(), + Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, } } @@ -63,19 +63,11 @@ impl Torrent { } } - pub(crate) async fn insert_or_update_peer(&mut self, peer: &peer::Peer) -> bool { + pub(crate) async fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { match self { - Torrent::Single(entry) => entry.insert_or_update_peer(peer), - Torrent::MutexStd(entry) => entry.insert_or_update_peer(peer), - Torrent::MutexTokio(entry) => entry.clone().insert_or_update_peer(peer).await, - } - } - - pub(crate) async fn insert_or_update_peer_and_get_stats(&mut self, peer: &peer::Peer) -> (bool, SwarmMetadata) { - match self { - Torrent::Single(entry) => entry.insert_or_update_peer_and_get_stats(peer), - Torrent::MutexStd(entry) => entry.insert_or_update_peer_and_get_stats(peer), - Torrent::MutexTokio(entry) => entry.clone().insert_or_update_peer_and_get_stats(peer).await, + Torrent::Single(entry) => entry.upsert_peer(peer), + Torrent::MutexStd(entry) => entry.upsert_peer(peer), + Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index c39bef636..3c564c6f8 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -62,34 +62,34 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { Makes::Empty => vec![], Makes::Started => { let peer = a_started_peer(1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Completed => { let peer = a_completed_peer(2); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Downloaded => { let mut peer = a_started_peer(3); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; peer.event = AnnounceEvent::Completed; peer.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; vec![peer] } Makes::Three => { let peer_1 = a_started_peer(1); - torrent.insert_or_update_peer(&peer_1).await; + torrent.upsert_peer(&peer_1).await; let peer_2 = a_completed_peer(2); - torrent.insert_or_update_peer(&peer_2).await; + torrent.upsert_peer(&peer_2).await; let mut peer_3 = a_started_peer(3); - torrent.insert_or_update_peer(&peer_3).await; + torrent.upsert_peer(&peer_3).await; peer_3.event = AnnounceEvent::Completed; peer_3.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer_3).await; + torrent.upsert_peer(&peer_3).await; vec![peer_1, peer_2, peer_3] } } @@ -182,7 +182,7 @@ async fn it_should_update_a_peer( // Make and insert a new peer. let mut peer = a_started_peer(-1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // Get the Inserted Peer by Id. let peers = torrent.get_peers(None).await; @@ -195,7 +195,7 @@ async fn it_should_update_a_peer( // Announce "Completed" torrent download event. peer.event = AnnounceEvent::Completed; - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // Get the Updated Peer by Id. let peers = torrent.get_peers(None).await; @@ -224,7 +224,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( let mut peer = a_started_peer(-1); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // The started peer should be inserted. let peers = torrent.get_peers(None).await; @@ -237,7 +237,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( // Change peer to "Stopped" and insert. peer.event = AnnounceEvent::Stopped; - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; // It should be removed now. let peers = torrent.get_peers(None).await; @@ -270,13 +270,12 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade // Announce "Completed" torrent download event. peer.event = AnnounceEvent::Completed; - let (updated, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if is_already_completed { - assert!(!updated); assert_eq!(stats.downloaded, downloaded); } else { - assert!(updated); assert_eq!(stats.downloaded, downloaded + 1); } } @@ -301,7 +300,8 @@ async fn it_should_update_a_peer_as_a_seeder( // Set Bytes Left to Zero peer.left = NumberOfBytes(0); - let (_, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; // Add the peer + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if is_already_non_left { // it was already complete @@ -332,7 +332,8 @@ async fn it_should_update_a_peer_as_incomplete( // Set Bytes Left to no Zero peer.left = NumberOfBytes(1); - let (_, stats) = torrent.insert_or_update_peer_and_get_stats(&peer).await; // Add the peer + torrent.upsert_peer(&peer).await; + let stats = torrent.get_stats().await; if completed_already { // now it is incomplete @@ -368,7 +369,7 @@ async fn it_should_get_peers_excluding_the_client_socket( // set the address to the socket. peer.peer_addr = socket; - torrent.insert_or_update_peer(&peer).await; // Add peer + torrent.upsert_peer(&peer).await; // Add peer // It should not include the peer that has the same socket. assert!(!torrent.get_peers_for_client(&socket, None).await.contains(&peer.into())); @@ -391,7 +392,7 @@ async fn it_should_limit_the_number_of_peers_returned( for peer_number in 1..=74 + 1 { let mut peer = a_started_peer(1); peer.peer_id = peer::Id::from(peer_number); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; } let peers = torrent.get_peers(Some(TORRENT_PEERS_LIMIT)).await; @@ -422,7 +423,7 @@ async fn it_should_remove_inactive_peers_beyond_cutoff( peer.updated = now.sub(EXPIRE); - torrent.insert_or_update_peer(&peer).await; + torrent.upsert_peer(&peer).await; assert_eq!(torrent.get_peers_len().await, peers.len() + 1); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 4bd9d69e5..aaa8f6768 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -6,6 +6,7 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{NumberOfBytes, PersistentTorrents}; use torrust_tracker_torrent_repository::entry::Entry as _; use torrust_tracker_torrent_repository::repository::dash_map_mutex_std::XacrimonDashMap; @@ -72,14 +73,14 @@ fn default() -> Entries { #[fixture] fn started() -> Entries { let mut torrent = EntrySingle::default(); - torrent.insert_or_update_peer(&a_started_peer(1)); + torrent.upsert_peer(&a_started_peer(1)); vec![(InfoHash::default(), torrent)] } #[fixture] fn completed() -> Entries { let mut torrent = EntrySingle::default(); - torrent.insert_or_update_peer(&a_completed_peer(2)); + torrent.upsert_peer(&a_completed_peer(2)); vec![(InfoHash::default(), torrent)] } @@ -87,10 +88,10 @@ fn completed() -> Entries { fn downloaded() -> Entries { let mut torrent = EntrySingle::default(); let mut peer = a_started_peer(3); - torrent.insert_or_update_peer(&peer); + torrent.upsert_peer(&peer); peer.event = AnnounceEvent::Completed; peer.left = NumberOfBytes(0); - torrent.insert_or_update_peer(&peer); + torrent.upsert_peer(&peer); vec![(InfoHash::default(), torrent)] } @@ -98,21 +99,21 @@ fn downloaded() -> Entries { fn three() -> Entries { let mut started = EntrySingle::default(); let started_h = &mut DefaultHasher::default(); - started.insert_or_update_peer(&a_started_peer(1)); + started.upsert_peer(&a_started_peer(1)); started.hash(started_h); let mut completed = EntrySingle::default(); let completed_h = &mut DefaultHasher::default(); - completed.insert_or_update_peer(&a_completed_peer(2)); + completed.upsert_peer(&a_completed_peer(2)); completed.hash(completed_h); let mut downloaded = EntrySingle::default(); let downloaded_h = &mut DefaultHasher::default(); let mut downloaded_peer = a_started_peer(3); - downloaded.insert_or_update_peer(&downloaded_peer); + downloaded.upsert_peer(&downloaded_peer); downloaded_peer.event = AnnounceEvent::Completed; downloaded_peer.left = NumberOfBytes(0); - downloaded.insert_or_update_peer(&downloaded_peer); + downloaded.upsert_peer(&downloaded_peer); downloaded.hash(downloaded_h); vec![ @@ -128,7 +129,7 @@ fn many_out_of_order() -> Entries { for i in 0..408 { let mut entry = EntrySingle::default(); - entry.insert_or_update_peer(&a_started_peer(i)); + entry.upsert_peer(&a_started_peer(i)); entries.insert((InfoHash::from(i), entry)); } @@ -143,7 +144,7 @@ fn many_hashed_in_order() -> Entries { for i in 0..408 { let mut entry = EntrySingle::default(); - entry.insert_or_update_peer(&a_started_peer(i)); + entry.upsert_peer(&a_started_peer(i)); let hash: &mut DefaultHasher = &mut DefaultHasher::default(); hash.write_i32(i); @@ -390,7 +391,7 @@ async fn it_should_get_metrics( let mut metrics = TorrentsMetrics::default(); for (_, torrent) in entries { - let stats = torrent.get_stats(); + let stats = torrent.get_swarm_metadata(); metrics.torrents += 1; metrics.incomplete += u64::from(stats.incomplete); @@ -537,10 +538,25 @@ async fn it_should_remove_inactive_peers( // Insert the infohash and peer into the repository // and verify there is an extra torrent entry. { - repo.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + repo.upsert_peer(&info_hash, &peer).await; assert_eq!(repo.get_metrics().await.torrents, entries.len() as u64 + 1); } + // Insert the infohash and peer into the repository + // and verify the swarm metadata was updated. + { + repo.upsert_peer(&info_hash, &peer).await; + let stats = repo.get_swarm_metadata(&info_hash).await; + assert_eq!( + stats, + Some(SwarmMetadata { + downloaded: 0, + complete: 1, + incomplete: 0 + }) + ); + } + // Verify that this new peer was inserted into the repository. { let entry = repo.get(&info_hash).await.expect("it_should_get_some"); diff --git a/src/core/mod.rs b/src/core/mod.rs index 81b3c9572..e4c4c66ac 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -628,10 +628,9 @@ impl Tracker { peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.external_ip)); debug!("After: {peer:?}"); - // we should update the torrent and get the stats before we get the peer list. - let stats = self.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + let stats = self.upsert_peer_and_get_stats(info_hash, peer).await; - let peers = self.get_torrent_peers_for_peer(info_hash, peer); + let peers = self.get_peers_for(info_hash, peer); AnnounceData { peers, @@ -662,7 +661,7 @@ impl Tracker { /// It returns the data for a `scrape` response. fn get_swarm_metadata(&self, info_hash: &InfoHash) -> SwarmMetadata { match self.torrents.get(info_hash) { - Some(torrent_entry) => torrent_entry.get_stats(), + Some(torrent_entry) => torrent_entry.get_swarm_metadata(), None => SwarmMetadata::default(), } } @@ -683,7 +682,7 @@ impl Tracker { Ok(()) } - fn get_torrent_peers_for_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec> { + fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec> { match self.torrents.get(info_hash) { None => vec![], Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(TORRENT_PEERS_LIMIT)), @@ -705,20 +704,36 @@ impl Tracker { /// needed for a `announce` request response. /// /// # Context: Tracker - pub async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> SwarmMetadata { - // code-review: consider splitting the function in two (command and query segregation). - // `update_torrent_with_peer` and `get_stats` + pub async fn upsert_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> SwarmMetadata { + let swarm_metadata_before = match self.torrents.get_swarm_metadata(info_hash) { + Some(swarm_metadata) => swarm_metadata, + None => SwarmMetadata::zeroed(), + }; - let (stats_updated, stats) = self.torrents.update_torrent_with_peer_and_get_stats(info_hash, peer); + self.torrents.upsert_peer(info_hash, peer); - if self.policy.persistent_torrent_completed_stat && stats_updated { - let completed = stats.downloaded; + let swarm_metadata_after = match self.torrents.get_swarm_metadata(info_hash) { + Some(swarm_metadata) => swarm_metadata, + None => SwarmMetadata::zeroed(), + }; + + if swarm_metadata_before != swarm_metadata_after { + self.persist_stats(info_hash, &swarm_metadata_after).await; + } + + swarm_metadata_after + } + + /// It stores the torrents stats into the database (if persistency is enabled). + /// + /// # Context: Tracker + async fn persist_stats(&self, info_hash: &InfoHash, swarm_metadata: &SwarmMetadata) { + if self.policy.persistent_torrent_completed_stat { + let completed = swarm_metadata.downloaded; let info_hash = *info_hash; drop(self.database.save_persistent_torrent(&info_hash, completed).await); } - - stats } /// It calculates and returns the general `Tracker` @@ -1132,7 +1147,7 @@ mod tests { let info_hash = sample_info_hash(); let peer = sample_peer(); - tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; let peers = tracker.get_torrent_peers(&info_hash); @@ -1146,9 +1161,9 @@ mod tests { let info_hash = sample_info_hash(); let peer = sample_peer(); - tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; - let peers = tracker.get_torrent_peers_for_peer(&info_hash, &peer); + let peers = tracker.get_peers_for(&info_hash, &peer); assert_eq!(peers, vec![]); } @@ -1157,9 +1172,7 @@ mod tests { async fn it_should_return_the_torrent_metrics() { let tracker = public_tracker(); - tracker - .update_torrent_with_peer_and_get_stats(&sample_info_hash(), &leecher()) - .await; + tracker.upsert_peer_and_get_stats(&sample_info_hash(), &leecher()).await; let torrent_metrics = tracker.get_torrents_metrics(); @@ -1180,9 +1193,7 @@ mod tests { let start_time = std::time::Instant::now(); for i in 0..1_000_000 { - tracker - .update_torrent_with_peer_and_get_stats(&gen_seeded_infohash(&i), &leecher()) - .await; + tracker.upsert_peer_and_get_stats(&gen_seeded_infohash(&i), &leecher()).await; } let result_a = start_time.elapsed(); @@ -1706,11 +1717,11 @@ mod tests { let mut peer = sample_peer(); peer.event = AnnounceEvent::Started; - let swarm_stats = tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + let swarm_stats = tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; assert_eq!(swarm_stats.downloaded, 0); peer.event = AnnounceEvent::Completed; - let swarm_stats = tracker.update_torrent_with_peer_and_get_stats(&info_hash, &peer).await; + let swarm_stats = tracker.upsert_peer_and_get_stats(&info_hash, &peer).await; assert_eq!(swarm_stats.downloaded, 1); // Remove the newly updated torrent from memory @@ -1721,7 +1732,7 @@ mod tests { let torrent_entry = tracker.torrents.get(&info_hash).expect("it should be able to get entry"); // It persists the number of completed peers. - assert_eq!(torrent_entry.get_stats().downloaded, 1); + assert_eq!(torrent_entry.get_swarm_metadata().downloaded, 1); // It does not persist the peers assert!(torrent_entry.peers_is_empty()); diff --git a/src/core/services/torrent.rs b/src/core/services/torrent.rs index ce44af3a8..9cba5de25 100644 --- a/src/core/services/torrent.rs +++ b/src/core/services/torrent.rs @@ -50,7 +50,7 @@ pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Op let torrent_entry = torrent_entry_option?; - let stats = torrent_entry.get_stats(); + let stats = torrent_entry.get_swarm_metadata(); let peers = torrent_entry.get_peers(None); @@ -70,7 +70,7 @@ pub async fn get_torrents_page(tracker: Arc, pagination: Option<&Pagina let mut basic_infos: Vec = vec![]; for (info_hash, torrent_entry) in tracker.torrents.get_paginated(pagination) { - let stats = torrent_entry.get_stats(); + let stats = torrent_entry.get_swarm_metadata(); basic_infos.push(BasicInfo { info_hash, @@ -88,7 +88,7 @@ pub async fn get_torrents(tracker: Arc, info_hashes: &[InfoHash]) -> Ve let mut basic_infos: Vec = vec![]; for info_hash in info_hashes { - if let Some(stats) = tracker.torrents.get(info_hash).map(|t| t.get_stats()) { + if let Some(stats) = tracker.torrents.get(info_hash).map(|t| t.get_swarm_metadata()) { basic_infos.push(BasicInfo { info_hash: *info_hash, seeders: u64::from(stats.complete), @@ -156,9 +156,7 @@ mod tests { let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer()).await; let torrent_info = get_torrent_info(tracker.clone(), &info_hash).await.unwrap(); @@ -208,9 +206,7 @@ mod tests { let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash = InfoHash::from_str(&hash).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash, &sample_peer()).await; let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await; @@ -234,12 +230,8 @@ mod tests { let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let offset = 0; let limit = 1; @@ -258,12 +250,8 @@ mod tests { let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let offset = 1; let limit = 4000; @@ -288,15 +276,11 @@ mod tests { let hash1 = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); let info_hash1 = InfoHash::from_str(&hash1).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash1, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash1, &sample_peer()).await; let hash2 = "03840548643af2a7b63a9f5cbca348bc7150ca3a".to_owned(); let info_hash2 = InfoHash::from_str(&hash2).unwrap(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash2, &sample_peer()) - .await; + tracker.upsert_peer_and_get_stats(&info_hash2, &sample_peer()).await; let torrents = get_torrents_page(tracker.clone(), Some(&Pagination::default())).await; diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index 0430b02a9..c5c25be51 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -726,9 +726,7 @@ mod tests { .with_peer_address(SocketAddr::new(IpAddr::V6(client_ip_v6), client_port)) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv6) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv6).await; } async fn announce_a_new_peer_using_ipv4(tracker: Arc) -> Response { @@ -954,9 +952,7 @@ mod tests { .with_peer_address(SocketAddr::new(IpAddr::V4(client_ip_v4), client_port)) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv4) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer_using_ipv4).await; } async fn announce_a_new_peer_using_ipv6(tracker: Arc) -> Response { @@ -1129,9 +1125,7 @@ mod tests { .with_number_of_bytes_left(0) .into(); - tracker - .update_torrent_with_peer_and_get_stats(&info_hash.0.into(), &peer) - .await; + tracker.upsert_peer_and_get_stats(&info_hash.0.into(), &peer).await; } fn build_scrape_request(target: &SocketAddr, info_hash: &InfoHash) -> ScrapeRequest { diff --git a/tests/common/udp/environment.rs b/tests/common/udp/environment.rs index fed84164d..4fe2b5e14 100644 --- a/tests/common/udp/environment.rs +++ b/tests/common/udp/environment.rs @@ -24,7 +24,7 @@ impl Environment { /// Add a torrent to the tracker #[allow(dead_code)] pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 1cb07d76a..f19332dce 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -26,7 +26,7 @@ pub struct Environment { impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index f6034c264..09c6a2501 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -25,7 +25,7 @@ pub struct Environment { impl Environment { /// Add a torrent to the tracker pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { - self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; + self.tracker.upsert_peer_and_get_stats(info_hash, peer).await; } } From e60c1ebb0495bacf356a07f9884851043d563be1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:39:50 +0100 Subject: [PATCH 133/217] refactor: extract PeerList Extract a type for a collection of peers. The performance adter the exatract is similar: ```output Requests out: 415067.21/second Responses in: 369397.08/second - Connect responses: 183049.81 - Announce responses: 182717.15 - Scrape responses: 3630.12 - Error responses: 0.00 Peers per announce response: 0.00 Announce responses per info hash: - p10: 1 - p25: 1 - p50: 1 - p75: 1 - p90: 2 - p95: 3 - p99: 104 - p99.9: 297 - p100: 375 ``` --- packages/torrent-repository/src/entry/mod.rs | 68 ++++++++++++++++++- .../torrent-repository/src/entry/single.rs | 32 ++------- .../src/repository/dash_map_mutex_std.rs | 5 +- .../src/repository/rw_lock_std.rs | 6 +- .../src/repository/rw_lock_std_mutex_std.rs | 5 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 5 +- .../src/repository/rw_lock_tokio.rs | 6 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 5 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 5 +- .../src/repository/skip_map_mutex_std.rs | 5 +- 10 files changed, 88 insertions(+), 54 deletions(-) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index d72ff254b..ee80305ee 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -82,8 +82,72 @@ pub trait EntryAsync { #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Torrent { /// The swarm: a network of peers that are all trying to download the torrent associated to this entry - // #[serde(skip)] - pub(crate) peers: std::collections::BTreeMap>, + pub(crate) peers: PeerList, /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PeerList { + peers: std::collections::BTreeMap>, +} + +impl PeerList { + fn len(&self) -> usize { + self.peers.len() + } + + fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { + self.peers.insert(key, value) + } + + fn remove(&mut self, key: &peer::Id) -> Option> { + self.peers.remove(key) + } + + fn retain(&mut self, f: F) + where + F: FnMut(&peer::Id, &mut Arc) -> bool, + { + self.peers.retain(f); + } + + fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + fn get_peers(&self, limit: Option) -> Vec> { + match limit { + Some(limit) => self.peers.values().take(limit).cloned().collect(), + None => self.peers.values().cloned().collect(), + } + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + match limit { + Some(limit) => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + // Limit the number of peers on the result + .take(limit) + .cloned() + .collect(), + None => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .cloned() + .collect(), + } + } +} diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index a38b54023..36d04c3cf 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -13,13 +13,12 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] fn get_swarm_metadata(&self) -> SwarmMetadata { - let complete: u32 = self.peers.values().filter(|peer| peer.is_seeder()).count() as u32; - let incomplete: u32 = self.peers.len() as u32 - complete; + let (seeders, leechers) = self.peers.seeders_and_leechers(); SwarmMetadata { downloaded: self.downloaded, - complete, - incomplete, + complete: seeders as u32, + incomplete: leechers as u32, } } @@ -42,32 +41,13 @@ impl Entry for EntrySingle { fn get_peers_len(&self) -> usize { self.peers.len() } + fn get_peers(&self, limit: Option) -> Vec> { - match limit { - Some(limit) => self.peers.values().take(limit).cloned().collect(), - None => self.peers.values().cloned().collect(), - } + self.peers.get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - match limit { - Some(limit) => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - // Limit the number of peers on the result - .take(limit) - .cloned() - .collect(), - None => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - .cloned() - .collect(), - } + self.peers.get_peers_for_client(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index b398b09dc..2aba7e54f 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use dashmap::DashMap; @@ -10,7 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] @@ -82,7 +81,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index af48428e4..7d8055fca 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,7 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::Entry; +use crate::entry::{Entry, PeerList}; use crate::{EntrySingle, TorrentsRwLockStd}; #[derive(Default, Debug)] @@ -102,7 +100,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *downloaded, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 74cdc4475..629f3484e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockStdMutexStd}; impl TorrentsRwLockStdMutexStd { @@ -97,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index 842a9bbfe..c148026b2 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::iter::zip; use std::sync::Arc; @@ -12,7 +11,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync}; +use crate::entry::{Entry, EntryAsync, PeerList}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockStdMutexTokio}; impl TorrentsRwLockStdMutexTokio { @@ -105,7 +104,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index b95f1e31e..0a481fdde 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::pagination::Pagination; @@ -8,7 +6,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::Entry; +use crate::entry::{Entry, PeerList}; use crate::{EntrySingle, TorrentsRwLockTokio}; #[derive(Default, Debug)] @@ -106,7 +104,7 @@ where } let entry = EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index bde959940..d3b17c2d2 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockTokioMutexStd}; impl TorrentsRwLockTokioMutexStd { @@ -97,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index 1d002e317..875a890ea 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use torrust_tracker_configuration::TrackerPolicy; @@ -9,7 +8,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync}; +use crate::entry::{Entry, EntryAsync, PeerList}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockTokioMutexTokio}; impl TorrentsRwLockTokioMutexTokio { @@ -100,7 +99,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index ef3e7e478..b3c71a4de 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use crossbeam_skiplist::SkipMap; @@ -10,7 +9,7 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync}; +use crate::entry::{Entry, EntrySync, PeerList}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] @@ -76,7 +75,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: BTreeMap::default(), + peers: PeerList::default(), downloaded: *completed, } .into(), From 57506ac0d6876e41101273ffeddc37f35ae1b6a0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:48:08 +0100 Subject: [PATCH 134/217] refactor: rename field from peers to swarm --- packages/torrent-repository/src/entry/mod.rs | 4 ++-- .../torrent-repository/src/entry/single.rs | 20 +++++++++---------- .../src/repository/dash_map_mutex_std.rs | 2 +- .../src/repository/rw_lock_std.rs | 2 +- .../src/repository/rw_lock_std_mutex_std.rs | 2 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 2 +- .../src/repository/rw_lock_tokio.rs | 2 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 2 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 2 +- .../src/repository/skip_map_mutex_std.rs | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index ee80305ee..648ded98a 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -81,8 +81,8 @@ pub trait EntryAsync { /// The tracker keeps one entry like this for every torrent. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Torrent { - /// The swarm: a network of peers that are all trying to download the torrent associated to this entry - pub(crate) peers: PeerList, + /// A network of peers that are all trying to download the torrent associated to this entry + pub(crate) swarm: PeerList, /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index 36d04c3cf..169ee2fbb 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -13,7 +13,7 @@ use crate::EntrySingle; impl Entry for EntrySingle { #[allow(clippy::cast_possible_truncation)] fn get_swarm_metadata(&self) -> SwarmMetadata { - let (seeders, leechers) = self.peers.seeders_and_leechers(); + let (seeders, leechers) = self.swarm.seeders_and_leechers(); SwarmMetadata { downloaded: self.downloaded, @@ -27,7 +27,7 @@ impl Entry for EntrySingle { return true; } - if policy.remove_peerless_torrents && self.peers.is_empty() { + if policy.remove_peerless_torrents && self.swarm.is_empty() { return false; } @@ -35,19 +35,19 @@ impl Entry for EntrySingle { } fn peers_is_empty(&self) -> bool { - self.peers.is_empty() + self.swarm.is_empty() } fn get_peers_len(&self) -> usize { - self.peers.len() + self.swarm.len() } fn get_peers(&self, limit: Option) -> Vec> { - self.peers.get_peers(limit) + self.swarm.get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.peers.get_peers_for_client(client, limit) + self.swarm.get_peers_for_client(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { @@ -55,10 +55,10 @@ impl Entry for EntrySingle { match peer::ReadInfo::get_event(peer) { AnnounceEvent::Stopped => { - drop(self.peers.remove(&peer::ReadInfo::get_id(peer))); + drop(self.swarm.remove(&peer::ReadInfo::get_id(peer))); } AnnounceEvent::Completed => { - let previous = self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); + let previous = self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); // Don't count if peer was not previously known and not already completed. if previous.is_some_and(|p| p.event != AnnounceEvent::Completed) { self.downloaded += 1; @@ -66,7 +66,7 @@ impl Entry for EntrySingle { } } _ => { - drop(self.peers.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); + drop(self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); } } @@ -74,7 +74,7 @@ impl Entry for EntrySingle { } fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { - self.peers + self.swarm .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); } } diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 2aba7e54f..7bc60dbc6 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -81,7 +81,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 7d8055fca..800b1b31f 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -100,7 +100,7 @@ where } let entry = EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *downloaded, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 629f3484e..9fefc9115 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -96,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index c148026b2..e0a53e48a 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -104,7 +104,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index 0a481fdde..0987b064a 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -104,7 +104,7 @@ where } let entry = EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, }; diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index d3b17c2d2..77a82e445 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -96,7 +96,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index 875a890ea..fc7608010 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -99,7 +99,7 @@ where let entry = EntryMutexTokio::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index b3c71a4de..7f84dae2a 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -75,7 +75,7 @@ where let entry = EntryMutexStd::new( EntrySingle { - peers: PeerList::default(), + swarm: PeerList::default(), downloaded: *completed, } .into(), From ca0a7bb52a8db2fa8aa00a6cae40298e254994d4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 15 Apr 2024 17:58:20 +0100 Subject: [PATCH 135/217] refactor: extract mod peer_list --- packages/torrent-repository/src/entry/mod.rs | 69 +---------------- .../torrent-repository/src/entry/peer_list.rs | 74 +++++++++++++++++++ .../src/repository/dash_map_mutex_std.rs | 3 +- .../src/repository/rw_lock_std.rs | 3 +- .../src/repository/rw_lock_std_mutex_std.rs | 3 +- .../src/repository/rw_lock_std_mutex_tokio.rs | 3 +- .../src/repository/rw_lock_tokio.rs | 3 +- .../src/repository/rw_lock_tokio_mutex_std.rs | 3 +- .../repository/rw_lock_tokio_mutex_tokio.rs | 3 +- .../src/repository/skip_map_mutex_std.rs | 3 +- 10 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 packages/torrent-repository/src/entry/peer_list.rs diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 648ded98a..40fa4efd5 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -2,13 +2,15 @@ use std::fmt::Debug; use std::net::SocketAddr; use std::sync::Arc; -//use serde::{Deserialize, Serialize}; use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; +use self::peer_list::PeerList; + pub mod mutex_std; pub mod mutex_tokio; +pub mod peer_list; pub mod single; pub trait Entry { @@ -86,68 +88,3 @@ pub struct Torrent { /// The number of peers that have ever completed downloading the torrent associated to this entry pub(crate) downloaded: u32, } - -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PeerList { - peers: std::collections::BTreeMap>, -} - -impl PeerList { - fn len(&self) -> usize { - self.peers.len() - } - - fn is_empty(&self) -> bool { - self.peers.is_empty() - } - - fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { - self.peers.insert(key, value) - } - - fn remove(&mut self, key: &peer::Id) -> Option> { - self.peers.remove(key) - } - - fn retain(&mut self, f: F) - where - F: FnMut(&peer::Id, &mut Arc) -> bool, - { - self.peers.retain(f); - } - - fn seeders_and_leechers(&self) -> (usize, usize) { - let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); - let leechers = self.len() - seeders; - - (seeders, leechers) - } - - fn get_peers(&self, limit: Option) -> Vec> { - match limit { - Some(limit) => self.peers.values().take(limit).cloned().collect(), - None => self.peers.values().cloned().collect(), - } - } - - fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - match limit { - Some(limit) => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - // Limit the number of peers on the result - .take(limit) - .cloned() - .collect(), - None => self - .peers - .values() - // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) - .cloned() - .collect(), - } - } -} diff --git a/packages/torrent-repository/src/entry/peer_list.rs b/packages/torrent-repository/src/entry/peer_list.rs new file mode 100644 index 000000000..4af9b1d77 --- /dev/null +++ b/packages/torrent-repository/src/entry/peer_list.rs @@ -0,0 +1,74 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_primitives::peer; + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PeerList { + peers: std::collections::BTreeMap>, +} + +impl PeerList { + #[must_use] + pub fn len(&self) -> usize { + self.peers.len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.peers.is_empty() + } + + pub fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { + self.peers.insert(key, value) + } + + pub fn remove(&mut self, key: &peer::Id) -> Option> { + self.peers.remove(key) + } + + pub fn retain(&mut self, f: F) + where + F: FnMut(&peer::Id, &mut Arc) -> bool, + { + self.peers.retain(f); + } + + #[must_use] + pub fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + #[must_use] + pub fn get_peers(&self, limit: Option) -> Vec> { + match limit { + Some(limit) => self.peers.values().take(limit).cloned().collect(), + None => self.peers.values().cloned().collect(), + } + } + + #[must_use] + pub fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + match limit { + Some(limit) => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + // Limit the number of peers on the result + .take(limit) + .cloned() + .collect(), + None => self + .peers + .values() + // Take peers which are not the client peer + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .cloned() + .collect(), + } + } +} diff --git a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs index 7bc60dbc6..a38205205 100644 --- a/packages/torrent-repository/src/repository/dash_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/dash_map_mutex_std.rs @@ -9,7 +9,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_std.rs b/packages/torrent-repository/src/repository/rw_lock_std.rs index 800b1b31f..0d96a2375 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std.rs @@ -6,7 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockStd}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs index 9fefc9115..76d5e8f1e 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_std.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockStdMutexStd}; impl TorrentsRwLockStdMutexStd { diff --git a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs index e0a53e48a..731866aeb 100644 --- a/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_std_mutex_tokio.rs @@ -11,7 +11,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockStdMutexTokio}; impl TorrentsRwLockStdMutexTokio { diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio.rs index 0987b064a..c360106b8 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio.rs @@ -6,7 +6,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::Entry; use crate::{EntrySingle, TorrentsRwLockTokio}; #[derive(Default, Debug)] diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs index 77a82e445..9fce79b44 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_std.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle, TorrentsRwLockTokioMutexStd}; impl TorrentsRwLockTokioMutexStd { diff --git a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs index fc7608010..c7e0d4054 100644 --- a/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs +++ b/packages/torrent-repository/src/repository/rw_lock_tokio_mutex_tokio.rs @@ -8,7 +8,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::RepositoryAsync; -use crate::entry::{Entry, EntryAsync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntryAsync}; use crate::{EntryMutexTokio, EntrySingle, TorrentsRwLockTokioMutexTokio}; impl TorrentsRwLockTokioMutexTokio { diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 7f84dae2a..bc9ecd066 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -9,7 +9,8 @@ use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrents}; use super::Repository; -use crate::entry::{Entry, EntrySync, PeerList}; +use crate::entry::peer_list::PeerList; +use crate::entry::{Entry, EntrySync}; use crate::{EntryMutexStd, EntrySingle}; #[derive(Default, Debug)] From e5b307060712f6f56052a95ea03f85d0f6082ad7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 12:00:28 +0100 Subject: [PATCH 136/217] test: add tests for PeerList type --- packages/primitives/src/peer.rs | 39 +++ .../torrent-repository/src/entry/peer_list.rs | 249 ++++++++++++++++-- .../torrent-repository/src/entry/single.rs | 11 +- 3 files changed, 276 insertions(+), 23 deletions(-) diff --git a/packages/primitives/src/peer.rs b/packages/primitives/src/peer.rs index f71067065..07162c886 100644 --- a/packages/primitives/src/peer.rs +++ b/packages/primitives/src/peer.rs @@ -362,6 +362,38 @@ pub mod fixture { } impl PeerBuilder { + #[allow(dead_code)] + #[must_use] + pub fn seeder() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000001"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Completed, + }; + + Self { peer } + } + + #[allow(dead_code)] + #[must_use] + pub fn leecher() -> Self { + let peer = Peer { + peer_id: Id(*b"-qB00000000000000002"), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 8080), + updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(10), + event: AnnounceEvent::Started, + }; + + Self { peer } + } + #[allow(dead_code)] #[must_use] pub fn with_peer_id(mut self, peer_id: &Id) -> Self { @@ -397,6 +429,13 @@ pub mod fixture { self } + #[allow(dead_code)] + #[must_use] + pub fn last_updated_on(mut self, updated: DurationSinceUnixEpoch) -> Self { + self.peer.updated = updated; + self + } + #[allow(dead_code)] #[must_use] pub fn build(self) -> Peer { diff --git a/packages/torrent-repository/src/entry/peer_list.rs b/packages/torrent-repository/src/entry/peer_list.rs index 4af9b1d77..3f69edbb5 100644 --- a/packages/torrent-repository/src/entry/peer_list.rs +++ b/packages/torrent-repository/src/entry/peer_list.rs @@ -1,7 +1,13 @@ +//! A peer list. use std::net::SocketAddr; use std::sync::Arc; -use torrust_tracker_primitives::peer; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +// code-review: the current implementation uses the peer Id as the ``BTreeMap`` +// key. That would allow adding two identical peers except for the Id. +// For example, two peers with the same socket address but a different peer Id +// would be allowed. That would lead to duplicated peers in the tracker responses. #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PeerList { @@ -19,31 +25,26 @@ impl PeerList { self.peers.is_empty() } - pub fn insert(&mut self, key: peer::Id, value: Arc) -> Option> { - self.peers.insert(key, value) + pub fn upsert(&mut self, value: Arc) -> Option> { + self.peers.insert(value.peer_id, value) } pub fn remove(&mut self, key: &peer::Id) -> Option> { self.peers.remove(key) } - pub fn retain(&mut self, f: F) - where - F: FnMut(&peer::Id, &mut Arc) -> bool, - { - self.peers.retain(f); + pub fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { + self.peers + .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); } #[must_use] - pub fn seeders_and_leechers(&self) -> (usize, usize) { - let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); - let leechers = self.len() - seeders; - - (seeders, leechers) + pub fn get(&self, peer_id: &peer::Id) -> Option<&Arc> { + self.peers.get(peer_id) } #[must_use] - pub fn get_peers(&self, limit: Option) -> Vec> { + pub fn get_all(&self, limit: Option) -> Vec> { match limit { Some(limit) => self.peers.values().take(limit).cloned().collect(), None => self.peers.values().cloned().collect(), @@ -51,13 +52,21 @@ impl PeerList { } #[must_use] - pub fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + pub fn seeders_and_leechers(&self) -> (usize, usize) { + let seeders = self.peers.values().filter(|peer| peer.is_seeder()).count(); + let leechers = self.len() - seeders; + + (seeders, leechers) + } + + #[must_use] + pub fn get_peers_excluding_addr(&self, peer_addr: &SocketAddr, limit: Option) -> Vec> { match limit { Some(limit) => self .peers .values() // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) // Limit the number of peers on the result .take(limit) .cloned() @@ -66,9 +75,215 @@ impl PeerList { .peers .values() // Take peers which are not the client peer - .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *client) + .filter(|peer| peer::ReadInfo::get_address(peer.as_ref()) != *peer_addr) .cloned() .collect(), } } } + +#[cfg(test)] +mod tests { + + mod it_should { + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::Arc; + + use torrust_tracker_primitives::peer::fixture::PeerBuilder; + use torrust_tracker_primitives::peer::{self}; + use torrust_tracker_primitives::DurationSinceUnixEpoch; + + use crate::entry::peer_list::PeerList; + + #[test] + fn be_empty_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert!(peer_list.is_empty()); + } + + #[test] + fn have_zero_length_when_no_peers_have_been_inserted() { + let peer_list = PeerList::default(); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn allow_inserting_a_new_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + assert_eq!(peer_list.upsert(peer.into()), None); + } + + #[test] + fn allow_updating_a_preexisting_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.upsert(peer.into()), Some(Arc::new(peer))); + } + + #[test] + fn allow_getting_all_peers() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get_all(None), [Arc::new(peer)]); + } + + #[test] + fn allow_getting_one_peer_by_id() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.get(&peer.peer_id), Some(Arc::new(peer)).as_ref()); + } + + #[test] + fn increase_the_number_of_peers_after_inserting_a_new_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn decrease_the_number_of_peers_after_removing_one() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert!(peer_list.is_empty()); + } + + #[test] + fn allow_removing_an_existing_peer() { + let mut peer_list = PeerList::default(); + + let peer = PeerBuilder::default().build(); + + peer_list.upsert(peer.into()); + + peer_list.remove(&peer.peer_id); + + assert_eq!(peer_list.get(&peer.peer_id), None); + } + + #[test] + fn allow_getting_all_peers_excluding_peers_with_a_given_address() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6969)) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 6969)) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.get_peers_excluding_addr(&peer2.peer_addr, None), [Arc::new(peer1)]); + } + + #[test] + fn return_the_number_of_seeders_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (seeders, _leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(seeders, 1); + } + + #[test] + fn return_the_number_of_leechers_in_the_list() { + let mut peer_list = PeerList::default(); + + let seeder = PeerBuilder::seeder().build(); + let leecher = PeerBuilder::leecher().build(); + + peer_list.upsert(seeder.into()); + peer_list.upsert(leecher.into()); + + let (_seeders, leechers) = peer_list.seeders_and_leechers(); + + assert_eq!(leechers, 1); + } + + #[test] + fn remove_inactive_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second after inserting the peer + peer_list.remove_inactive_peers(last_update_time + one_second); + + assert_eq!(peer_list.len(), 0); + } + + #[test] + fn not_remove_active_peers() { + let mut peer_list = PeerList::default(); + let one_second = DurationSinceUnixEpoch::new(1, 0); + + // Insert the peer + let last_update_time = DurationSinceUnixEpoch::new(1_669_397_478_934, 0); + let peer = PeerBuilder::default().last_updated_on(last_update_time).build(); + peer_list.upsert(peer.into()); + + // Remove peers not updated since one second before inserting the peer. + peer_list.remove_inactive_peers(last_update_time - one_second); + + assert_eq!(peer_list.len(), 1); + } + + #[test] + fn allow_inserting_two_identical_peers_except_for_the_id() { + let mut peer_list = PeerList::default(); + + let peer1 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + peer_list.upsert(peer1.into()); + + let peer2 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .build(); + peer_list.upsert(peer2.into()); + + assert_eq!(peer_list.len(), 2); + } + } +} diff --git a/packages/torrent-repository/src/entry/single.rs b/packages/torrent-repository/src/entry/single.rs index 169ee2fbb..a01124454 100644 --- a/packages/torrent-repository/src/entry/single.rs +++ b/packages/torrent-repository/src/entry/single.rs @@ -43,11 +43,11 @@ impl Entry for EntrySingle { } fn get_peers(&self, limit: Option) -> Vec> { - self.swarm.get_peers(limit) + self.swarm.get_all(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.swarm.get_peers_for_client(client, limit) + self.swarm.get_peers_excluding_addr(client, limit) } fn upsert_peer(&mut self, peer: &peer::Peer) -> bool { @@ -58,7 +58,7 @@ impl Entry for EntrySingle { drop(self.swarm.remove(&peer::ReadInfo::get_id(peer))); } AnnounceEvent::Completed => { - let previous = self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer)); + let previous = self.swarm.upsert(Arc::new(*peer)); // Don't count if peer was not previously known and not already completed. if previous.is_some_and(|p| p.event != AnnounceEvent::Completed) { self.downloaded += 1; @@ -66,7 +66,7 @@ impl Entry for EntrySingle { } } _ => { - drop(self.swarm.insert(peer::ReadInfo::get_id(peer), Arc::new(*peer))); + drop(self.swarm.upsert(Arc::new(*peer))); } } @@ -74,7 +74,6 @@ impl Entry for EntrySingle { } fn remove_inactive_peers(&mut self, current_cutoff: DurationSinceUnixEpoch) { - self.swarm - .retain(|_, peer| peer::ReadInfo::get_updated(peer) > current_cutoff); + self.swarm.remove_inactive_peers(current_cutoff); } } From fae47b175ad1885600a15baed8a9b04b27a63117 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 14:16:13 +0100 Subject: [PATCH 137/217] chore(deps): add dependency parking_lot It provides implementations of Mutex and RwLock that are smaller, faster and more flexible than those in the Rust standard library. It will be used to check if a new torrent repo implementation using these lock is faster. --- Cargo.lock | 2 ++ Cargo.toml | 1 + packages/torrent-repository/Cargo.toml | 1 + 3 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 28ad86cb2..1addf8bb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3688,6 +3688,7 @@ dependencies = [ "local-ip-address", "mockall", "multimap", + "parking_lot", "percent-encoding", "r2d2", "r2d2_mysql", @@ -3787,6 +3788,7 @@ dependencies = [ "crossbeam-skiplist", "dashmap", "futures", + "parking_lot", "rstest", "tokio", "torrust-tracker-clock", diff --git a/Cargo.toml b/Cargo.toml index 0f0b016ee..f633d5b9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ hex-literal = "0" hyper = "1" lazy_static = "1" multimap = "0" +parking_lot = "0.12.1" percent-encoding = "2" r2d2 = "0" r2d2_mysql = "24" diff --git a/packages/torrent-repository/Cargo.toml b/packages/torrent-repository/Cargo.toml index 5364a59a7..7e4d1f214 100644 --- a/packages/torrent-repository/Cargo.toml +++ b/packages/torrent-repository/Cargo.toml @@ -19,6 +19,7 @@ version.workspace = true crossbeam-skiplist = "0.1" dashmap = "5.5.3" futures = "0.3.29" +parking_lot = "0.12.1" tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] } torrust-tracker-clock = { version = "3.0.0-alpha.12-develop", path = "../clock" } torrust-tracker-configuration = { version = "3.0.0-alpha.12-develop", path = "../configuration" } From 7c5083d51f1f74b71367398e8b20ca2745a822a8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 14:21:10 +0100 Subject: [PATCH 138/217] feat: new torrent repo implementation using parking_lot RwLock --- .../benches/repository_benchmark.rs | 22 ++++- packages/torrent-repository/src/entry/mod.rs | 1 + .../src/entry/mutex_parking_lot.rs | 49 ++++++++++ packages/torrent-repository/src/lib.rs | 7 ++ .../src/repository/skip_map_mutex_std.rs | 93 ++++++++++++++++++- .../torrent-repository/tests/common/repo.rs | 18 ++++ .../tests/common/torrent.rs | 11 ++- .../torrent-repository/tests/entry/mod.rs | 34 +++---- .../tests/repository/mod.rs | 31 +++++-- 9 files changed, 238 insertions(+), 28 deletions(-) create mode 100644 packages/torrent-repository/src/entry/mutex_parking_lot.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 58cd70d9a..75e7fd5b8 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,7 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, }; use crate::helpers::{asyn, sync}; @@ -49,6 +49,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.bench_function("DashMapMutexStd", |b| { b.iter_custom(sync::add_one_torrent::); }); @@ -102,6 +106,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); @@ -156,6 +165,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); @@ -211,6 +225,12 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapRwLockParkingLot", |b| { + b.to_async(&rt).iter_custom(|iters| { + sync::update_multiple_torrents_in_parallel::(&rt, iters, None) + }); + }); + group.bench_function("DashMapMutexStd", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index 40fa4efd5..dbe1416be 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -8,6 +8,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use self::peer_list::PeerList; +pub mod mutex_parking_lot; pub mod mutex_std; pub mod mutex_tokio; pub mod peer_list; diff --git a/packages/torrent-repository/src/entry/mutex_parking_lot.rs b/packages/torrent-repository/src/entry/mutex_parking_lot.rs new file mode 100644 index 000000000..ef0e958d5 --- /dev/null +++ b/packages/torrent-repository/src/entry/mutex_parking_lot.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +use super::{Entry, EntrySync}; +use crate::{EntryRwLockParkingLot, EntrySingle}; + +impl EntrySync for EntryRwLockParkingLot { + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.read().get_swarm_metadata() + } + + fn is_good(&self, policy: &TrackerPolicy) -> bool { + self.read().is_good(policy) + } + + fn peers_is_empty(&self) -> bool { + self.read().peers_is_empty() + } + + fn get_peers_len(&self) -> usize { + self.read().get_peers_len() + } + + fn get_peers(&self, limit: Option) -> Vec> { + self.read().get_peers(limit) + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + self.read().get_peers_for_client(client, limit) + } + + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.write().upsert_peer(peer) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + self.write().remove_inactive_peers(current_cutoff); + } +} + +impl From for EntryRwLockParkingLot { + fn from(entry: EntrySingle) -> Self { + Arc::new(parking_lot::RwLock::new(entry)) + } +} diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 7a6d209b9..5d3a7ed45 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -9,9 +9,14 @@ use torrust_tracker_clock::clock; pub mod entry; pub mod repository; +// Repo Entries + pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; +pub type EntryRwLockParkingLot = Arc>; + +// Repos pub type TorrentsRwLockStd = RwLockStd; pub type TorrentsRwLockStdMutexStd = RwLockStd; @@ -21,6 +26,8 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsSkipMapRwLockParkingLot = CrossbeamSkipList; + pub type TorrentsDashMapMutexStd = XacrimonDashMap; /// This code needs to be copied into each crate. diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index bc9ecd066..0a2a566e7 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -11,7 +11,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use super::Repository; use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; -use crate::{EntryMutexStd, EntrySingle}; +use crate::{EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { @@ -108,3 +108,94 @@ where } } } + +impl Repository for CrossbeamSkipList +where + EntryRwLockParkingLot: EntrySync, + EntrySingle: Entry, +{ + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().read().get_swarm_metadata(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryRwLockParkingLot)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryRwLockParkingLot::new( + EntrySingle { + swarm: PeerList::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index 7c245fe04..c5da6258d 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -8,6 +8,7 @@ use torrust_tracker_torrent_repository::repository::{Repository as _, Repository use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, + TorrentsSkipMapRwLockParkingLot, }; #[derive(Debug)] @@ -19,6 +20,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + SkipMapRwLockParkingLot(TorrentsSkipMapRwLockParkingLot), DashMapMutexStd(TorrentsDashMapMutexStd), } @@ -32,6 +34,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::SkipMapRwLockParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), } } @@ -45,6 +48,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::SkipMapRwLockParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), } } @@ -58,6 +62,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::SkipMapRwLockParkingLot(repo) => Some(repo.get(key)?.read().clone()), Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } } @@ -71,6 +76,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::SkipMapRwLockParkingLot(repo) => repo.get_metrics(), Repo::DashMapMutexStd(repo) => repo.get_metrics(), } } @@ -111,6 +117,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::SkipMapRwLockParkingLot(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.read().clone())) + .collect(), Repo::DashMapMutexStd(repo) => repo .get_paginated(pagination) .iter() @@ -128,6 +139,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::SkipMapRwLockParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } } @@ -141,6 +153,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::SkipMapRwLockParkingLot(repo) => Some(repo.remove(key)?.write().clone()), Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } } @@ -154,6 +167,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::SkipMapRwLockParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } } @@ -167,6 +181,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::SkipMapRwLockParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } } @@ -194,6 +209,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::SkipMapRwLockParkingLot(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } Repo::DashMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index c0699479e..f672d14ef 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -5,13 +5,14 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_torrent_repository::entry::{Entry as _, EntryAsync as _, EntrySync as _}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntrySingle}; +use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; #[derive(Debug, Clone)] pub(crate) enum Torrent { Single(EntrySingle), MutexStd(EntryMutexStd), MutexTokio(EntryMutexTokio), + RwLockParkingLot(EntryRwLockParkingLot), } impl Torrent { @@ -20,6 +21,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_swarm_metadata(), Torrent::MutexStd(entry) => entry.get_swarm_metadata(), Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, + Torrent::RwLockParkingLot(entry) => entry.clone().get_swarm_metadata(), } } @@ -28,6 +30,7 @@ impl Torrent { Torrent::Single(entry) => entry.is_good(policy), Torrent::MutexStd(entry) => entry.is_good(policy), Torrent::MutexTokio(entry) => entry.clone().check_good(policy).await, + Torrent::RwLockParkingLot(entry) => entry.is_good(policy), } } @@ -36,6 +39,7 @@ impl Torrent { Torrent::Single(entry) => entry.peers_is_empty(), Torrent::MutexStd(entry) => entry.peers_is_empty(), Torrent::MutexTokio(entry) => entry.clone().peers_is_empty().await, + Torrent::RwLockParkingLot(entry) => entry.peers_is_empty(), } } @@ -44,6 +48,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_len(), Torrent::MutexStd(entry) => entry.get_peers_len(), Torrent::MutexTokio(entry) => entry.clone().get_peers_len().await, + Torrent::RwLockParkingLot(entry) => entry.get_peers_len(), } } @@ -52,6 +57,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers(limit), Torrent::MutexStd(entry) => entry.get_peers(limit), Torrent::MutexTokio(entry) => entry.clone().get_peers(limit).await, + Torrent::RwLockParkingLot(entry) => entry.get_peers(limit), } } @@ -60,6 +66,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexStd(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexTokio(entry) => entry.clone().get_peers_for_client(client, limit).await, + Torrent::RwLockParkingLot(entry) => entry.get_peers_for_client(client, limit), } } @@ -68,6 +75,7 @@ impl Torrent { Torrent::Single(entry) => entry.upsert_peer(peer), Torrent::MutexStd(entry) => entry.upsert_peer(peer), Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, + Torrent::RwLockParkingLot(entry) => entry.upsert_peer(peer), } } @@ -76,6 +84,7 @@ impl Torrent { Torrent::Single(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexStd(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexTokio(entry) => entry.clone().remove_inactive_peers(current_cutoff).await, + Torrent::RwLockParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), } } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index 3c564c6f8..aa3126000 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -9,7 +9,7 @@ use torrust_tracker_configuration::{TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::peer::Peer; use torrust_tracker_primitives::{peer, NumberOfBytes}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntrySingle}; +use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; use crate::common::torrent::Torrent; use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; @@ -20,7 +20,7 @@ fn single() -> Torrent { Torrent::Single(EntrySingle::default()) } #[fixture] -fn standard_mutex() -> Torrent { +fn mutex_std() -> Torrent { Torrent::MutexStd(EntryMutexStd::default()) } @@ -29,6 +29,11 @@ fn mutex_tokio() -> Torrent { Torrent::MutexTokio(EntryMutexTokio::default()) } +#[fixture] +fn rw_lock_parking_lot() -> Torrent { + Torrent::RwLockParkingLot(EntryRwLockParkingLot::default()) +} + #[fixture] fn policy_none() -> TrackerPolicy { TrackerPolicy::new(false, 0, false) @@ -99,7 +104,7 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { #[case::empty(&Makes::Empty)] #[tokio::test] async fn it_should_be_empty_by_default( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -115,7 +120,7 @@ async fn it_should_be_empty_by_default( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_check_if_entry_is_good( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { @@ -153,7 +158,7 @@ async fn it_should_check_if_entry_is_good( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_for_torrent_entry( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -174,10 +179,7 @@ async fn it_should_get_peers_for_torrent_entry( #[case::downloaded(&Makes::Downloaded)] #[case::three(&Makes::Three)] #[tokio::test] -async fn it_should_update_a_peer( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, - #[case] makes: &Makes, -) { +async fn it_should_update_a_peer(#[values(single(), mutex_std(), mutex_tokio())] mut torrent: Torrent, #[case] makes: &Makes) { make(&mut torrent, makes).await; // Make and insert a new peer. @@ -215,7 +217,7 @@ async fn it_should_update_a_peer( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_a_peer_upon_stopped_announcement( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { use torrust_tracker_primitives::peer::ReadInfo as _; @@ -256,7 +258,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloaded_statistic( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -287,7 +289,7 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_a_seeder( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -319,7 +321,7 @@ async fn it_should_update_a_peer_as_a_seeder( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_incomplete( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -351,7 +353,7 @@ async fn it_should_update_a_peer_as_incomplete( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_excluding_the_client_socket( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -383,7 +385,7 @@ async fn it_should_get_peers_excluding_the_client_socket( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_limit_the_number_of_peers_returned( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -408,7 +410,7 @@ async fn it_should_limit_the_number_of_peers_returned( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_inactive_peers_beyond_cutoff( - #[values(single(), standard_mutex(), mutex_tokio())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { const TIMEOUT: Duration = Duration::from_secs(120); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index aaa8f6768..23a99e5c1 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -49,10 +49,15 @@ fn tokio_tokio() -> Repo { } #[fixture] -fn skip_list_std() -> Repo { +fn skip_list_mutex_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn skip_list_rw_lock_parking_lot() -> Repo { + Repo::SkipMapRwLockParkingLot(CrossbeamSkipList::default()) +} + #[fixture] fn dash_map_std() -> Repo { Repo::DashMapMutexStd(XacrimonDashMap::default()) @@ -246,7 +251,8 @@ async fn it_should_get_a_torrent_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -279,7 +285,8 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot() )] repo: Repo, #[case] entries: Entries, @@ -321,7 +328,8 @@ async fn it_should_get_paginated( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std() + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot() )] repo: Repo, #[case] entries: Entries, @@ -378,7 +386,8 @@ async fn it_should_get_metrics( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -420,7 +429,8 @@ async fn it_should_import_persistent_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -459,7 +469,8 @@ async fn it_should_remove_an_entry( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -496,7 +507,8 @@ async fn it_should_remove_inactive_peers( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, @@ -594,7 +606,8 @@ async fn it_should_remove_peerless_torrents( tokio_std(), tokio_mutex(), tokio_tokio(), - skip_list_std(), + skip_list_mutex_std(), + skip_list_rw_lock_parking_lot(), dash_map_std() )] repo: Repo, From 96df6adbb49c6c842dc0451c730de53074e3b16e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 16:18:44 +0100 Subject: [PATCH 139/217] chore(deps): add parking_lot to cargo machete It's used for benchmarking in the torrent-repository workspace package. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f633d5b9f..94ff86419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap"] +ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] [dev-dependencies] local-ip-address = "0" From 68c4fbbbd3d7ca9f70b7684f324de77bc34a3f79 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 16 Apr 2024 16:28:38 +0100 Subject: [PATCH 140/217] feat: new torrent repo implementation using parking_lot Mutex --- .../benches/repository_benchmark.rs | 23 ++++- packages/torrent-repository/src/entry/mod.rs | 1 + .../src/entry/mutex_parking_lot.rs | 24 ++--- .../src/entry/rw_lock_parking_lot.rs | 49 ++++++++++ packages/torrent-repository/src/lib.rs | 2 + .../src/repository/skip_map_mutex_std.rs | 93 ++++++++++++++++++- .../torrent-repository/tests/common/repo.rs | 21 ++++- .../tests/common/torrent.rs | 13 ++- .../torrent-repository/tests/entry/mod.rs | 29 +++--- .../tests/repository/mod.rs | 13 +++ 10 files changed, 240 insertions(+), 28 deletions(-) create mode 100644 packages/torrent-repository/src/entry/rw_lock_parking_lot.rs diff --git a/packages/torrent-repository/benches/repository_benchmark.rs b/packages/torrent-repository/benches/repository_benchmark.rs index 75e7fd5b8..4e50f1454 100644 --- a/packages/torrent-repository/benches/repository_benchmark.rs +++ b/packages/torrent-repository/benches/repository_benchmark.rs @@ -5,7 +5,8 @@ mod helpers; use criterion::{criterion_group, criterion_main, Criterion}; use torrust_tracker_torrent_repository::{ TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, TorrentsRwLockTokio, - TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, + TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexParkingLot, TorrentsSkipMapMutexStd, + TorrentsSkipMapRwLockParkingLot, }; use crate::helpers::{asyn, sync}; @@ -49,6 +50,10 @@ fn add_one_torrent(c: &mut Criterion) { b.iter_custom(sync::add_one_torrent::); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.iter_custom(sync::add_one_torrent::); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.iter_custom(sync::add_one_torrent::); }); @@ -106,6 +111,11 @@ fn add_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt) .iter_custom(|iters| sync::add_multiple_torrents_in_parallel::(&rt, iters, None)); @@ -165,6 +175,11 @@ fn update_one_torrent_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt) + .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt) .iter_custom(|iters| sync::update_one_torrent_in_parallel::(&rt, iters, None)); @@ -225,6 +240,12 @@ fn update_multiple_torrents_in_parallel(c: &mut Criterion) { .iter_custom(|iters| sync::update_multiple_torrents_in_parallel::(&rt, iters, None)); }); + group.bench_function("SkipMapMutexParkingLot", |b| { + b.to_async(&rt).iter_custom(|iters| { + sync::update_multiple_torrents_in_parallel::(&rt, iters, None) + }); + }); + group.bench_function("SkipMapRwLockParkingLot", |b| { b.to_async(&rt).iter_custom(|iters| { sync::update_multiple_torrents_in_parallel::(&rt, iters, None) diff --git a/packages/torrent-repository/src/entry/mod.rs b/packages/torrent-repository/src/entry/mod.rs index dbe1416be..b811d3262 100644 --- a/packages/torrent-repository/src/entry/mod.rs +++ b/packages/torrent-repository/src/entry/mod.rs @@ -12,6 +12,7 @@ pub mod mutex_parking_lot; pub mod mutex_std; pub mod mutex_tokio; pub mod peer_list; +pub mod rw_lock_parking_lot; pub mod single; pub trait Entry { diff --git a/packages/torrent-repository/src/entry/mutex_parking_lot.rs b/packages/torrent-repository/src/entry/mutex_parking_lot.rs index ef0e958d5..4f3921ea7 100644 --- a/packages/torrent-repository/src/entry/mutex_parking_lot.rs +++ b/packages/torrent-repository/src/entry/mutex_parking_lot.rs @@ -6,44 +6,44 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use super::{Entry, EntrySync}; -use crate::{EntryRwLockParkingLot, EntrySingle}; +use crate::{EntryMutexParkingLot, EntrySingle}; -impl EntrySync for EntryRwLockParkingLot { +impl EntrySync for EntryMutexParkingLot { fn get_swarm_metadata(&self) -> SwarmMetadata { - self.read().get_swarm_metadata() + self.lock().get_swarm_metadata() } fn is_good(&self, policy: &TrackerPolicy) -> bool { - self.read().is_good(policy) + self.lock().is_good(policy) } fn peers_is_empty(&self) -> bool { - self.read().peers_is_empty() + self.lock().peers_is_empty() } fn get_peers_len(&self) -> usize { - self.read().get_peers_len() + self.lock().get_peers_len() } fn get_peers(&self, limit: Option) -> Vec> { - self.read().get_peers(limit) + self.lock().get_peers(limit) } fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { - self.read().get_peers_for_client(client, limit) + self.lock().get_peers_for_client(client, limit) } fn upsert_peer(&self, peer: &peer::Peer) -> bool { - self.write().upsert_peer(peer) + self.lock().upsert_peer(peer) } fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { - self.write().remove_inactive_peers(current_cutoff); + self.lock().remove_inactive_peers(current_cutoff); } } -impl From for EntryRwLockParkingLot { +impl From for EntryMutexParkingLot { fn from(entry: EntrySingle) -> Self { - Arc::new(parking_lot::RwLock::new(entry)) + Arc::new(parking_lot::Mutex::new(entry)) } } diff --git a/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs b/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs new file mode 100644 index 000000000..ef0e958d5 --- /dev/null +++ b/packages/torrent-repository/src/entry/rw_lock_parking_lot.rs @@ -0,0 +1,49 @@ +use std::net::SocketAddr; +use std::sync::Arc; + +use torrust_tracker_configuration::TrackerPolicy; +use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; + +use super::{Entry, EntrySync}; +use crate::{EntryRwLockParkingLot, EntrySingle}; + +impl EntrySync for EntryRwLockParkingLot { + fn get_swarm_metadata(&self) -> SwarmMetadata { + self.read().get_swarm_metadata() + } + + fn is_good(&self, policy: &TrackerPolicy) -> bool { + self.read().is_good(policy) + } + + fn peers_is_empty(&self) -> bool { + self.read().peers_is_empty() + } + + fn get_peers_len(&self) -> usize { + self.read().get_peers_len() + } + + fn get_peers(&self, limit: Option) -> Vec> { + self.read().get_peers(limit) + } + + fn get_peers_for_client(&self, client: &SocketAddr, limit: Option) -> Vec> { + self.read().get_peers_for_client(client, limit) + } + + fn upsert_peer(&self, peer: &peer::Peer) -> bool { + self.write().upsert_peer(peer) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + self.write().remove_inactive_peers(current_cutoff); + } +} + +impl From for EntryRwLockParkingLot { + fn from(entry: EntrySingle) -> Self { + Arc::new(parking_lot::RwLock::new(entry)) + } +} diff --git a/packages/torrent-repository/src/lib.rs b/packages/torrent-repository/src/lib.rs index 5d3a7ed45..a8955808e 100644 --- a/packages/torrent-repository/src/lib.rs +++ b/packages/torrent-repository/src/lib.rs @@ -14,6 +14,7 @@ pub mod repository; pub type EntrySingle = entry::Torrent; pub type EntryMutexStd = Arc>; pub type EntryMutexTokio = Arc>; +pub type EntryMutexParkingLot = Arc>; pub type EntryRwLockParkingLot = Arc>; // Repos @@ -26,6 +27,7 @@ pub type TorrentsRwLockTokioMutexStd = RwLockTokio; pub type TorrentsRwLockTokioMutexTokio = RwLockTokio; pub type TorrentsSkipMapMutexStd = CrossbeamSkipList; +pub type TorrentsSkipMapMutexParkingLot = CrossbeamSkipList; pub type TorrentsSkipMapRwLockParkingLot = CrossbeamSkipList; pub type TorrentsDashMapMutexStd = XacrimonDashMap; diff --git a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs index 0a2a566e7..9960b0c30 100644 --- a/packages/torrent-repository/src/repository/skip_map_mutex_std.rs +++ b/packages/torrent-repository/src/repository/skip_map_mutex_std.rs @@ -11,7 +11,7 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use super::Repository; use crate::entry::peer_list::PeerList; use crate::entry::{Entry, EntrySync}; -use crate::{EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; +use crate::{EntryMutexParkingLot, EntryMutexStd, EntryRwLockParkingLot, EntrySingle}; #[derive(Default, Debug)] pub struct CrossbeamSkipList { @@ -199,3 +199,94 @@ where } } } + +impl Repository for CrossbeamSkipList +where + EntryMutexParkingLot: EntrySync, + EntrySingle: Entry, +{ + fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { + let entry = self.torrents.get_or_insert(*info_hash, Arc::default()); + entry.value().upsert_peer(peer); + } + + fn get_swarm_metadata(&self, info_hash: &InfoHash) -> Option { + self.torrents.get(info_hash).map(|entry| entry.value().get_swarm_metadata()) + } + + fn get(&self, key: &InfoHash) -> Option { + let maybe_entry = self.torrents.get(key); + maybe_entry.map(|entry| entry.value().clone()) + } + + fn get_metrics(&self) -> TorrentsMetrics { + let mut metrics = TorrentsMetrics::default(); + + for entry in &self.torrents { + let stats = entry.value().lock().get_swarm_metadata(); + metrics.complete += u64::from(stats.complete); + metrics.downloaded += u64::from(stats.downloaded); + metrics.incomplete += u64::from(stats.incomplete); + metrics.torrents += 1; + } + + metrics + } + + fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexParkingLot)> { + match pagination { + Some(pagination) => self + .torrents + .iter() + .skip(pagination.offset as usize) + .take(pagination.limit as usize) + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + None => self + .torrents + .iter() + .map(|entry| (*entry.key(), entry.value().clone())) + .collect(), + } + } + + fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { + for (info_hash, completed) in persistent_torrents { + if self.torrents.contains_key(info_hash) { + continue; + } + + let entry = EntryMutexParkingLot::new( + EntrySingle { + swarm: PeerList::default(), + downloaded: *completed, + } + .into(), + ); + + // Since SkipMap is lock-free the torrent could have been inserted + // after checking if it exists. + self.torrents.get_or_insert(*info_hash, entry); + } + } + + fn remove(&self, key: &InfoHash) -> Option { + self.torrents.remove(key).map(|entry| entry.value().clone()) + } + + fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { + for entry in &self.torrents { + entry.value().remove_inactive_peers(current_cutoff); + } + } + + fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { + for entry in &self.torrents { + if entry.value().is_good(policy) { + continue; + } + + entry.remove(); + } + } +} diff --git a/packages/torrent-repository/tests/common/repo.rs b/packages/torrent-repository/tests/common/repo.rs index c5da6258d..f317d0d17 100644 --- a/packages/torrent-repository/tests/common/repo.rs +++ b/packages/torrent-repository/tests/common/repo.rs @@ -7,8 +7,8 @@ use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch, PersistentTorrent use torrust_tracker_torrent_repository::repository::{Repository as _, RepositoryAsync as _}; use torrust_tracker_torrent_repository::{ EntrySingle, TorrentsDashMapMutexStd, TorrentsRwLockStd, TorrentsRwLockStdMutexStd, TorrentsRwLockStdMutexTokio, - TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexStd, - TorrentsSkipMapRwLockParkingLot, + TorrentsRwLockTokio, TorrentsRwLockTokioMutexStd, TorrentsRwLockTokioMutexTokio, TorrentsSkipMapMutexParkingLot, + TorrentsSkipMapMutexStd, TorrentsSkipMapRwLockParkingLot, }; #[derive(Debug)] @@ -20,6 +20,7 @@ pub(crate) enum Repo { RwLockTokioMutexStd(TorrentsRwLockTokioMutexStd), RwLockTokioMutexTokio(TorrentsRwLockTokioMutexTokio), SkipMapMutexStd(TorrentsSkipMapMutexStd), + SkipMapMutexParkingLot(TorrentsSkipMapMutexParkingLot), SkipMapRwLockParkingLot(TorrentsSkipMapRwLockParkingLot), DashMapMutexStd(TorrentsDashMapMutexStd), } @@ -34,6 +35,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.upsert_peer(info_hash, peer).await, Repo::RwLockTokioMutexTokio(repo) => repo.upsert_peer(info_hash, peer).await, Repo::SkipMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), + Repo::SkipMapMutexParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::SkipMapRwLockParkingLot(repo) => repo.upsert_peer(info_hash, peer), Repo::DashMapMutexStd(repo) => repo.upsert_peer(info_hash, peer), } @@ -48,6 +50,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_swarm_metadata(info_hash).await, Repo::RwLockTokioMutexTokio(repo) => repo.get_swarm_metadata(info_hash).await, Repo::SkipMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), + Repo::SkipMapMutexParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::SkipMapRwLockParkingLot(repo) => repo.get_swarm_metadata(info_hash), Repo::DashMapMutexStd(repo) => repo.get_swarm_metadata(info_hash), } @@ -62,6 +65,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.get(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.get(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), + Repo::SkipMapMutexParkingLot(repo) => Some(repo.get(key)?.lock().clone()), Repo::SkipMapRwLockParkingLot(repo) => Some(repo.get(key)?.read().clone()), Repo::DashMapMutexStd(repo) => Some(repo.get(key)?.lock().unwrap().clone()), } @@ -76,6 +80,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.get_metrics().await, Repo::RwLockTokioMutexTokio(repo) => repo.get_metrics().await, Repo::SkipMapMutexStd(repo) => repo.get_metrics(), + Repo::SkipMapMutexParkingLot(repo) => repo.get_metrics(), Repo::SkipMapRwLockParkingLot(repo) => repo.get_metrics(), Repo::DashMapMutexStd(repo) => repo.get_metrics(), } @@ -117,6 +122,11 @@ impl Repo { .iter() .map(|(i, t)| (*i, t.lock().expect("it should get a lock").clone())) .collect(), + Repo::SkipMapMutexParkingLot(repo) => repo + .get_paginated(pagination) + .iter() + .map(|(i, t)| (*i, t.lock().clone())) + .collect(), Repo::SkipMapRwLockParkingLot(repo) => repo .get_paginated(pagination) .iter() @@ -139,6 +149,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.import_persistent(persistent_torrents).await, Repo::RwLockTokioMutexTokio(repo) => repo.import_persistent(persistent_torrents).await, Repo::SkipMapMutexStd(repo) => repo.import_persistent(persistent_torrents), + Repo::SkipMapMutexParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::SkipMapRwLockParkingLot(repo) => repo.import_persistent(persistent_torrents), Repo::DashMapMutexStd(repo) => repo.import_persistent(persistent_torrents), } @@ -153,6 +164,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => Some(repo.remove(key).await?.lock().unwrap().clone()), Repo::RwLockTokioMutexTokio(repo) => Some(repo.remove(key).await?.lock().await.clone()), Repo::SkipMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), + Repo::SkipMapMutexParkingLot(repo) => Some(repo.remove(key)?.lock().clone()), Repo::SkipMapRwLockParkingLot(repo) => Some(repo.remove(key)?.write().clone()), Repo::DashMapMutexStd(repo) => Some(repo.remove(key)?.lock().unwrap().clone()), } @@ -167,6 +179,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_inactive_peers(current_cutoff).await, Repo::SkipMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), + Repo::SkipMapMutexParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::SkipMapRwLockParkingLot(repo) => repo.remove_inactive_peers(current_cutoff), Repo::DashMapMutexStd(repo) => repo.remove_inactive_peers(current_cutoff), } @@ -181,6 +194,7 @@ impl Repo { Repo::RwLockTokioMutexStd(repo) => repo.remove_peerless_torrents(policy).await, Repo::RwLockTokioMutexTokio(repo) => repo.remove_peerless_torrents(policy).await, Repo::SkipMapMutexStd(repo) => repo.remove_peerless_torrents(policy), + Repo::SkipMapMutexParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::SkipMapRwLockParkingLot(repo) => repo.remove_peerless_torrents(policy), Repo::DashMapMutexStd(repo) => repo.remove_peerless_torrents(policy), } @@ -209,6 +223,9 @@ impl Repo { Repo::SkipMapMutexStd(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } + Repo::SkipMapMutexParkingLot(repo) => { + repo.torrents.insert(*info_hash, torrent.into()); + } Repo::SkipMapRwLockParkingLot(repo) => { repo.torrents.insert(*info_hash, torrent.into()); } diff --git a/packages/torrent-repository/tests/common/torrent.rs b/packages/torrent-repository/tests/common/torrent.rs index f672d14ef..abcf5525e 100644 --- a/packages/torrent-repository/tests/common/torrent.rs +++ b/packages/torrent-repository/tests/common/torrent.rs @@ -5,13 +5,16 @@ use torrust_tracker_configuration::TrackerPolicy; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_torrent_repository::entry::{Entry as _, EntryAsync as _, EntrySync as _}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; +use torrust_tracker_torrent_repository::{ + EntryMutexParkingLot, EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle, +}; #[derive(Debug, Clone)] pub(crate) enum Torrent { Single(EntrySingle), MutexStd(EntryMutexStd), MutexTokio(EntryMutexTokio), + MutexParkingLot(EntryMutexParkingLot), RwLockParkingLot(EntryRwLockParkingLot), } @@ -21,6 +24,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_swarm_metadata(), Torrent::MutexStd(entry) => entry.get_swarm_metadata(), Torrent::MutexTokio(entry) => entry.clone().get_swarm_metadata().await, + Torrent::MutexParkingLot(entry) => entry.clone().get_swarm_metadata(), Torrent::RwLockParkingLot(entry) => entry.clone().get_swarm_metadata(), } } @@ -30,6 +34,7 @@ impl Torrent { Torrent::Single(entry) => entry.is_good(policy), Torrent::MutexStd(entry) => entry.is_good(policy), Torrent::MutexTokio(entry) => entry.clone().check_good(policy).await, + Torrent::MutexParkingLot(entry) => entry.is_good(policy), Torrent::RwLockParkingLot(entry) => entry.is_good(policy), } } @@ -39,6 +44,7 @@ impl Torrent { Torrent::Single(entry) => entry.peers_is_empty(), Torrent::MutexStd(entry) => entry.peers_is_empty(), Torrent::MutexTokio(entry) => entry.clone().peers_is_empty().await, + Torrent::MutexParkingLot(entry) => entry.peers_is_empty(), Torrent::RwLockParkingLot(entry) => entry.peers_is_empty(), } } @@ -48,6 +54,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_len(), Torrent::MutexStd(entry) => entry.get_peers_len(), Torrent::MutexTokio(entry) => entry.clone().get_peers_len().await, + Torrent::MutexParkingLot(entry) => entry.get_peers_len(), Torrent::RwLockParkingLot(entry) => entry.get_peers_len(), } } @@ -57,6 +64,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers(limit), Torrent::MutexStd(entry) => entry.get_peers(limit), Torrent::MutexTokio(entry) => entry.clone().get_peers(limit).await, + Torrent::MutexParkingLot(entry) => entry.get_peers(limit), Torrent::RwLockParkingLot(entry) => entry.get_peers(limit), } } @@ -66,6 +74,7 @@ impl Torrent { Torrent::Single(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexStd(entry) => entry.get_peers_for_client(client, limit), Torrent::MutexTokio(entry) => entry.clone().get_peers_for_client(client, limit).await, + Torrent::MutexParkingLot(entry) => entry.get_peers_for_client(client, limit), Torrent::RwLockParkingLot(entry) => entry.get_peers_for_client(client, limit), } } @@ -75,6 +84,7 @@ impl Torrent { Torrent::Single(entry) => entry.upsert_peer(peer), Torrent::MutexStd(entry) => entry.upsert_peer(peer), Torrent::MutexTokio(entry) => entry.clone().upsert_peer(peer).await, + Torrent::MutexParkingLot(entry) => entry.upsert_peer(peer), Torrent::RwLockParkingLot(entry) => entry.upsert_peer(peer), } } @@ -84,6 +94,7 @@ impl Torrent { Torrent::Single(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexStd(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::MutexTokio(entry) => entry.clone().remove_inactive_peers(current_cutoff).await, + Torrent::MutexParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), Torrent::RwLockParkingLot(entry) => entry.remove_inactive_peers(current_cutoff), } } diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index aa3126000..3b9f3e3ad 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -9,7 +9,9 @@ use torrust_tracker_configuration::{TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::announce_event::AnnounceEvent; use torrust_tracker_primitives::peer::Peer; use torrust_tracker_primitives::{peer, NumberOfBytes}; -use torrust_tracker_torrent_repository::{EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle}; +use torrust_tracker_torrent_repository::{ + EntryMutexParkingLot, EntryMutexStd, EntryMutexTokio, EntryRwLockParkingLot, EntrySingle, +}; use crate::common::torrent::Torrent; use crate::common::torrent_peer_builder::{a_completed_peer, a_started_peer}; @@ -29,6 +31,11 @@ fn mutex_tokio() -> Torrent { Torrent::MutexTokio(EntryMutexTokio::default()) } +#[fixture] +fn mutex_parking_lot() -> Torrent { + Torrent::MutexParkingLot(EntryMutexParkingLot::default()) +} + #[fixture] fn rw_lock_parking_lot() -> Torrent { Torrent::RwLockParkingLot(EntryRwLockParkingLot::default()) @@ -104,7 +111,7 @@ async fn make(torrent: &mut Torrent, makes: &Makes) -> Vec { #[case::empty(&Makes::Empty)] #[tokio::test] async fn it_should_be_empty_by_default( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -120,7 +127,7 @@ async fn it_should_be_empty_by_default( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_check_if_entry_is_good( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, #[values(policy_none(), policy_persist(), policy_remove(), policy_remove_persist())] policy: TrackerPolicy, ) { @@ -158,7 +165,7 @@ async fn it_should_check_if_entry_is_good( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_for_torrent_entry( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -217,7 +224,7 @@ async fn it_should_update_a_peer(#[values(single(), mutex_std(), mutex_tokio())] #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_a_peer_upon_stopped_announcement( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { use torrust_tracker_primitives::peer::ReadInfo as _; @@ -258,7 +265,7 @@ async fn it_should_remove_a_peer_upon_stopped_announcement( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloaded_statistic( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -289,7 +296,7 @@ async fn it_should_handle_a_peer_completed_announcement_and_update_the_downloade #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_a_seeder( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -321,7 +328,7 @@ async fn it_should_update_a_peer_as_a_seeder( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_update_a_peer_as_incomplete( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { let peers = make(&mut torrent, makes).await; @@ -353,7 +360,7 @@ async fn it_should_update_a_peer_as_incomplete( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_get_peers_excluding_the_client_socket( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -385,7 +392,7 @@ async fn it_should_get_peers_excluding_the_client_socket( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_limit_the_number_of_peers_returned( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { make(&mut torrent, makes).await; @@ -410,7 +417,7 @@ async fn it_should_limit_the_number_of_peers_returned( #[case::three(&Makes::Three)] #[tokio::test] async fn it_should_remove_inactive_peers_beyond_cutoff( - #[values(single(), mutex_std(), mutex_tokio(), rw_lock_parking_lot())] mut torrent: Torrent, + #[values(single(), mutex_std(), mutex_tokio(), mutex_parking_lot(), rw_lock_parking_lot())] mut torrent: Torrent, #[case] makes: &Makes, ) { const TIMEOUT: Duration = Duration::from_secs(120); diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 23a99e5c1..53c19fcdb 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -53,6 +53,11 @@ fn skip_list_mutex_std() -> Repo { Repo::SkipMapMutexStd(CrossbeamSkipList::default()) } +#[fixture] +fn skip_list_mutex_parking_lot() -> Repo { + Repo::SkipMapMutexParkingLot(CrossbeamSkipList::default()) +} + #[fixture] fn skip_list_rw_lock_parking_lot() -> Repo { Repo::SkipMapRwLockParkingLot(CrossbeamSkipList::default()) @@ -252,6 +257,7 @@ async fn it_should_get_a_torrent_entry( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -286,6 +292,7 @@ async fn it_should_get_paginated_entries_in_a_stable_or_sorted_order( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot() )] repo: Repo, @@ -329,6 +336,7 @@ async fn it_should_get_paginated( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot() )] repo: Repo, @@ -387,6 +395,7 @@ async fn it_should_get_metrics( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -430,6 +439,7 @@ async fn it_should_import_persistent_torrents( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -470,6 +480,7 @@ async fn it_should_remove_an_entry( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -508,6 +519,7 @@ async fn it_should_remove_inactive_peers( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] @@ -607,6 +619,7 @@ async fn it_should_remove_peerless_torrents( tokio_mutex(), tokio_tokio(), skip_list_mutex_std(), + skip_list_mutex_parking_lot(), skip_list_rw_lock_parking_lot(), dash_map_std() )] From e32475e65578fc8a681c5da7aff1e058573381e7 Mon Sep 17 00:00:00 2001 From: ngthhu Date: Wed, 24 Apr 2024 23:00:27 +0700 Subject: [PATCH 141/217] remove unused dependencies --- Cargo.lock | 11 ----------- Cargo.toml | 1 - 2 files changed, 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1addf8bb0..a1795662f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -686,16 +686,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - [[package]] name = "compact_str" version = "0.7.1" @@ -3676,7 +3666,6 @@ dependencies = [ "axum-server", "chrono", "clap", - "colored", "config", "crossbeam-skiplist", "dashmap", diff --git a/Cargo.toml b/Cargo.toml index 94ff86419..fc26f3fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } -colored = "2" config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" From b072e103b7249c7a5b8d464dd5c14da75a5c43f5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 6 May 2024 08:03:56 +0100 Subject: [PATCH 142/217] chore(deps): update dependencies --- Cargo.lock | 67 ++++-------------------------------------------------- Cargo.toml | 4 ++-- 2 files changed, 6 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1795662f..1a66637e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,30 +140,14 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "aquatic_peer_id" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0732a73df221dcb25713849c6ebaf57b85355f669716652a7466f688cc06f25" -dependencies = [ - "compact_str", - "hex", - "quickcheck", - "regex", - "serde", - "zerocopy", -] - [[package]] name = "aquatic_udp_protocol" -version = "0.9.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0af90e5162f5fcbde33524128f08dc52a779f32512d5f8692eadd4b55c89389e" +checksum = "b2919b480121f7d20d247524da62bad1b6b7928bc3f50898f624b5c592727341" dependencies = [ - "aquatic_peer_id", "byteorder", "either", - "zerocopy", ] [[package]] @@ -539,15 +523,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "castaway" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.0.99" @@ -686,19 +661,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - [[package]] name = "config" version = "0.14.0" @@ -1030,16 +992,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "log", - "regex", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -2528,17 +2480,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "quickcheck" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" -dependencies = [ - "env_logger", - "log", - "rand", -] - [[package]] name = "quote" version = "1.0.36" @@ -2749,9 +2690,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.4.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c65e4c865bc3d2e3294493dff0acf7e6c259d066e34e22059fa9c39645c3636" +checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" dependencies = [ "crossbeam-utils", ] diff --git a/Cargo.toml b/Cargo.toml index fc26f3fdf..b823c2d6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "3.0.0-alpha.12-develop" [dependencies] anyhow = "1" -aquatic_udp_protocol = "0" +aquatic_udp_protocol = "0.8" async-trait = "0" axum = { version = "0", features = ["macros"] } axum-client-ip = "0" @@ -55,7 +55,7 @@ r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" reqwest = { version = "0", features = ["json"] } -ringbuf = "0" +ringbuf = "0.3.3" serde = { version = "1", features = ["derive"] } serde_bencode = "0" serde_bytes = "0" From bf0ea2b4a5282ff256ac7c5092cdb101d0c63ceb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 12:25:48 +0100 Subject: [PATCH 143/217] feat: log aborted UDP requests This will add a warning to the lofs when a UDP request is aborted. --- Cargo.lock | 67 ++++++++++++++++++-- Cargo.toml | 4 +- src/console/clients/checker/checks/health.rs | 12 +--- src/console/clients/checker/checks/http.rs | 11 ++-- src/console/clients/checker/checks/udp.rs | 15 ++--- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a66637e5..a1795662f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,14 +140,30 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "aquatic_peer_id" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0732a73df221dcb25713849c6ebaf57b85355f669716652a7466f688cc06f25" +dependencies = [ + "compact_str", + "hex", + "quickcheck", + "regex", + "serde", + "zerocopy", +] + [[package]] name = "aquatic_udp_protocol" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2919b480121f7d20d247524da62bad1b6b7928bc3f50898f624b5c592727341" +checksum = "0af90e5162f5fcbde33524128f08dc52a779f32512d5f8692eadd4b55c89389e" dependencies = [ + "aquatic_peer_id", "byteorder", "either", + "zerocopy", ] [[package]] @@ -523,6 +539,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.99" @@ -661,6 +686,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "config" version = "0.14.0" @@ -992,6 +1030,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2480,6 +2528,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand", +] + [[package]] name = "quote" version = "1.0.36" @@ -2690,9 +2749,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +checksum = "5c65e4c865bc3d2e3294493dff0acf7e6c259d066e34e22059fa9c39645c3636" dependencies = [ "crossbeam-utils", ] diff --git a/Cargo.toml b/Cargo.toml index b823c2d6f..fc26f3fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ version = "3.0.0-alpha.12-develop" [dependencies] anyhow = "1" -aquatic_udp_protocol = "0.8" +aquatic_udp_protocol = "0" async-trait = "0" axum = { version = "0", features = ["macros"] } axum-client-ip = "0" @@ -55,7 +55,7 @@ r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" reqwest = { version = "0", features = ["json"] } -ringbuf = "0.3.3" +ringbuf = "0" serde = { version = "1", features = ["derive"] } serde_bencode = "0" serde_bytes = "0" diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index def9ac015..a15b4133c 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; -use colored::Colorize; use hyper::StatusCode; use reqwest::{Client as HttpClient, Response}; use thiserror::Error; @@ -28,21 +27,14 @@ pub async fn run(health_checks: Vec, timeout: Duration, console: Console) - console.println("Health checks ..."); for url in health_checks { - let colored_url = url.to_string().yellow(); - match run_health_check(url.clone(), &timeout).await { Ok(response) => { - console.println(&format!( - "{} - Health API at {} is {}", - "✓".green(), - colored_url, - response.status() - )); + console.println(&format!("{} - Health API at {} is {}", "✓", url, response.status())); check_results.push(Ok(())); } Err(err) => { - console.eprintln(&format!("{} - Health API at {} is failing: {}", "✗".red(), colored_url, err)); + console.eprintln(&format!("{} - Health API at {} is failing: {}", "✗", url, err)); check_results.push(Err(CheckError::HealthCheckError { url, err })); } diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index b6bd98372..56c51a857 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,7 +1,6 @@ use std::str::FromStr as _; use std::time::Duration; -use colored::Colorize; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; @@ -18,26 +17,24 @@ pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) - console.println("HTTP trackers ..."); for ref url in http_trackers { - let colored_url = url.to_string().yellow(); - check_results.push(match check_http_announce(url, &timeout).await { Ok(_) => { - console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_url)); + console.println(&format!("{} - Announce at {} is OK", "✓", url)); Ok(()) } Err(err) => { - console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_url)); + console.println(&format!("{} - Announce at {} is failing", "✗", url)); Err(CheckError::HttpCheckError { url: url.clone(), err }) } }); check_results.push(match check_http_scrape(url, &timeout).await { Ok(_) => { - console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_url)); + console.println(&format!("{} - Scrape at {} is OK", "✓", url)); Ok(()) } Err(err) => { - console.println(&format!("{} - Scrape at {} is failing", "✗".red(), colored_url)); + console.println(&format!("{} - Scrape at {} is failing", "✗", url)); Err(CheckError::HttpCheckError { url: url.clone(), err }) } }); diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index 0db08020e..d89ef70af 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -2,7 +2,6 @@ use std::net::SocketAddr; use std::num::NonZeroU16; use std::time::Duration; -use colored::Colorize; use hex_literal::hex; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::debug; @@ -23,19 +22,17 @@ pub async fn run(udp_trackers: Vec, timeout: Duration, console: Cons for ref addr in udp_trackers { debug!("UDP tracker: {:?}", addr); - let colored_addr = addr.to_string().yellow(); - // Setup Connection let Ok((client, ctx)) = ({ let res = setup_connection(addr, &timeout).await; check_results.push(match res { Ok(_) => { - console.println(&format!("{} - Setup of {} is OK", "✓".green(), colored_addr)); + console.println(&format!("{} - Setup of {} is OK", "✓", addr)); Ok(()) } Err(ref e) => { - console.println(&format!("{} - Setup of {} is failing", "✗".red(), colored_addr)); + console.println(&format!("{} - Setup of {} is failing", "✗", addr)); Err(CheckError::UdpCheckError { addr: *addr, err: e.clone(), @@ -54,11 +51,11 @@ pub async fn run(udp_trackers: Vec, timeout: Duration, console: Cons check_results.push(match res { Ok(_) => { - console.println(&format!("{} - Announce of {} is OK", "✓".green(), colored_addr)); + console.println(&format!("{} - Announce of {} is OK", "✓", addr)); Ok(()) } Err(ref e) => { - console.println(&format!("{} - Announce of {} is failing", "✗".red(), colored_addr)); + console.println(&format!("{} - Announce of {} is failing", "✗", addr)); Err(CheckError::UdpCheckError { addr: *addr, err: e.clone(), @@ -79,11 +76,11 @@ pub async fn run(udp_trackers: Vec, timeout: Duration, console: Cons check_results.push(match res { Ok(_) => { - console.println(&format!("{} - Announce of {} is OK", "✓".green(), colored_addr)); + console.println(&format!("{} - Announce of {} is OK", "✓", addr)); Ok(()) } Err(ref e) => { - console.println(&format!("{} - Announce of {} is failing", "✗".red(), colored_addr)); + console.println(&format!("{} - Announce of {} is failing", "✗", addr)); Err(CheckError::UdpCheckError { addr: *addr, err: e.clone(), From 68f8e99293ba544bdb1c5d5111d530e72ebed65a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 7 May 2024 17:48:09 +0100 Subject: [PATCH 144/217] docs: udpate installation docs --- README.md | 4 ++++ src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ea5078b19..8431c00e4 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ The following services are provided by the default configuration: ## Documentation +You can read the [latest documentation][docs] from . + +Some specific sections: + - [Management API (Version 1)][API] - [Tracker (HTTP/TLS)][HTTP] - [Tracker (UDP)][UDP] diff --git a/src/lib.rs b/src/lib.rs index 064f50eb6..22bc133e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,29 +109,47 @@ //! With the default configuration you will need to create the `storage` directory: //! //! ```text -//! storage/ -//! ├── database -//! │   └── data.db -//! └── tls -//! ├── localhost.crt -//! └── localhost.key +//! ./storage/ +//! └── tracker +//! ├── etc +//! ├── lib +//! │   ├── database +//! │   │   └── sqlite3.db +//! │   └── tls +//! └── log //! ``` //! //! The default configuration expects a directory `./storage/tracker/lib/database` to be writable by the tracker process. //! -//! By default the tracker uses `SQLite` and the database file name `data.db`. +//! By default the tracker uses `SQLite` and the database file name `sqlite3.db`. //! //! You only need the `tls` directory in case you are setting up SSL for the HTTP tracker or the tracker API. //! Visit [`HTTP`](crate::servers::http) or [`API`](crate::servers::apis) if you want to know how you can use HTTPS. //! //! ## Install from sources //! +//! First, you need to create a folder to clone the repository. +//! +//! ```text +//! cd /tmp +//! mkdir torrust +//! ``` +//! //! ```text //! git clone https://github.com/torrust/torrust-tracker.git \ //! && cd torrust-tracker \ //! && cargo build --release \ +//! && mkdir -p ./storage/tracker/etc \ //! && mkdir -p ./storage/tracker/lib/database \ -//! && mkdir -p ./storage/tracker/lib/tls +//! && mkdir -p ./storage/tracker/lib/tls \ +//! && mkdir -p ./storage/tracker/log +//! ``` +//! +//! To run the tracker we will have to use the command "cargo run" this will +//! compile and after being compiled it will start running the tracker. +//! +//! ```text +//! cargo run //! ``` //! //! ## Run with docker @@ -141,9 +159,10 @@ //! //! # Configuration //! -//! In order to run the tracker you need to provide the configuration. If you run the tracker without providing the configuration, -//! the tracker will generate the default configuration the first time you run it. It will generate a `tracker.toml` file with -//! in the root directory. +//! In order to run the tracker you need to provide the configuration. If you +//! run the tracker without providing the configuration, the tracker will +//! generate the default configuration the first time you run it. It will +//! generate a `tracker.toml` file with in the root directory. //! //! The default configuration is: //! @@ -187,26 +206,37 @@ //! bind_address = "127.0.0.1:1313" //!``` //! -//! The default configuration includes one disabled UDP server, one disabled HTTP server and the enabled API. +//! The default configuration includes one disabled UDP server, one disabled +//! HTTP server and the enabled API. //! -//! For more information about each service and options you can visit the documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). +//! For more information about each service and options you can visit the +//! documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). //! -//! Alternatively to the `tracker.toml` file you can use one environment variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: +//! Alternatively to the `tracker.toml` file you can use one environment +//! variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: //! //! ```text -//! TORRUST_TRACKER_CONFIG=$(cat tracker.toml) -//! cargo run +//! TORRUST_TRACKER_CONFIG=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker //! ``` //! -//! In the previous example you are just setting the env var with the contents of the `tracker.toml` file. +//! In the previous example you are just setting the env var with the contents +//! of the `tracker.toml` file. +//! +//! The env var contains the same data as the `tracker.toml`. It's particularly +//! useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! +//! > NOTICE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. //! -//! The env var contains the same data as the `tracker.toml`. It's particularly useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! By default, if you don’t specify any `tracker.toml` file, the application +//! will use `./share/default/config/tracker.development.sqlite3.toml`. //! -//! > NOTE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. +//! > IMPORTANT: Every time you change the configuration you need to restart the +//! service. //! //! # Usage //! -//! Running the tracker with the default configuration and enabling the UDP and HTTP trackers will expose the services on these URLs: +//! Running the tracker with the default configuration and enabling the UDP and +//! HTTP trackers will expose the services on these URLs: //! //! - REST API: //! - UDP tracker: From cb96ba7dd5444ec7f7612753c7bd4dea02f1f5a0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 12:15:41 +0100 Subject: [PATCH 145/217] docs: fix profiling docs - Remove unnecessary file - Fix filename typo --- ... => flamegraph_generated_without_sudo.svg} | 0 docs/profiling.md | 2 +- flamegraph_generated_withput_sudo.svg | 491 ------------------ 3 files changed, 1 insertion(+), 492 deletions(-) rename docs/media/{flamegraph_generated_withput_sudo.svg => flamegraph_generated_without_sudo.svg} (100%) delete mode 100644 flamegraph_generated_withput_sudo.svg diff --git a/docs/media/flamegraph_generated_withput_sudo.svg b/docs/media/flamegraph_generated_without_sudo.svg similarity index 100% rename from docs/media/flamegraph_generated_withput_sudo.svg rename to docs/media/flamegraph_generated_without_sudo.svg diff --git a/docs/profiling.md b/docs/profiling.md index 26e5b786e..406560f3c 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -79,7 +79,7 @@ Loading configuration file: `./share/default/config/tracker.udp.benchmarking.tom And some bars in the graph will have the `unknown` label. -![flamegraph generated without sudo](./media/flamegraph_generated_withput_sudo.svg) +![flamegraph generated without sudo](./media/flamegraph_generated_without_sudo.svg) ## Using valgrind and kcachegrind diff --git a/flamegraph_generated_withput_sudo.svg b/flamegraph_generated_withput_sudo.svg deleted file mode 100644 index 84c00ffe3..000000000 --- a/flamegraph_generated_withput_sudo.svg +++ /dev/null @@ -1,491 +0,0 @@ -Flame Graph Reset ZoomSearch [unknown] (188 samples, 0.14%)[unknown] (187 samples, 0.14%)[unknown] (186 samples, 0.14%)[unknown] (178 samples, 0.14%)[unknown] (172 samples, 0.13%)[unknown] (158 samples, 0.12%)[unknown] (158 samples, 0.12%)[unknown] (125 samples, 0.10%)[unknown] (102 samples, 0.08%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (41 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (25 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (15 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)__GI___mmap64 (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)profiling (214 samples, 0.16%)clone3 (22 samples, 0.02%)start_thread (22 samples, 0.02%)std::sys::pal::unix::thread::Thread::new::thread_start (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::Handler::new (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::make_handler (20 samples, 0.02%)std::sys::pal::unix::stack_overflow::imp::get_stack (19 samples, 0.01%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (30 samples, 0.02%)[[vdso]] (93 samples, 0.07%)<torrust_tracker::shared::crypto::ephemeral_instance_keys::RANDOM_SEED as core::ops::deref::Deref>::deref::__stability::LAZY (143 samples, 0.11%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (31 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<BorrowType,K,V>::init_front (21 samples, 0.02%)[[vdso]] (91 samples, 0.07%)__GI___clock_gettime (14 samples, 0.01%)_int_malloc (53 samples, 0.04%)epoll_wait (254 samples, 0.19%)tokio::runtime::context::with_scheduler (28 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::context::with_scheduler::{{closure}} (14 samples, 0.01%)core::option::Option<T>::map (17 samples, 0.01%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (17 samples, 0.01%)mio::poll::Poll::poll (27 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select (27 samples, 0.02%)tokio::runtime::io::driver::Driver::turn (54 samples, 0.04%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (26 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (65 samples, 0.05%)core::sync::atomic::AtomicUsize::fetch_add (65 samples, 0.05%)core::sync::atomic::atomic_add (65 samples, 0.05%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (31 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark_condvar (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (49 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (33 samples, 0.03%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (93 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Parker::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Inner::park (75 samples, 0.06%)core::cell::RefCell<T>::borrow_mut (18 samples, 0.01%)core::cell::RefCell<T>::try_borrow_mut (18 samples, 0.01%)core::cell::BorrowRefMut::new (18 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (96 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (18 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (14 samples, 0.01%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (220 samples, 0.17%)<T as core::slice::cmp::SliceContains>::slice_contains (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (220 samples, 0.17%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (54 samples, 0.04%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (54 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (240 samples, 0.18%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (265 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park (284 samples, 0.22%)core::option::Option<T>::or_else (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (40 samples, 0.03%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (17 samples, 0.01%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (17 samples, 0.01%)core::num::<impl u32>::wrapping_add (17 samples, 0.01%)core::sync::atomic::AtomicU64::compare_exchange (26 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (129 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (128 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (119 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::pack (39 samples, 0.03%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run (613 samples, 0.47%)tokio::runtime::context::runtime::enter_runtime (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (613 samples, 0.47%)tokio::runtime::context::set_scheduler (613 samples, 0.47%)std::thread::local::LocalKey<T>::with (613 samples, 0.47%)std::thread::local::LocalKey<T>::try_with (613 samples, 0.47%)tokio::runtime::context::set_scheduler::{{closure}} (613 samples, 0.47%)tokio::runtime::context::scoped::Scoped<T>::set (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (613 samples, 0.47%)tokio::runtime::scheduler::multi_thread::worker::Context::run (613 samples, 0.47%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (777 samples, 0.59%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (776 samples, 0.59%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (16 samples, 0.01%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::runtime::context::set_current_task_id (16 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (16 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (20 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::poll (835 samples, 0.64%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (56 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage (46 samples, 0.04%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (897 samples, 0.68%)tokio::runtime::task::harness::poll_future::{{closure}} (897 samples, 0.68%)tokio::runtime::task::core::Core<T,S>::store_output (62 samples, 0.05%)tokio::runtime::task::harness::poll_future (930 samples, 0.71%)std::panic::catch_unwind (927 samples, 0.71%)std::panicking::try (927 samples, 0.71%)std::panicking::try::do_call (925 samples, 0.70%)core::mem::manually_drop::ManuallyDrop<T>::take (28 samples, 0.02%)core::ptr::read (28 samples, 0.02%)tokio::runtime::task::raw::poll (938 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll (934 samples, 0.71%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (934 samples, 0.71%)core::array::<impl core::default::Default for [T: 32]>::default (26 samples, 0.02%)tokio::runtime::time::Inner::lock (16 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::time::wheel::Wheel::poll (25 samples, 0.02%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (98 samples, 0.07%)tokio::runtime::time::Driver::park_internal (51 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<F as core::future::into_future::IntoFuture>::into_future (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (24 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (131 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (24 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (14 samples, 0.01%)core::sync::atomic::AtomicU32::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (39 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (34 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (32 samples, 0.02%)[[heap]] (2,361 samples, 1.80%)[..[[vdso]] (313 samples, 0.24%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (41 samples, 0.03%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (28 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)<alloc::string::String as core::fmt::Write>::write_str (67 samples, 0.05%)alloc::string::String::push_str (18 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (18 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (18 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (18 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (36 samples, 0.03%)core::num::<impl u64>::rotate_left (28 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (60 samples, 0.05%)core::num::<impl u64>::wrapping_add (14 samples, 0.01%)core::hash::sip::u8to64_le (60 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (184 samples, 0.14%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (15 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (19 samples, 0.01%)core::cell::Cell<T>::get (17 samples, 0.01%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (26 samples, 0.02%)core::ops::function::FnMut::call_mut (21 samples, 0.02%)tokio::runtime::coop::poll_proceed (21 samples, 0.02%)tokio::runtime::context::budget (21 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (21 samples, 0.02%)[unknown] (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (195 samples, 0.15%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (14 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (14 samples, 0.01%)core::result::Result<T,E>::is_err (18 samples, 0.01%)core::result::Result<T,E>::is_ok (18 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (46 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (39 samples, 0.03%)core::sync::atomic::AtomicU32::compare_exchange (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (19 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (245 samples, 0.19%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (26 samples, 0.02%)[[vdso]] (748 samples, 0.57%)[profiling] (34 samples, 0.03%)core::fmt::write (31 samples, 0.02%)__GI___clock_gettime (29 samples, 0.02%)__GI___libc_free (131 samples, 0.10%)arena_for_chunk (20 samples, 0.02%)arena_for_chunk (19 samples, 0.01%)heap_for_ptr (19 samples, 0.01%)heap_max_size (14 samples, 0.01%)__GI___libc_malloc (114 samples, 0.09%)__GI___libc_realloc (15 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)__GI___pthread_disable_asynccancel (66 samples, 0.05%)__GI_getsockname (249 samples, 0.19%)__libc_calloc (15 samples, 0.01%)__libc_recvfrom (23 samples, 0.02%)__libc_sendto (130 samples, 0.10%)__memcmp_evex_movbe (451 samples, 0.34%)__memcpy_avx512_unaligned_erms (426 samples, 0.32%)__memset_avx512_unaligned_erms (215 samples, 0.16%)__posix_memalign (17 samples, 0.01%)_int_free (418 samples, 0.32%)tcache_put (24 samples, 0.02%)_int_malloc (385 samples, 0.29%)_int_memalign (31 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (26 samples, 0.02%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (15 samples, 0.01%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (15 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::grow_one (15 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (96 samples, 0.07%)alloc::raw_vec::RawVec<T,A>::grow_amortized (66 samples, 0.05%)core::num::<impl usize>::checked_add (18 samples, 0.01%)core::num::<impl usize>::overflowing_add (18 samples, 0.01%)alloc::raw_vec::finish_grow (74 samples, 0.06%)alloc::sync::Arc<T,A>::drop_slow (16 samples, 0.01%)core::mem::drop (14 samples, 0.01%)core::fmt::Formatter::pad_integral (14 samples, 0.01%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (93 samples, 0.07%)core::ptr::drop_in_place<tokio::net::udp::UdpSocket::send_to<&core::net::socket_addr::SocketAddr>::{{closure}}> (23 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (188 samples, 0.14%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (30 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_connect::{{closure}}> (22 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_packet::{{closure}}> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}}> (19 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::send_response::{{closure}}> (22 samples, 0.02%)malloc_consolidate (24 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (15 samples, 0.01%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (17 samples, 0.01%)rand_chacha::guts::round (66 samples, 0.05%)rand_chacha::guts::refill_wide::impl_avx2 (99 samples, 0.08%)rand_chacha::guts::refill_wide::fn_impl (98 samples, 0.07%)rand_chacha::guts::refill_wide_impl (98 samples, 0.07%)std::io::error::Error::kind (14 samples, 0.01%)[unknown] (42 samples, 0.03%)[unknown] (14 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (490 samples, 0.37%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (211 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (84 samples, 0.06%)tokio::runtime::task::core::Header::get_owner_id (18 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (18 samples, 0.01%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (20 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::remove (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (31 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (29 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (108 samples, 0.08%)tokio::runtime::task::core::TaskIdGuard::enter (14 samples, 0.01%)tokio::runtime::context::set_current_task_id (14 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (14 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::complete (21 samples, 0.02%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (32 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (54 samples, 0.04%)tokio::runtime::task::raw::drop_abort_handle (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (17 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (22 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (22 samples, 0.02%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (79 samples, 0.06%)core::slice::<impl [T]>::contains (178 samples, 0.14%)<T as core::slice::cmp::SliceContains>::slice_contains (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (178 samples, 0.14%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (40 samples, 0.03%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (40 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (216 samples, 0.16%)tokio::loom::std::mutex::Mutex<T>::lock (16 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (219 samples, 0.17%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (29 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (29 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (54 samples, 0.04%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (18 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (113 samples, 0.09%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (41 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (31 samples, 0.02%)core::sync::atomic::AtomicU64::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (447 samples, 0.34%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (174 samples, 0.13%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (19 samples, 0.01%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (489 samples, 0.37%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (489 samples, 0.37%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run (484 samples, 0.37%)tokio::runtime::context::runtime::enter_runtime (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (484 samples, 0.37%)tokio::runtime::context::set_scheduler (484 samples, 0.37%)std::thread::local::LocalKey<T>::with (484 samples, 0.37%)std::thread::local::LocalKey<T>::try_with (484 samples, 0.37%)tokio::runtime::context::set_scheduler::{{closure}} (484 samples, 0.37%)tokio::runtime::context::scoped::Scoped<T>::set (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Context::run (484 samples, 0.37%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (24 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (20 samples, 0.02%)tokio::runtime::task::raw::poll (515 samples, 0.39%)tokio::runtime::task::harness::Harness<T,S>::poll (493 samples, 0.38%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (493 samples, 0.38%)tokio::runtime::task::harness::poll_future (493 samples, 0.38%)std::panic::catch_unwind (493 samples, 0.38%)std::panicking::try (493 samples, 0.38%)std::panicking::try::do_call (493 samples, 0.38%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (493 samples, 0.38%)tokio::runtime::task::harness::poll_future::{{closure}} (493 samples, 0.38%)tokio::runtime::task::core::Core<T,S>::poll (493 samples, 0.38%)tokio::runtime::time::wheel::Wheel::next_expiration (16 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (27 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (15 samples, 0.01%)torrust_tracker::core::Tracker::send_stats_event::{{closure}} (44 samples, 0.03%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (15 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (47 samples, 0.04%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::d_rounds (29 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (74 samples, 0.06%)torrust_tracker::servers::udp::peer_builder::from_request (17 samples, 0.01%)torrust_tracker::servers::udp::request::AnnounceWrapper::new (51 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (54 samples, 0.04%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (58 samples, 0.04%)torrust_tracker::core::Tracker::announce::{{closure}} (70 samples, 0.05%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (113 samples, 0.09%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (175 samples, 0.13%)<T as alloc::string::ToString>::to_string (38 samples, 0.03%)core::option::Option<T>::expect (56 samples, 0.04%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (18 samples, 0.01%)<T as alloc::string::ToString>::to_string (18 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (180 samples, 0.14%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (468 samples, 0.36%)torrust_tracker::servers::udp::logging::log_response (38 samples, 0.03%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (669 samples, 0.51%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (152 samples, 0.12%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (147 samples, 0.11%)tokio::net::udp::UdpSocket::send_to::{{closure}} (138 samples, 0.11%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (119 samples, 0.09%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (75 samples, 0.06%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to (39 samples, 0.03%)mio::io_source::IoSource<T>::do_io (39 samples, 0.03%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (39 samples, 0.03%)mio::net::udp::UdpSocket::send_to::{{closure}} (39 samples, 0.03%)std::net::udp::UdpSocket::send_to (39 samples, 0.03%)std::sys_common::net::UdpSocket::send_to (39 samples, 0.03%)std::sys::pal::unix::cvt (39 samples, 0.03%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (39 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_stats (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (14 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count::to_usize::{{closure}} (33 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats::{{closure}} (33 samples, 0.03%)torrust_tracker_primitives::peer::Peer::is_seeder (33 samples, 0.03%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::count (75 samples, 0.06%)core::iter::traits::iterator::Iterator::sum (75 samples, 0.06%)<usize as core::iter::traits::accum::Sum>::sum (75 samples, 0.06%)<core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::fold (75 samples, 0.06%)core::iter::traits::iterator::Iterator::fold (75 samples, 0.06%)core::iter::adapters::map::map_fold::{{closure}} (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (104 samples, 0.08%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (24 samples, 0.02%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (215 samples, 0.16%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (198 samples, 0.15%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (89 samples, 0.07%)core::option::Option<T>::is_some_and (32 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (30 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (30 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (26 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (34 samples, 0.03%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (58 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (58 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (58 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (58 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (238 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (236 samples, 0.18%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (208 samples, 0.16%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (208 samples, 0.16%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (282 samples, 0.21%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (67 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (22 samples, 0.02%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (22 samples, 0.02%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (22 samples, 0.02%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (22 samples, 0.02%)<u8 as core::slice::cmp::SliceOrd>::compare (22 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (18 samples, 0.01%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (23 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (43 samples, 0.03%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (43 samples, 0.03%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (43 samples, 0.03%)<u8 as core::slice::cmp::SliceOrd>::compare (43 samples, 0.03%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (151 samples, 0.12%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (145 samples, 0.11%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (137 samples, 0.10%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (137 samples, 0.10%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (266 samples, 0.20%)core::sync::atomic::AtomicU32::load (27 samples, 0.02%)core::sync::atomic::atomic_load (27 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (38 samples, 0.03%)std::sync::rwlock::RwLock<T>::read (37 samples, 0.03%)std::sys::sync::rwlock::futex::RwLock::read (36 samples, 0.03%)tracing::span::Span::log (16 samples, 0.01%)tracing::span::Span::record_all (70 samples, 0.05%)unlink_chunk (139 samples, 0.11%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (30 samples, 0.02%)rand::rng::Rng::gen (30 samples, 0.02%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (30 samples, 0.02%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (30 samples, 0.02%)rand_core::block::BlockRng<R>::generate_and_set (28 samples, 0.02%)[anon] (8,759 samples, 6.67%)[anon]uuid::v4::<impl uuid::Uuid>::new_v4 (32 samples, 0.02%)uuid::rng::bytes (32 samples, 0.02%)rand::random (32 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (15 samples, 0.01%)_int_free (338 samples, 0.26%)tcache_put (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (22 samples, 0.02%)hashbrown::raw::h2 (14 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (23 samples, 0.02%)hashbrown::raw::RawTableInner::find_or_find_insert_slot_inner (17 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (25 samples, 0.02%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (15 samples, 0.01%)[profiling] (545 samples, 0.42%)<alloc::collections::btree::map::Values<K,V> as core::iter::traits::iterator::Iterator>::next (32 samples, 0.02%)<alloc::collections::btree::map::Iter<K,V> as core::iter::traits::iterator::Iterator>::next (22 samples, 0.02%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Immut,K,V>::next_unchecked (16 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (30 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (28 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (83 samples, 0.06%)alloc::string::String::push_str (57 samples, 0.04%)alloc::vec::Vec<T,A>::extend_from_slice (57 samples, 0.04%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (57 samples, 0.04%)alloc::vec::Vec<T,A>::append_elements (57 samples, 0.04%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (20 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (41 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (151 samples, 0.12%)core::hash::sip::u8to64_le (50 samples, 0.04%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (33 samples, 0.03%)tokio::runtime::context::CONTEXT::__getit (35 samples, 0.03%)core::cell::Cell<T>::get (33 samples, 0.03%)[unknown] (20 samples, 0.02%)<tokio::future::poll_fn::PollFn<F> as core::future::future::Future>::poll (75 samples, 0.06%)core::ops::function::FnMut::call_mut (66 samples, 0.05%)tokio::runtime::coop::poll_proceed (66 samples, 0.05%)tokio::runtime::context::budget (66 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (66 samples, 0.05%)tokio::runtime::context::budget::{{closure}} (27 samples, 0.02%)tokio::runtime::coop::poll_proceed::{{closure}} (27 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (110 samples, 0.08%)[unknown] (15 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::io::scheduled_io::Waiters>> (27 samples, 0.02%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (27 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (70 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (55 samples, 0.04%)core::sync::atomic::atomic_compare_exchange (55 samples, 0.04%)[unknown] (33 samples, 0.03%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (214 samples, 0.16%)__memcpy_avx512_unaligned_erms (168 samples, 0.13%)[profiling] (171 samples, 0.13%)binascii::bin2hex (77 samples, 0.06%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (280 samples, 0.21%)[unknown] (317 samples, 0.24%)[[vdso]] (2,648 samples, 2.02%)[..[unknown] (669 samples, 0.51%)[unknown] (396 samples, 0.30%)[unknown] (251 samples, 0.19%)[unknown] (65 samples, 0.05%)[unknown] (30 samples, 0.02%)[unknown] (21 samples, 0.02%)__GI___clock_gettime (56 samples, 0.04%)arena_for_chunk (72 samples, 0.05%)arena_for_chunk (62 samples, 0.05%)heap_for_ptr (49 samples, 0.04%)heap_max_size (28 samples, 0.02%)__GI___libc_free (194 samples, 0.15%)arena_for_chunk (19 samples, 0.01%)checked_request2size (24 samples, 0.02%)__GI___libc_malloc (220 samples, 0.17%)tcache_get (44 samples, 0.03%)__GI___libc_write (25 samples, 0.02%)__GI___libc_write (14 samples, 0.01%)__GI___pthread_disable_asynccancel (97 samples, 0.07%)core::num::<impl u128>::leading_zeros (15 samples, 0.01%)compiler_builtins::float::conv::int_to_float::u128_to_f64_bits (72 samples, 0.05%)__floattidf (90 samples, 0.07%)compiler_builtins::float::conv::__floattidf (86 samples, 0.07%)exp_inline (40 samples, 0.03%)log_inline (64 samples, 0.05%)__ieee754_pow_fma (114 samples, 0.09%)__libc_calloc (106 samples, 0.08%)__libc_recvfrom (252 samples, 0.19%)__libc_sendto (133 samples, 0.10%)__memcmp_evex_movbe (137 samples, 0.10%)__memcpy_avx512_unaligned_erms (1,399 samples, 1.07%)__posix_memalign (172 samples, 0.13%)__posix_memalign (80 samples, 0.06%)_mid_memalign (71 samples, 0.05%)arena_for_chunk (14 samples, 0.01%)__pow (18 samples, 0.01%)__vdso_clock_gettime (40 samples, 0.03%)[unknown] (24 samples, 0.02%)_int_free (462 samples, 0.35%)tcache_put (54 samples, 0.04%)[unknown] (14 samples, 0.01%)_int_malloc (508 samples, 0.39%)_int_memalign (68 samples, 0.05%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (54 samples, 0.04%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (78 samples, 0.06%)alloc::raw_vec::RawVec<T,A>::grow_amortized (73 samples, 0.06%)alloc::raw_vec::finish_grow (91 samples, 0.07%)core::result::Result<T,E>::map_err (31 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Weak<ring::ec::curve25519::ed25519::signing::Ed25519KeyPair,&alloc::alloc::Global>> (16 samples, 0.01%)<alloc::sync::Weak<T,A> as core::ops::drop::Drop>::drop (16 samples, 0.01%)core::mem::drop (18 samples, 0.01%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)alloc_new_heap (49 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (49 samples, 0.04%)core::fmt::Formatter::pad_integral (40 samples, 0.03%)core::fmt::Formatter::pad_integral::write_prefix (19 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (155 samples, 0.12%)core::ptr::drop_in_place<core::option::Option<core::task::wake::Waker>> (71 samples, 0.05%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (245 samples, 0.19%)core::ptr::drop_in_place<torrust_tracker::servers::udp::handlers::handle_announce::{{closure}}> (33 samples, 0.03%)core::ptr::drop_in_place<torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}}> (37 samples, 0.03%)core::str::converts::from_utf8 (33 samples, 0.03%)core::str::validations::run_utf8_validation (20 samples, 0.02%)epoll_wait (31 samples, 0.02%)hashbrown::map::HashMap<K,V,S,A>::insert (17 samples, 0.01%)rand_chacha::guts::refill_wide (19 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (17 samples, 0.01%)std_detect::detect::check_for (17 samples, 0.01%)std_detect::detect::cache::test (17 samples, 0.01%)std_detect::detect::cache::Cache::test (17 samples, 0.01%)core::sync::atomic::AtomicUsize::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)std::sys::pal::unix::time::Timespec::new (29 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (132 samples, 0.10%)core::cmp::impls::<impl core::cmp::PartialOrd<&B> for &A>::ge (22 samples, 0.02%)core::cmp::PartialOrd::ge (22 samples, 0.02%)std::sys::pal::unix::time::Timespec::sub_timespec (67 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock_contended (18 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr (29 samples, 0.02%)std::sys_common::net::sockname (28 samples, 0.02%)syscall (552 samples, 0.42%)core::ptr::drop_in_place<core::cell::RefMut<core::option::Option<alloc::boxed::Box<tokio::runtime::scheduler::multi_thread::worker::Core>>>> (74 samples, 0.06%)core::ptr::drop_in_place<core::cell::BorrowRefMut> (74 samples, 0.06%)<core::cell::BorrowRefMut as core::ops::drop::Drop>::drop (74 samples, 0.06%)core::cell::Cell<T>::set (74 samples, 0.06%)core::cell::Cell<T>::replace (74 samples, 0.06%)core::mem::replace (74 samples, 0.06%)core::ptr::write (74 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::push_back_or_overflow (14 samples, 0.01%)tokio::runtime::context::with_scheduler (176 samples, 0.13%)std::thread::local::LocalKey<T>::try_with (152 samples, 0.12%)tokio::runtime::context::with_scheduler::{{closure}} (151 samples, 0.12%)tokio::runtime::context::scoped::Scoped<T>::with (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (150 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (71 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (16 samples, 0.01%)core::option::Option<T>::map (19 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (24 samples, 0.02%)mio::poll::Poll::poll (53 samples, 0.04%)mio::sys::unix::selector::epoll::Selector::select (53 samples, 0.04%)core::result::Result<T,E>::map (28 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (28 samples, 0.02%)tokio::io::ready::Ready::from_mio (14 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (126 samples, 0.10%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (18 samples, 0.01%)[unknown] (51 samples, 0.04%)[unknown] (100 samples, 0.08%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (326 samples, 0.25%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (205 samples, 0.16%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (77 samples, 0.06%)[unknown] (26 samples, 0.02%)<tokio::util::linked_list::DrainFilter<T,F> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (396 samples, 0.30%)tokio::loom::std::mutex::Mutex<T>::lock (18 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (573 samples, 0.44%)core::sync::atomic::AtomicUsize::fetch_add (566 samples, 0.43%)core::sync::atomic::atomic_add (566 samples, 0.43%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (635 samples, 0.48%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::next_remote_task (44 samples, 0.03%)tokio::runtime::scheduler::inject::shared::Shared<T>::is_empty (21 samples, 0.02%)tokio::runtime::scheduler::inject::shared::Shared<T>::len (21 samples, 0.02%)core::sync::atomic::AtomicUsize::load (21 samples, 0.02%)core::sync::atomic::atomic_load (21 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id (32 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with (32 samples, 0.02%)tokio::runtime::task::core::Header::get_owner_id::{{closure}} (32 samples, 0.02%)std::sync::poison::Flag::done (32 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>,tokio::runtime::task::core::Header>>> (43 samples, 0.03%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (43 samples, 0.03%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (123 samples, 0.09%)tokio::runtime::task::list::OwnedTasks<S>::remove (117 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (80 samples, 0.06%)tokio::runtime::scheduler::defer::Defer::wake (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (71 samples, 0.05%)std::sync::condvar::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait (56 samples, 0.04%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (56 samples, 0.04%)core::sync::atomic::AtomicUsize::compare_exchange (37 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (138 samples, 0.11%)tokio::runtime::driver::Driver::park (77 samples, 0.06%)tokio::runtime::driver::TimeDriver::park (77 samples, 0.06%)tokio::runtime::time::Driver::park (75 samples, 0.06%)tokio::runtime::scheduler::multi_thread::park::Parker::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::park::Inner::park (266 samples, 0.20%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (432 samples, 0.33%)tokio::runtime::scheduler::multi_thread::worker::Core::should_notify_others (26 samples, 0.02%)core::cell::RefCell<T>::borrow_mut (94 samples, 0.07%)core::cell::RefCell<T>::try_borrow_mut (94 samples, 0.07%)core::cell::BorrowRefMut::new (94 samples, 0.07%)tokio::runtime::coop::budget (142 samples, 0.11%)tokio::runtime::coop::with_budget (142 samples, 0.11%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (121 samples, 0.09%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (44 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (208 samples, 0.16%)tokio::runtime::signal::Driver::process (30 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (46 samples, 0.04%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (46 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (35 samples, 0.03%)tokio::runtime::task::core::Core<T,S>::set_stage (75 samples, 0.06%)core::sync::atomic::AtomicUsize::fetch_xor (76 samples, 0.06%)core::sync::atomic::atomic_xor (76 samples, 0.06%)tokio::runtime::task::state::State::transition_to_complete (79 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::complete (113 samples, 0.09%)tokio::runtime::task::state::State::transition_to_terminal (18 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (28 samples, 0.02%)core::mem::drop (18 samples, 0.01%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (18 samples, 0.01%)core::ptr::drop_in_place<tokio::util::sharded_list::ShardGuard<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>> (16 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::util::linked_list::LinkedList<tokio::runtime::task::Task<alloc::sync::Arc<tokio::runtime::scheduler::current_thread::Handle>>,tokio::runtime::task::core::Header>>> (16 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (16 samples, 0.01%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (53 samples, 0.04%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (21 samples, 0.02%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (113 samples, 0.09%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (15 samples, 0.01%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (15 samples, 0.01%)tokio::loom::std::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sync::mutex::Mutex<T>::lock (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::lock (14 samples, 0.01%)tokio::runtime::task::raw::drop_abort_handle (82 samples, 0.06%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (23 samples, 0.02%)tokio::runtime::task::state::State::ref_dec (23 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::task::raw::drop_join_handle_slow (34 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::drop_join_handle_slow (32 samples, 0.02%)tokio::runtime::task::state::State::unset_join_interested (23 samples, 0.02%)tokio::runtime::task::state::State::fetch_update (23 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (43 samples, 0.03%)core::num::<impl u32>::wrapping_add (23 samples, 0.02%)core::option::Option<T>::or_else (37 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (36 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (59 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (45 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (132 samples, 0.10%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (63 samples, 0.05%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run (290 samples, 0.22%)tokio::runtime::context::runtime::enter_runtime (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (290 samples, 0.22%)tokio::runtime::context::set_scheduler (290 samples, 0.22%)std::thread::local::LocalKey<T>::with (290 samples, 0.22%)std::thread::local::LocalKey<T>::try_with (290 samples, 0.22%)tokio::runtime::context::set_scheduler::{{closure}} (290 samples, 0.22%)tokio::runtime::context::scoped::Scoped<T>::set (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (290 samples, 0.22%)tokio::runtime::scheduler::multi_thread::worker::Context::run (290 samples, 0.22%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (327 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (322 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::poll (333 samples, 0.25%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (342 samples, 0.26%)tokio::runtime::task::harness::poll_future::{{closure}} (342 samples, 0.26%)tokio::runtime::task::harness::poll_future (348 samples, 0.27%)std::panic::catch_unwind (347 samples, 0.26%)std::panicking::try (347 samples, 0.26%)std::panicking::try::do_call (347 samples, 0.26%)core::sync::atomic::AtomicUsize::compare_exchange (18 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (18 samples, 0.01%)tokio::runtime::task::state::State::transition_to_running (47 samples, 0.04%)tokio::runtime::task::state::State::fetch_update_action (47 samples, 0.04%)tokio::runtime::task::state::State::transition_to_running::{{closure}} (19 samples, 0.01%)tokio::runtime::task::raw::poll (427 samples, 0.33%)tokio::runtime::task::harness::Harness<T,S>::poll (408 samples, 0.31%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (407 samples, 0.31%)tokio::runtime::task::state::State::transition_to_idle (17 samples, 0.01%)core::array::<impl core::default::Default for [T: 32]>::default (21 samples, 0.02%)tokio::runtime::time::wheel::Wheel::poll (14 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (72 samples, 0.05%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (23 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (15 samples, 0.01%)tokio::runtime::time::source::TimeSource::now (14 samples, 0.01%)tokio::runtime::time::Driver::park_internal (155 samples, 0.12%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (96 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (35 samples, 0.03%)core::num::<impl usize>::pow (35 samples, 0.03%)tokio::runtime::time::wheel::level::level_range (39 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (33 samples, 0.03%)core::num::<impl usize>::pow (33 samples, 0.03%)tokio::runtime::time::wheel::level::Level::next_expiration (208 samples, 0.16%)tokio::runtime::time::wheel::level::slot_range (48 samples, 0.04%)core::num::<impl usize>::pow (48 samples, 0.04%)tokio::runtime::time::wheel::Wheel::next_expiration (277 samples, 0.21%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::is_empty (18 samples, 0.01%)core::option::Option<T>::is_some (18 samples, 0.01%)torrust_tracker::core::Tracker::authorize::{{closure}} (50 samples, 0.04%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (37 samples, 0.03%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (19 samples, 0.01%)core::iter::traits::iterator::Iterator::collect (17 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (17 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (17 samples, 0.01%)<std::hash::random::DefaultHasher as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::finish (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::finish (20 samples, 0.02%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (62 samples, 0.05%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (40 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (27 samples, 0.02%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (17 samples, 0.01%)torrust_tracker::servers::udp::peer_builder::from_request (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (19 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (355 samples, 0.27%)<F as core::future::into_future::IntoFuture>::into_future (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (37 samples, 0.03%)core::sync::atomic::AtomicUsize::fetch_add (25 samples, 0.02%)core::sync::atomic::atomic_add (25 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet (14 samples, 0.01%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (20 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::result::Result<T,E>::map_err (16 samples, 0.01%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (136 samples, 0.10%)torrust_tracker::core::Tracker::announce::{{closure}} (173 samples, 0.13%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (267 samples, 0.20%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (30 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (423 samples, 0.32%)core::fmt::Formatter::new (26 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (80 samples, 0.06%)core::fmt::num::imp::fmt_u64 (58 samples, 0.04%)core::intrinsics::copy_nonoverlapping (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (74 samples, 0.06%)core::fmt::num::imp::fmt_u64 (70 samples, 0.05%)<T as alloc::string::ToString>::to_string (207 samples, 0.16%)core::option::Option<T>::expect (19 samples, 0.01%)core::ptr::drop_in_place<alloc::string::String> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (18 samples, 0.01%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (18 samples, 0.01%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (18 samples, 0.01%)torrust_tracker::servers::udp::logging::map_action_name (25 samples, 0.02%)alloc::str::<impl alloc::borrow::ToOwned for str>::to_owned (14 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request (345 samples, 0.26%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (18 samples, 0.01%)core::fmt::num::imp::fmt_u64 (14 samples, 0.01%)<T as alloc::string::ToString>::to_string (35 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (1,067 samples, 0.81%)torrust_tracker::servers::udp::logging::log_response (72 samples, 0.05%)alloc::vec::from_elem (68 samples, 0.05%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (68 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (68 samples, 0.05%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (68 samples, 0.05%)alloc::alloc::Global::alloc_impl (68 samples, 0.05%)alloc::alloc::alloc_zeroed (68 samples, 0.05%)__rdl_alloc_zeroed (68 samples, 0.05%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (68 samples, 0.05%)[unknown] (48 samples, 0.04%)[unknown] (16 samples, 0.01%)[unknown] (28 samples, 0.02%)std::sys::pal::unix::cvt (134 samples, 0.10%)<isize as std::sys::pal::unix::IsMinusOne>::is_minus_one (134 samples, 0.10%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (1,908 samples, 1.45%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (504 samples, 0.38%)torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (382 samples, 0.29%)tokio::net::udp::UdpSocket::send_to::{{closure}} (344 samples, 0.26%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (332 samples, 0.25%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (304 samples, 0.23%)tokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (215 samples, 0.16%)mio::net::udp::UdpSocket::send_to (185 samples, 0.14%)mio::io_source::IoSource<T>::do_io (185 samples, 0.14%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (185 samples, 0.14%)mio::net::udp::UdpSocket::send_to::{{closure}} (185 samples, 0.14%)std::net::udp::UdpSocket::send_to (185 samples, 0.14%)std::sys_common::net::UdpSocket::send_to (169 samples, 0.13%)alloc::vec::Vec<T>::with_capacity (17 samples, 0.01%)alloc::vec::Vec<T,A>::with_capacity_in (17 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (104 samples, 0.08%)tokio::net::udp::UdpSocket::ready::{{closure}} (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (190 samples, 0.14%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (49 samples, 0.04%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (28 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (330 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (327 samples, 0.25%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (92 samples, 0.07%)tokio::task::spawn::spawn (92 samples, 0.07%)tokio::task::spawn::spawn_inner (92 samples, 0.07%)tokio::runtime::context::current::with_current (92 samples, 0.07%)std::thread::local::LocalKey<T>::try_with (92 samples, 0.07%)tokio::runtime::context::current::with_current::{{closure}} (92 samples, 0.07%)core::option::Option<T>::map (92 samples, 0.07%)tokio::task::spawn::spawn_inner::{{closure}} (92 samples, 0.07%)tokio::runtime::scheduler::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (92 samples, 0.07%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (92 samples, 0.07%)tokio::runtime::task::list::OwnedTasks<S>::bind (90 samples, 0.07%)tokio::runtime::task::new_task (89 samples, 0.07%)tokio::runtime::task::raw::RawTask::new (89 samples, 0.07%)tokio::runtime::task::core::Cell<T,S>::new (89 samples, 0.07%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (34 samples, 0.03%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (27 samples, 0.02%)alloc::sync::Arc<T>::new (21 samples, 0.02%)alloc::boxed::Box<T>::new (21 samples, 0.02%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (152 samples, 0.12%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (125 samples, 0.10%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (88 samples, 0.07%)core::option::Option<T>::is_some_and (18 samples, 0.01%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (17 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (17 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (22 samples, 0.02%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (17 samples, 0.01%)std::sync::rwlock::RwLock<T>::read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::read (16 samples, 0.01%)tracing::span::Span::log (26 samples, 0.02%)core::fmt::Arguments::new_v1 (15 samples, 0.01%)tracing_core::span::Record::is_empty (34 samples, 0.03%)tracing_core::field::ValueSet::is_empty (34 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::all (22 samples, 0.02%)tracing_core::field::ValueSet::is_empty::{{closure}} (18 samples, 0.01%)core::option::Option<T>::is_none (16 samples, 0.01%)core::option::Option<T>::is_some (16 samples, 0.01%)tracing::span::Span::record_all (143 samples, 0.11%)unlink_chunk (185 samples, 0.14%)uuid::builder::Builder::with_variant (48 samples, 0.04%)[unknown] (40 samples, 0.03%)uuid::builder::Builder::from_random_bytes (77 samples, 0.06%)uuid::builder::Builder::with_version (29 samples, 0.02%)[unknown] (24 samples, 0.02%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (161 samples, 0.12%)[unknown] (92 samples, 0.07%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (162 samples, 0.12%)rand::rng::Rng::gen (162 samples, 0.12%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (162 samples, 0.12%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (162 samples, 0.12%)[unknown] (18,233 samples, 13.89%)[unknown]uuid::v4::<impl uuid::Uuid>::new_v4 (270 samples, 0.21%)uuid::rng::bytes (190 samples, 0.14%)rand::random (190 samples, 0.14%)__memcpy_avx512_unaligned_erms (69 samples, 0.05%)_int_free (23 samples, 0.02%)_int_malloc (23 samples, 0.02%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)advise_stack_range (31 samples, 0.02%)__GI_madvise (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (28 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sys::pal::unix::futex::futex_wait (31 samples, 0.02%)syscall (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (31 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (30 samples, 0.02%)[unknown] (29 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (17 samples, 0.01%)std::sync::condvar::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_timeout (35 samples, 0.03%)std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (35 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (56 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (56 samples, 0.04%)std::sys::pal::unix::futex::futex_wait (56 samples, 0.04%)syscall (56 samples, 0.04%)[unknown] (56 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (55 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (54 samples, 0.04%)[unknown] (53 samples, 0.04%)[unknown] (52 samples, 0.04%)[unknown] (46 samples, 0.04%)[unknown] (39 samples, 0.03%)[unknown] (38 samples, 0.03%)[unknown] (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[[vdso]] (26 samples, 0.02%)[[vdso]] (263 samples, 0.20%)__ieee754_pow_fma (26 samples, 0.02%)__pow (314 samples, 0.24%)std::f64::<impl f64>::powf (345 samples, 0.26%)__GI___clock_gettime (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::end_processing_scheduled_tasks (416 samples, 0.32%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Timespec::now (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_processing_scheduled_tasks (24 samples, 0.02%)std::time::Instant::now (18 samples, 0.01%)std::sys::pal::unix::time::Instant::now (18 samples, 0.01%)mio::poll::Poll::poll (102 samples, 0.08%)mio::sys::unix::selector::epoll::Selector::select (102 samples, 0.08%)epoll_wait (99 samples, 0.08%)[unknown] (92 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (91 samples, 0.07%)[unknown] (88 samples, 0.07%)[unknown] (85 samples, 0.06%)[unknown] (84 samples, 0.06%)[unknown] (43 samples, 0.03%)[unknown] (29 samples, 0.02%)[unknown] (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (125 samples, 0.10%)tokio::runtime::scheduler::multi_thread::park::Parker::park_timeout (125 samples, 0.10%)tokio::runtime::driver::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::driver::TimeDriver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_timeout (125 samples, 0.10%)tokio::runtime::time::Driver::park_internal (116 samples, 0.09%)tokio::runtime::io::driver::Driver::turn (116 samples, 0.09%)tokio::runtime::scheduler::multi_thread::worker::Context::maintenance (148 samples, 0.11%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (111 samples, 0.08%)alloc::sync::Arc<T,A>::inner (111 samples, 0.08%)core::ptr::non_null::NonNull<T>::as_ref (111 samples, 0.08%)core::sync::atomic::AtomicUsize::compare_exchange (16 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (16 samples, 0.01%)core::bool::<impl bool>::then (88 samples, 0.07%)std::sys::pal::unix::futex::futex_wait (13,339 samples, 10.16%)std::sys::pal::..syscall (13,003 samples, 9.90%)syscall[unknown] (12,895 samples, 9.82%)[unknown][unknown] (12,759 samples, 9.72%)[unknown][unknown] (12,313 samples, 9.38%)[unknown][unknown] (12,032 samples, 9.16%)[unknown][unknown] (11,734 samples, 8.94%)[unknown][unknown] (11,209 samples, 8.54%)[unknown][unknown] (10,265 samples, 7.82%)[unknown][unknown] (9,345 samples, 7.12%)[unknown][unknown] (8,623 samples, 6.57%)[unknown][unknown] (7,744 samples, 5.90%)[unknow..[unknown] (5,922 samples, 4.51%)[unkn..[unknown] (4,459 samples, 3.40%)[un..[unknown] (2,808 samples, 2.14%)[..[unknown] (1,275 samples, 0.97%)[unknown] (1,022 samples, 0.78%)[unknown] (738 samples, 0.56%)[unknown] (607 samples, 0.46%)[unknown] (155 samples, 0.12%)core::result::Result<T,E>::is_err (77 samples, 0.06%)core::result::Result<T,E>::is_ok (77 samples, 0.06%)std::sync::condvar::Condvar::wait (13,429 samples, 10.23%)std::sync::cond..std::sys::sync::condvar::futex::Condvar::wait (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::condvar::futex::Condvar::wait_optional_timeout (13,428 samples, 10.23%)std::sys::sync:..std::sys::sync::mutex::futex::Mutex::lock (89 samples, 0.07%)tokio::runtime::scheduler::multi_thread::park::Inner::park_condvar (13,508 samples, 10.29%)tokio::runtime:..tokio::loom::std::mutex::Mutex<T>::lock (64 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (31 samples, 0.02%)core::sync::atomic::AtomicU32::compare_exchange (30 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (30 samples, 0.02%)core::sync::atomic::AtomicUsize::compare_exchange (15 samples, 0.01%)core::sync::atomic::atomic_compare_exchange (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (38 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Parker::park (34 samples, 0.03%)tokio::runtime::scheduler::multi_thread::park::Inner::park (34 samples, 0.03%)core::array::<impl core::default::Default for [T: 32]>::default (17 samples, 0.01%)core::ptr::drop_in_place<[core::option::Option<core::task::wake::Waker>: 32]> (19 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_occupied_slot (33 samples, 0.03%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::level_range (17 samples, 0.01%)tokio::runtime::time::wheel::level::slot_range (15 samples, 0.01%)core::num::<impl usize>::pow (15 samples, 0.01%)tokio::runtime::time::wheel::level::Level::next_expiration (95 samples, 0.07%)tokio::runtime::time::wheel::level::slot_range (41 samples, 0.03%)core::num::<impl usize>::pow (41 samples, 0.03%)tokio::runtime::time::wheel::Wheel::next_expiration (129 samples, 0.10%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process_at_time (202 samples, 0.15%)tokio::runtime::time::wheel::Wheel::poll_at (17 samples, 0.01%)tokio::runtime::time::wheel::Wheel::next_expiration (15 samples, 0.01%)<mio::event::events::Iter as core::iter::traits::iterator::Iterator>::next (38 samples, 0.03%)core::option::Option<T>::map (38 samples, 0.03%)core::result::Result<T,E>::map (31 samples, 0.02%)mio::sys::unix::selector::epoll::Selector::select::{{closure}} (31 samples, 0.02%)alloc::vec::Vec<T,A>::set_len (17 samples, 0.01%)[[vdso]] (28 samples, 0.02%)[unknown] (11,031 samples, 8.40%)[unknown][unknown] (10,941 samples, 8.33%)[unknown][unknown] (10,850 samples, 8.26%)[unknown][unknown] (10,691 samples, 8.14%)[unknown][unknown] (10,070 samples, 7.67%)[unknown][unknown] (9,737 samples, 7.42%)[unknown][unknown] (7,659 samples, 5.83%)[unknow..[unknown] (6,530 samples, 4.97%)[unkno..[unknown] (5,633 samples, 4.29%)[unkn..[unknown] (5,055 samples, 3.85%)[unk..[unknown] (4,046 samples, 3.08%)[un..[unknown] (2,911 samples, 2.22%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,226 samples, 0.93%)[unknown] (455 samples, 0.35%)[unknown] (408 samples, 0.31%)[unknown] (249 samples, 0.19%)[unknown] (202 samples, 0.15%)[unknown] (100 samples, 0.08%)mio::poll::Poll::poll (11,328 samples, 8.63%)mio::poll::P..mio::sys::unix::selector::epoll::Selector::select (11,328 samples, 8.63%)mio::sys::un..epoll_wait (11,229 samples, 8.55%)epoll_wait__GI___pthread_disable_asynccancel (50 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (47 samples, 0.04%)tokio::util::bit::Pack::pack (38 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (25 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (23 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (19 samples, 0.01%)tokio::runtime::io::driver::Driver::turn (11,595 samples, 8.83%)tokio::runti..tokio::runtime::io::scheduled_io::ScheduledIo::wake (175 samples, 0.13%)__GI___clock_gettime (15 samples, 0.01%)std::sys::pal::unix::time::Timespec::now (18 samples, 0.01%)tokio::runtime::time::<impl tokio::runtime::time::handle::Handle>::process (26 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (26 samples, 0.02%)tokio::time::clock::Clock::now (20 samples, 0.02%)tokio::time::clock::now (20 samples, 0.02%)std::time::Instant::now (20 samples, 0.02%)std::sys::pal::unix::time::Instant::now (20 samples, 0.02%)tokio::runtime::time::source::TimeSource::now (17 samples, 0.01%)tokio::runtime::time::Driver::park_internal (11,686 samples, 8.90%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Inner::park_driver (11,957 samples, 9.11%)tokio::runtim..tokio::runtime::driver::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::driver::TimeDriver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::time::Driver::park (11,950 samples, 9.10%)tokio::runtim..tokio::runtime::scheduler::multi_thread::park::Parker::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::park::Inner::park (25,502 samples, 19.42%)tokio::runtime::scheduler::mul..tokio::runtime::scheduler::multi_thread::worker::Context::park_timeout (25,547 samples, 19.46%)tokio::runtime::scheduler::mul..core::result::Result<T,E>::is_err (14 samples, 0.01%)core::result::Result<T,E>::is_ok (14 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (45 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (45 samples, 0.03%)tokio::loom::std::mutex::Mutex<T>::lock (84 samples, 0.06%)std::sync::mutex::Mutex<T>::lock (81 samples, 0.06%)std::sys::sync::mutex::futex::Mutex::lock (73 samples, 0.06%)tokio::runtime::scheduler::multi_thread::worker::Core::maintenance (122 samples, 0.09%)<T as core::slice::cmp::SliceContains>::slice_contains::{{closure}} (90 samples, 0.07%)core::cmp::impls::<impl core::cmp::PartialEq for usize>::eq (90 samples, 0.07%)core::slice::<impl [T]>::contains (241 samples, 0.18%)<T as core::slice::cmp::SliceContains>::slice_contains (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::any (241 samples, 0.18%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (75 samples, 0.06%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (75 samples, 0.06%)core::sync::atomic::AtomicU32::compare_exchange (20 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::is_parked (283 samples, 0.22%)tokio::loom::std::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sync::mutex::Mutex<T>::lock (32 samples, 0.02%)std::sys::sync::mutex::futex::Mutex::lock (24 samples, 0.02%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (33 samples, 0.03%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (33 samples, 0.03%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::idle::Idle::unpark_worker_by_id (98 samples, 0.07%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (401 samples, 0.31%)alloc::vec::Vec<T,A>::push (14 samples, 0.01%)core::ptr::drop_in_place<std::sync::mutex::MutexGuard<tokio::runtime::scheduler::multi_thread::worker::Synced>> (15 samples, 0.01%)<std::sync::mutex::MutexGuard<T> as core::ops::drop::Drop>::drop (15 samples, 0.01%)std::sys::sync::mutex::futex::Mutex::unlock (14 samples, 0.01%)core::result::Result<T,E>::is_err (15 samples, 0.01%)core::result::Result<T,E>::is_ok (15 samples, 0.01%)core::sync::atomic::AtomicU32::compare_exchange (22 samples, 0.02%)core::sync::atomic::atomic_compare_exchange (22 samples, 0.02%)tokio::loom::std::mutex::Mutex<T>::lock (63 samples, 0.05%)std::sync::mutex::Mutex<T>::lock (62 samples, 0.05%)std::sys::sync::mutex::futex::Mutex::lock (59 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock_contended (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_parked (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::idle::State::dec_num_unparked (14 samples, 0.01%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (21 samples, 0.02%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (21 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (17 samples, 0.01%)alloc::sync::Arc<T,A>::inner (17 samples, 0.01%)core::ptr::non_null::NonNull<T>::as_ref (17 samples, 0.01%)core::sync::atomic::AtomicU32::load (17 samples, 0.01%)core::sync::atomic::atomic_load (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::is_empty (68 samples, 0.05%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::is_empty (51 samples, 0.04%)tokio::runtime::scheduler::multi_thread::queue::Inner<T>::len (33 samples, 0.03%)core::sync::atomic::AtomicU64::load (16 samples, 0.01%)core::sync::atomic::atomic_load (16 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_if_work_pending (106 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::park (26,672 samples, 20.31%)tokio::runtime::scheduler::multi..tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_parked (272 samples, 0.21%)tokio::runtime::scheduler::multi_thread::worker::Core::has_tasks (33 samples, 0.03%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::has_tasks (24 samples, 0.02%)tokio::runtime::context::budget (18 samples, 0.01%)std::thread::local::LocalKey<T>::try_with (18 samples, 0.01%)syscall (61 samples, 0.05%)__memcpy_avx512_unaligned_erms (172 samples, 0.13%)__memcpy_avx512_unaligned_erms (224 samples, 0.17%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (228 samples, 0.17%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (228 samples, 0.17%)std::panic::catch_unwind (415 samples, 0.32%)std::panicking::try (415 samples, 0.32%)std::panicking::try::do_call (415 samples, 0.32%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (415 samples, 0.32%)core::ops::function::FnOnce::call_once (415 samples, 0.32%)tokio::runtime::task::harness::Harness<T,S>::complete::{{closure}} (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (415 samples, 0.32%)tokio::runtime::task::core::Core<T,S>::set_stage (410 samples, 0.31%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (27 samples, 0.02%)core::result::Result<T,E>::is_err (43 samples, 0.03%)core::result::Result<T,E>::is_ok (43 samples, 0.03%)tokio::runtime::task::harness::Harness<T,S>::complete (570 samples, 0.43%)tokio::runtime::task::harness::Harness<T,S>::release (155 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::task::Schedule for alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>::release (152 samples, 0.12%)tokio::runtime::task::list::OwnedTasks<S>::remove (152 samples, 0.12%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::remove (103 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (65 samples, 0.05%)tokio::loom::std::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (58 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (54 samples, 0.04%)std::io::stdio::stderr::INSTANCE (17 samples, 0.01%)tokio::runtime::coop::budget (26 samples, 0.02%)tokio::runtime::coop::with_budget (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (35 samples, 0.03%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (70 samples, 0.05%)__memcpy_avx512_unaligned_erms (42 samples, 0.03%)core::cmp::Ord::min (22 samples, 0.02%)core::cmp::min_by (22 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (27 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (30 samples, 0.02%)std::io::cursor::Cursor<T>::remaining_slice (24 samples, 0.02%)core::slice::index::<impl core::ops::index::Index<I> for [T]>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::index (19 samples, 0.01%)<core::ops::range::RangeFrom<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<core::ops::range::Range<usize> as core::slice::index::SliceIndex<[T]>>::get_unchecked (19 samples, 0.01%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (44 samples, 0.03%)std::io::impls::<impl std::io::Read for &[u8]>::read_exact (20 samples, 0.02%)byteorder::io::ReadBytesExt::read_i32 (46 samples, 0.04%)core::cmp::Ord::min (14 samples, 0.01%)core::cmp::min_by (14 samples, 0.01%)std::io::cursor::Cursor<T>::remaining_slice (19 samples, 0.01%)byteorder::io::ReadBytesExt::read_i64 (24 samples, 0.02%)<std::io::cursor::Cursor<T> as std::io::Read>::read_exact (24 samples, 0.02%)aquatic_udp_protocol::request::Request::from_bytes (349 samples, 0.27%)__GI___lll_lock_wake_private (148 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (137 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (111 samples, 0.08%)[unknown] (98 samples, 0.07%)[unknown] (42 samples, 0.03%)[unknown] (30 samples, 0.02%)__GI___lll_lock_wait_private (553 samples, 0.42%)futex_wait (541 samples, 0.41%)[unknown] (536 samples, 0.41%)[unknown] (531 samples, 0.40%)[unknown] (524 samples, 0.40%)[unknown] (515 samples, 0.39%)[unknown] (498 samples, 0.38%)[unknown] (470 samples, 0.36%)[unknown] (435 samples, 0.33%)[unknown] (350 samples, 0.27%)[unknown] (327 samples, 0.25%)[unknown] (290 samples, 0.22%)[unknown] (222 samples, 0.17%)[unknown] (160 samples, 0.12%)[unknown] (104 samples, 0.08%)[unknown] (33 samples, 0.03%)[unknown] (25 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (703 samples, 0.54%)__GI___libc_free (866 samples, 0.66%)tracing::span::Span::record_all (30 samples, 0.02%)unlink_chunk (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::servers::udp::UdpRequest> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (899 samples, 0.68%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (899 samples, 0.68%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (899 samples, 0.68%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (899 samples, 0.68%)alloc::alloc::dealloc (899 samples, 0.68%)__rdl_dealloc (899 samples, 0.68%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (899 samples, 0.68%)core::result::Result<T,E>::expect (91 samples, 0.07%)core::result::Result<T,E>::map_err (28 samples, 0.02%)[[vdso]] (28 samples, 0.02%)__GI___clock_gettime (47 samples, 0.04%)std::time::Instant::elapsed (67 samples, 0.05%)std::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Instant::now (54 samples, 0.04%)std::sys::pal::unix::time::Timespec::now (53 samples, 0.04%)std::sys::pal::unix::cvt (23 samples, 0.02%)__GI_getsockname (3,792 samples, 2.89%)__..[unknown] (3,714 samples, 2.83%)[u..[unknown] (3,661 samples, 2.79%)[u..[unknown] (3,557 samples, 2.71%)[u..[unknown] (3,416 samples, 2.60%)[u..[unknown] (2,695 samples, 2.05%)[..[unknown] (2,063 samples, 1.57%)[unknown] (891 samples, 0.68%)[unknown] (270 samples, 0.21%)[unknown] (99 samples, 0.08%)[unknown] (94 samples, 0.07%)[unknown] (84 samples, 0.06%)[unknown] (77 samples, 0.06%)[unknown] (25 samples, 0.02%)[unknown] (16 samples, 0.01%)std::sys_common::net::TcpListener::socket_addr::{{closure}} (3,800 samples, 2.89%)st..tokio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)to..mio::net::udp::UdpSocket::local_addr (3,838 samples, 2.92%)mi..std::net::tcp::TcpListener::local_addr (3,838 samples, 2.92%)st..std::sys_common::net::TcpListener::socket_addr (3,838 samples, 2.92%)st..std::sys_common::net::sockname (3,835 samples, 2.92%)st..[[vdso]] (60 samples, 0.05%)rand_chacha::guts::ChaCha::pos64 (168 samples, 0.13%)<ppv_lite86::soft::x2<W,G> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::AddAssign>::add_assign (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as core::ops::arith::Add>::add (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_add_epi32 (26 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right16 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (26 samples, 0.02%)core::core_arch::x86::avx2::_mm256_or_si256 (29 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right20 (31 samples, 0.02%)<ppv_lite86::soft::x2<W,G> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)<ppv_lite86::x86_64::sse2::avx2::u32x4x2_avx2<NI> as ppv_lite86::types::RotateEachWord32>::rotate_each_word_right24 (18 samples, 0.01%)core::core_arch::x86::avx2::_mm256_shuffle_epi8 (18 samples, 0.01%)rand_chacha::guts::round (118 samples, 0.09%)rand_chacha::guts::refill_wide::impl_avx2 (312 samples, 0.24%)rand_chacha::guts::refill_wide::fn_impl (312 samples, 0.24%)rand_chacha::guts::refill_wide_impl (312 samples, 0.24%)<rand_chacha::chacha::ChaCha12Core as rand_core::block::BlockRngCore>::generate (384 samples, 0.29%)rand_chacha::guts::ChaCha::refill4 (384 samples, 0.29%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::other::<impl rand::distributions::distribution::Distribution<[T: _]> for rand::distributions::Standard>::sample (432 samples, 0.33%)rand::rng::Rng::gen (432 samples, 0.33%)rand::distributions::integer::<impl rand::distributions::distribution::Distribution<u8> for rand::distributions::Standard>::sample (432 samples, 0.33%)<rand::rngs::thread::ThreadRng as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand::rngs::adapter::reseeding::ReseedingRng<R,Rsdr> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)<rand_core::block::BlockRng<R> as rand_core::RngCore>::next_u32 (432 samples, 0.33%)rand_core::block::BlockRng<R>::generate_and_set (392 samples, 0.30%)<rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr> as rand_core::block::BlockRngCore>::generate (392 samples, 0.30%)torrust_tracker::servers::udp::handlers::RequestId::make (440 samples, 0.34%)uuid::v4::<impl uuid::Uuid>::new_v4 (436 samples, 0.33%)uuid::rng::bytes (435 samples, 0.33%)rand::random (435 samples, 0.33%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::get_peers_for_client (34 samples, 0.03%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_peers_for_client (22 samples, 0.02%)core::iter::traits::iterator::Iterator::collect (16 samples, 0.01%)<alloc::vec::Vec<T> as core::iter::traits::collect::FromIterator<T>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter (16 samples, 0.01%)<alloc::vec::Vec<T> as alloc::vec::spec_from_iter_nested::SpecFromIterNested<T,I>>::from_iter (16 samples, 0.01%)<core::iter::adapters::cloned::Cloned<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::take::Take<I> as core::iter::traits::iterator::Iterator>::next (16 samples, 0.01%)<core::iter::adapters::filter::Filter<I,P> as core::iter::traits::iterator::Iterator>::next (15 samples, 0.01%)core::iter::traits::iterator::Iterator::find (15 samples, 0.01%)core::iter::traits::iterator::Iterator::try_fold (15 samples, 0.01%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (31 samples, 0.02%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (45 samples, 0.03%)core::slice::iter::Iter<T>::post_inc_start (14 samples, 0.01%)core::ptr::non_null::NonNull<T>::add (14 samples, 0.01%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (26 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (165 samples, 0.13%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (165 samples, 0.13%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (165 samples, 0.13%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (165 samples, 0.13%)<u8 as core::slice::cmp::SliceOrd>::compare (165 samples, 0.13%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (339 samples, 0.26%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (308 samples, 0.23%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (308 samples, 0.23%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (342 samples, 0.26%)std::sys::sync::rwlock::futex::RwLock::spin_read (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (25 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (28 samples, 0.02%)torrust_tracker::core::Tracker::get_torrent_peers_for_peer (436 samples, 0.33%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (397 samples, 0.30%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (29 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (29 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (29 samples, 0.02%)__memcmp_evex_movbe (31 samples, 0.02%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (52 samples, 0.04%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (52 samples, 0.04%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (52 samples, 0.04%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (52 samples, 0.04%)<u8 as core::slice::cmp::SliceOrd>::compare (52 samples, 0.04%)alloc::collections::btree::map::BTreeMap<K,V,A>::entry (103 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (102 samples, 0.08%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (96 samples, 0.07%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (96 samples, 0.07%)<core::ptr::non_null::NonNull<T> as core::cmp::PartialEq>::eq (72 samples, 0.05%)<core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next (104 samples, 0.08%)core::slice::iter::Iter<T>::post_inc_start (32 samples, 0.02%)core::ptr::non_null::NonNull<T>::add (32 samples, 0.02%)__memcmp_evex_movbe (79 samples, 0.06%)core::cmp::impls::<impl core::cmp::Ord for isize>::cmp (81 samples, 0.06%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (271 samples, 0.21%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (271 samples, 0.21%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (271 samples, 0.21%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (271 samples, 0.21%)<u8 as core::slice::cmp::SliceOrd>::compare (271 samples, 0.21%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (610 samples, 0.46%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (566 samples, 0.43%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (566 samples, 0.43%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Immut,K,V,Type>::keys (18 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (616 samples, 0.47%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::KV>::split (15 samples, 0.01%)alloc::collections::btree::map::entry::Entry<K,V,A>::or_insert (46 samples, 0.04%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (45 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert_recursing (40 samples, 0.03%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Mut,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>::insert (27 samples, 0.02%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::get_stats (29 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::values (20 samples, 0.02%)alloc::collections::btree::map::BTreeMap<K,V,A>::insert (120 samples, 0.09%)alloc::collections::btree::map::entry::VacantEntry<K,V,A>::insert (118 samples, 0.09%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Owned,K,V,alloc::collections::btree::node::marker::Leaf>::new_leaf (118 samples, 0.09%)alloc::collections::btree::node::LeafNode<K,V>::new (118 samples, 0.09%)alloc::boxed::Box<T,A>::new_uninit_in (118 samples, 0.09%)alloc::boxed::Box<T,A>::try_new_uninit_in (118 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (118 samples, 0.09%)alloc::alloc::Global::alloc_impl (118 samples, 0.09%)alloc::alloc::alloc (118 samples, 0.09%)__rdl_alloc (118 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (118 samples, 0.09%)__GI___libc_malloc (118 samples, 0.09%)_int_malloc (107 samples, 0.08%)_int_malloc (28 samples, 0.02%)__GI___libc_malloc (32 samples, 0.02%)__rdl_alloc (36 samples, 0.03%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (36 samples, 0.03%)alloc::sync::Arc<T>::new (42 samples, 0.03%)alloc::boxed::Box<T>::new (42 samples, 0.03%)alloc::alloc::exchange_malloc (39 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (39 samples, 0.03%)alloc::alloc::Global::alloc_impl (39 samples, 0.03%)alloc::alloc::alloc (39 samples, 0.03%)core::mem::drop (15 samples, 0.01%)core::ptr::drop_in_place<core::option::Option<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (15 samples, 0.01%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (15 samples, 0.01%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (15 samples, 0.01%)__GI___libc_free (39 samples, 0.03%)_int_free (37 samples, 0.03%)get_max_fast (16 samples, 0.01%)core::option::Option<T>::is_some_and (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer::{{closure}} (50 samples, 0.04%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>> (50 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (50 samples, 0.04%)torrust_tracker_torrent_repository::entry::mutex_std::<impl torrust_tracker_torrent_repository::entry::EntrySync for alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>::insert_or_update_peer_and_get_stats (290 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer_and_get_stats (284 samples, 0.22%)torrust_tracker_torrent_repository::entry::single::<impl torrust_tracker_torrent_repository::entry::Entry for torrust_tracker_torrent_repository::entry::Torrent>::insert_or_update_peer (255 samples, 0.19%)std::sys::sync::rwlock::futex::RwLock::spin_read (16 samples, 0.01%)std::sys::sync::rwlock::futex::RwLock::spin_until (16 samples, 0.01%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents (21 samples, 0.02%)std::sync::rwlock::RwLock<T>::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read (21 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::read_contended (21 samples, 0.02%)torrust_tracker::core::Tracker::update_torrent_with_peer_and_get_stats::{{closure}} (1,147 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::update_torrent_with_peer_and_get_stats (1,144 samples, 0.87%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get_torrents_mut (32 samples, 0.02%)std::sync::rwlock::RwLock<T>::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::write_contended (32 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_write (28 samples, 0.02%)std::sys::sync::rwlock::futex::RwLock::spin_until (28 samples, 0.02%)torrust_tracker::core::Tracker::announce::{{closure}} (1,597 samples, 1.22%)<core::net::socket_addr::SocketAddrV4 as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::ip_addr::Ipv4Addr as core::hash::Hash>::hash (14 samples, 0.01%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (29 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (24 samples, 0.02%)<core::time::Nanoseconds as core::hash::Hash>::hash (25 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (25 samples, 0.02%)core::hash::Hasher::write_u32 (25 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (25 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (36 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (37 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (37 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (64 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (39 samples, 0.03%)core::hash::Hasher::write_u64 (39 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (122 samples, 0.09%)core::hash::impls::<impl core::hash::Hash for u64>::hash (58 samples, 0.04%)core::hash::Hasher::write_u64 (58 samples, 0.04%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (58 samples, 0.04%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (57 samples, 0.04%)core::hash::sip::u8to64_le (23 samples, 0.02%)core::hash::Hasher::write_length_prefix (27 samples, 0.02%)core::hash::Hasher::write_usize (27 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (27 samples, 0.02%)<core::hash::sip::Sip13Rounds as core::hash::sip::Sip>::c_rounds (16 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (246 samples, 0.19%)core::array::<impl core::hash::Hash for [T: N]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (93 samples, 0.07%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (62 samples, 0.05%)core::hash::sip::u8to64_le (17 samples, 0.01%)torrust_tracker::servers::udp::connection_cookie::check (285 samples, 0.22%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (36 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (24 samples, 0.02%)std::time::SystemTime::now (19 samples, 0.01%)std::sys::pal::unix::time::SystemTime::now (19 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_announce::{{closure}} (1,954 samples, 1.49%)<core::net::socket_addr::SocketAddr as core::hash::Hash>::hash (24 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (18 samples, 0.01%)<core::time::Nanoseconds as core::hash::Hash>::hash (20 samples, 0.02%)core::hash::impls::<impl core::hash::Hash for u32>::hash (20 samples, 0.02%)core::hash::Hasher::write_u32 (20 samples, 0.02%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (20 samples, 0.02%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (44 samples, 0.03%)<core::time::Duration as core::hash::Hash>::hash (65 samples, 0.05%)core::hash::impls::<impl core::hash::Hash for u64>::hash (45 samples, 0.03%)core::hash::Hasher::write_u64 (45 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (45 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (45 samples, 0.03%)<torrust_tracker_clock::time_extent::TimeExtent as core::hash::Hash>::hash (105 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u64>::hash (40 samples, 0.03%)core::hash::Hasher::write_u64 (40 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (40 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (39 samples, 0.03%)core::hash::Hasher::write_length_prefix (34 samples, 0.03%)core::hash::Hasher::write_usize (34 samples, 0.03%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (34 samples, 0.03%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (33 samples, 0.03%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::build (231 samples, 0.18%)core::array::<impl core::hash::Hash for [T: N]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for [T]>::hash (100 samples, 0.08%)core::hash::impls::<impl core::hash::Hash for u8>::hash_slice (66 samples, 0.05%)<std::hash::random::DefaultHasher as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::SipHasher13 as core::hash::Hasher>::write (66 samples, 0.05%)<core::hash::sip::Hasher<S> as core::hash::Hasher>::write (61 samples, 0.05%)core::hash::sip::u8to64_le (16 samples, 0.01%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::handlers::handle_connect::{{closure}} (270 samples, 0.21%)torrust_tracker::servers::udp::connection_cookie::make (268 samples, 0.20%)torrust_tracker::servers::udp::connection_cookie::cookie_builder::get_last_time_extent (36 samples, 0.03%)torrust_tracker_clock::time_extent::Make::now (35 samples, 0.03%)torrust_tracker_clock::clock::working::<impl torrust_tracker_clock::clock::Time for torrust_tracker_clock::clock::Clock<torrust_tracker_clock::clock::working::WorkingClock>>::now (31 samples, 0.02%)std::time::SystemTime::now (26 samples, 0.02%)std::sys::pal::unix::time::SystemTime::now (26 samples, 0.02%)torrust_tracker::core::ScrapeData::add_file (19 samples, 0.01%)std::collections::hash::map::HashMap<K,V,S>::insert (19 samples, 0.01%)hashbrown::map::HashMap<K,V,S,A>::insert (19 samples, 0.01%)hashbrown::raw::RawTable<T,A>::find_or_find_insert_slot (16 samples, 0.01%)hashbrown::raw::RawTable<T,A>::reserve (16 samples, 0.01%)<torrust_tracker_primitives::info_hash::InfoHash as core::cmp::Ord>::cmp (17 samples, 0.01%)core::array::<impl core::cmp::Ord for [T: N]>::cmp (17 samples, 0.01%)core::cmp::impls::<impl core::cmp::Ord for &A>::cmp (17 samples, 0.01%)core::slice::cmp::<impl core::cmp::Ord for [T]>::cmp (17 samples, 0.01%)<u8 as core::slice::cmp::SliceOrd>::compare (17 samples, 0.01%)alloc::collections::btree::map::BTreeMap<K,V,A>::get (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,alloc::collections::btree::node::marker::LeafOrInternal>>::search_tree (61 samples, 0.05%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::search_node (53 samples, 0.04%)alloc::collections::btree::search::<impl alloc::collections::btree::node::NodeRef<BorrowType,K,V,Type>>::find_key_index (53 samples, 0.04%)torrust_tracker::servers::udp::handlers::handle_request::{{closure}} (2,336 samples, 1.78%)t..torrust_tracker::servers::udp::handlers::handle_scrape::{{closure}} (101 samples, 0.08%)torrust_tracker::core::Tracker::scrape::{{closure}} (90 samples, 0.07%)torrust_tracker::core::Tracker::get_swarm_metadata (68 samples, 0.05%)torrust_tracker_torrent_repository::repository::rw_lock_std_mutex_std::<impl torrust_tracker_torrent_repository::repository::Repository<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> for torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>::get (64 samples, 0.05%)alloc::raw_vec::finish_grow (19 samples, 0.01%)alloc::vec::Vec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (21 samples, 0.02%)alloc::raw_vec::RawVec<T,A>::grow_amortized (21 samples, 0.02%)<alloc::string::String as core::fmt::Write>::write_str (23 samples, 0.02%)alloc::string::String::push_str (23 samples, 0.02%)alloc::vec::Vec<T,A>::extend_from_slice (23 samples, 0.02%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (23 samples, 0.02%)alloc::vec::Vec<T,A>::append_elements (23 samples, 0.02%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (85 samples, 0.06%)core::fmt::num::imp::fmt_u64 (78 samples, 0.06%)<alloc::string::String as core::fmt::Write>::write_str (15 samples, 0.01%)alloc::string::String::push_str (15 samples, 0.01%)alloc::vec::Vec<T,A>::extend_from_slice (15 samples, 0.01%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (15 samples, 0.01%)alloc::vec::Vec<T,A>::append_elements (15 samples, 0.01%)core::fmt::num::imp::<impl core::fmt::Display for i64>::fmt (37 samples, 0.03%)core::fmt::num::imp::fmt_u64 (36 samples, 0.03%)<T as alloc::string::ToString>::to_string (141 samples, 0.11%)core::option::Option<T>::expect (34 samples, 0.03%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (28 samples, 0.02%)alloc::alloc::dealloc (28 samples, 0.02%)__rdl_dealloc (28 samples, 0.02%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (28 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (55 samples, 0.04%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (55 samples, 0.04%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (55 samples, 0.04%)alloc::raw_vec::RawVec<T,A>::current_memory (20 samples, 0.02%)torrust_tracker::servers::udp::logging::map_action_name (16 samples, 0.01%)binascii::bin2hex (51 samples, 0.04%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (16 samples, 0.01%)core::fmt::write (25 samples, 0.02%)core::fmt::rt::Argument::fmt (15 samples, 0.01%)core::fmt::Formatter::write_fmt (87 samples, 0.07%)core::str::converts::from_utf8 (43 samples, 0.03%)core::str::validations::run_utf8_validation (37 samples, 0.03%)torrust_tracker_primitives::info_hash::InfoHash::to_hex_string (161 samples, 0.12%)<T as alloc::string::ToString>::to_string (161 samples, 0.12%)<torrust_tracker_primitives::info_hash::InfoHash as core::fmt::Display>::fmt (156 samples, 0.12%)torrust_tracker::servers::udp::logging::log_request (479 samples, 0.36%)[[vdso]] (51 samples, 0.04%)alloc::raw_vec::finish_grow (56 samples, 0.04%)alloc::vec::Vec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::reserve::do_reserve_and_handle (64 samples, 0.05%)alloc::raw_vec::RawVec<T,A>::grow_amortized (64 samples, 0.05%)<alloc::string::String as core::fmt::Write>::write_str (65 samples, 0.05%)alloc::string::String::push_str (65 samples, 0.05%)alloc::vec::Vec<T,A>::extend_from_slice (65 samples, 0.05%)<alloc::vec::Vec<T,A> as alloc::vec::spec_extend::SpecExtend<&T,core::slice::iter::Iter<T>>>::spec_extend (65 samples, 0.05%)alloc::vec::Vec<T,A>::append_elements (65 samples, 0.05%)core::fmt::num::imp::<impl core::fmt::Display for i32>::fmt (114 samples, 0.09%)core::fmt::num::imp::fmt_u64 (110 samples, 0.08%)<T as alloc::string::ToString>::to_string (132 samples, 0.10%)core::option::Option<T>::expect (20 samples, 0.02%)core::ptr::drop_in_place<alloc::string::String> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (22 samples, 0.02%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (22 samples, 0.02%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (22 samples, 0.02%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (8,883 samples, 6.77%)torrust_t..torrust_tracker::servers::udp::logging::log_response (238 samples, 0.18%)__GI___lll_lock_wait_private (14 samples, 0.01%)futex_wait (14 samples, 0.01%)__GI___lll_lock_wake_private (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)_int_malloc (191 samples, 0.15%)__libc_calloc (238 samples, 0.18%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)alloc::vec::from_elem (316 samples, 0.24%)<u8 as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (316 samples, 0.24%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (316 samples, 0.24%)<alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (312 samples, 0.24%)alloc::alloc::Global::alloc_impl (312 samples, 0.24%)alloc::alloc::alloc_zeroed (312 samples, 0.24%)__rdl_alloc_zeroed (312 samples, 0.24%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc_zeroed (312 samples, 0.24%)byteorder::ByteOrder::write_i32 (18 samples, 0.01%)<byteorder::BigEndian as byteorder::ByteOrder>::write_u32 (18 samples, 0.01%)core::num::<impl u32>::to_be_bytes (18 samples, 0.01%)core::num::<impl u32>::to_be (18 samples, 0.01%)core::num::<impl u32>::swap_bytes (18 samples, 0.01%)byteorder::io::WriteBytesExt::write_i32 (89 samples, 0.07%)std::io::Write::write_all (71 samples, 0.05%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (71 samples, 0.05%)std::io::cursor::vec_write (71 samples, 0.05%)std::io::cursor::vec_write_unchecked (51 samples, 0.04%)core::ptr::mut_ptr::<impl *mut T>::copy_from (51 samples, 0.04%)core::intrinsics::copy (51 samples, 0.04%)aquatic_udp_protocol::response::Response::write (227 samples, 0.17%)byteorder::io::WriteBytesExt::write_i64 (28 samples, 0.02%)std::io::Write::write_all (21 samples, 0.02%)<std::io::cursor::Cursor<alloc::vec::Vec<u8,A>> as std::io::Write>::write (21 samples, 0.02%)std::io::cursor::vec_write (21 samples, 0.02%)std::io::cursor::vec_write_unchecked (21 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::copy_from (21 samples, 0.02%)core::intrinsics::copy (21 samples, 0.02%)__GI___lll_lock_wake_private (17 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (136 samples, 0.10%)__GI___libc_free (206 samples, 0.16%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (211 samples, 0.16%)alloc::alloc::dealloc (211 samples, 0.16%)__rdl_dealloc (211 samples, 0.16%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (211 samples, 0.16%)core::ptr::drop_in_place<std::io::cursor::Cursor<alloc::vec::Vec<u8>>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::vec::Vec<u8>> (224 samples, 0.17%)core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8>> (224 samples, 0.17%)<alloc::raw_vec::RawVec<T,A> as core::ops::drop::Drop>::drop (224 samples, 0.17%)std::io::cursor::Cursor<T>::new (56 samples, 0.04%)tokio::io::ready::Ready::intersection (23 samples, 0.02%)tokio::io::ready::Ready::from_interest (23 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (83 samples, 0.06%)[unknown] (32,674 samples, 24.88%)[unknown][unknown] (32,402 samples, 24.68%)[unknown][unknown] (32,272 samples, 24.58%)[unknown][unknown] (32,215 samples, 24.54%)[unknown][unknown] (31,174 samples, 23.74%)[unknown][unknown] (30,794 samples, 23.45%)[unknown][unknown] (30,036 samples, 22.88%)[unknown][unknown] (28,639 samples, 21.81%)[unknown][unknown] (27,908 samples, 21.25%)[unknown][unknown] (26,013 samples, 19.81%)[unknown][unknown] (23,181 samples, 17.65%)[unknown][unknown] (19,559 samples, 14.90%)[unknown][unknown] (18,052 samples, 13.75%)[unknown][unknown] (15,794 samples, 12.03%)[unknown][unknown] (14,740 samples, 11.23%)[unknown][unknown] (12,486 samples, 9.51%)[unknown][unknown] (11,317 samples, 8.62%)[unknown][unknown] (10,725 samples, 8.17%)[unknown][unknown] (10,017 samples, 7.63%)[unknown][unknown] (9,713 samples, 7.40%)[unknown][unknown] (8,432 samples, 6.42%)[unknown][unknown] (8,062 samples, 6.14%)[unknown][unknown] (6,973 samples, 5.31%)[unknow..[unknown] (5,328 samples, 4.06%)[unk..[unknown] (4,352 samples, 3.31%)[un..[unknown] (3,786 samples, 2.88%)[u..[unknown] (3,659 samples, 2.79%)[u..[unknown] (3,276 samples, 2.50%)[u..[unknown] (2,417 samples, 1.84%)[..[unknown] (2,115 samples, 1.61%)[unknown] (1,610 samples, 1.23%)[unknown] (422 samples, 0.32%)[unknown] (84 samples, 0.06%)[unknown] (69 samples, 0.05%)__GI___pthread_disable_asynccancel (67 samples, 0.05%)__libc_sendto (32,896 samples, 25.05%)__libc_sendtotokio::net::udp::UdpSocket::send_to_addr::{{closure}}::{{closure}} (32,981 samples, 25.12%)tokio::net::udp::UdpSocket::send_to_addr..mio::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_tomio::io_source::IoSource<T>::do_io (32,981 samples, 25.12%)mio::io_source::IoSource<T>::do_iomio::sys::unix::stateless_io_source::IoSourceState::do_io (32,981 samples, 25.12%)mio::sys::unix::stateless_io_source::IoS..mio::net::udp::UdpSocket::send_to::{{closure}} (32,981 samples, 25.12%)mio::net::udp::UdpSocket::send_to::{{clo..std::net::udp::UdpSocket::send_to (32,981 samples, 25.12%)std::net::udp::UdpSocket::send_tostd::sys_common::net::UdpSocket::send_to (32,981 samples, 25.12%)std::sys_common::net::UdpSocket::send_tostd::sys::pal::unix::cvt (85 samples, 0.06%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (44,349 samples, 33.78%)torrust_tracker::servers::udp::server::Udp::process_req..torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (43,412 samples, 33.06%)torrust_tracker::servers::udp::server::Udp::process_va..torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (34,320 samples, 26.14%)torrust_tracker::servers::udp::server::Udp..torrust_tracker::servers::udp::server::Udp::send_packet::{{closure}} (33,360 samples, 25.41%)torrust_tracker::servers::udp::server::Ud..tokio::net::udp::UdpSocket::send_to::{{closure}} (33,227 samples, 25.31%)tokio::net::udp::UdpSocket::send_to::{{c..tokio::net::udp::UdpSocket::send_to_addr::{{closure}} (33,142 samples, 25.24%)tokio::net::udp::UdpSocket::send_to_addr..tokio::runtime::io::registration::Registration::async_io::{{closure}} (33,115 samples, 25.22%)tokio::runtime::io::registration::Regist..tokio::runtime::io::registration::Registration::readiness::{{closure}} (28 samples, 0.02%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (18 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (15 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (14 samples, 0.01%)<alloc::sync::Arc<T,A> as core::clone::Clone>::clone (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (15 samples, 0.01%)core::sync::atomic::atomic_add (15 samples, 0.01%)__GI___lll_lock_wait_private (16 samples, 0.01%)futex_wait (16 samples, 0.01%)[unknown] (16 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (15 samples, 0.01%)[unknown] (14 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (135 samples, 0.10%)__GI___libc_free (147 samples, 0.11%)syscall (22 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Core<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (15 samples, 0.01%)tokio::runtime::task::harness::Harness<T,S>::dealloc (24 samples, 0.02%)core::mem::drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::boxed::Box<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::core::Cell<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}},alloc::sync::Arc<tokio::runtime::scheduler::multi_thread::handle::Handle>>> (24 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::task::abort::AbortHandle> (262 samples, 0.20%)<tokio::runtime::task::abort::AbortHandle as core::ops::drop::Drop>::drop (262 samples, 0.20%)tokio::runtime::task::raw::RawTask::drop_abort_handle (256 samples, 0.19%)tokio::runtime::task::raw::drop_abort_handle (59 samples, 0.04%)tokio::runtime::task::harness::Harness<T,S>::drop_reference (50 samples, 0.04%)tokio::runtime::task::state::State::ref_dec (50 samples, 0.04%)tokio::runtime::task::raw::RawTask::drop_join_handle_slow (16 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::join::JoinHandle<()>> (47 samples, 0.04%)<tokio::runtime::task::join::JoinHandle<T> as core::ops::drop::Drop>::drop (47 samples, 0.04%)tokio::runtime::task::state::State::drop_join_handle_fast (19 samples, 0.01%)core::sync::atomic::AtomicUsize::compare_exchange_weak (19 samples, 0.01%)core::sync::atomic::atomic_compare_exchange_weak (19 samples, 0.01%)ringbuf::ring_buffer::base::RbBase::is_full (14 samples, 0.01%)<ringbuf::ring_buffer::shared::SharedRb<T,C> as ringbuf::ring_buffer::base::RbBase<T>>::head (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)ringbuf::consumer::Consumer<T,R>::advance (29 samples, 0.02%)ringbuf::ring_buffer::base::RbRead::advance_head (29 samples, 0.02%)ringbuf::ring_buffer::rb::Rb::pop (50 samples, 0.04%)ringbuf::consumer::Consumer<T,R>::pop (50 samples, 0.04%)ringbuf::producer::Producer<T,R>::advance (23 samples, 0.02%)ringbuf::ring_buffer::base::RbWrite::advance_tail (23 samples, 0.02%)core::num::nonzero::<impl core::ops::arith::Rem<core::num::nonzero::NonZero<usize>> for usize>::rem (19 samples, 0.01%)ringbuf::ring_buffer::rb::Rb::push_overwrite (107 samples, 0.08%)ringbuf::ring_buffer::rb::Rb::push (43 samples, 0.03%)ringbuf::producer::Producer<T,R>::push (43 samples, 0.03%)tokio::runtime::task::abort::AbortHandle::is_finished (84 samples, 0.06%)tokio::runtime::task::state::Snapshot::is_complete (84 samples, 0.06%)tokio::runtime::task::join::JoinHandle<T>::abort_handle (17 samples, 0.01%)tokio::runtime::task::raw::RawTask::ref_inc (17 samples, 0.01%)tokio::runtime::task::state::State::ref_inc (17 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (14 samples, 0.01%)core::sync::atomic::atomic_add (14 samples, 0.01%)__GI___lll_lock_wake_private (22 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)malloc_consolidate (95 samples, 0.07%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (76 samples, 0.06%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (31 samples, 0.02%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (26 samples, 0.02%)_int_malloc (282 samples, 0.21%)__GI___libc_malloc (323 samples, 0.25%)alloc::vec::Vec<T>::with_capacity (326 samples, 0.25%)alloc::vec::Vec<T,A>::with_capacity_in (326 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (324 samples, 0.25%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (324 samples, 0.25%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (324 samples, 0.25%)alloc::alloc::Global::alloc_impl (324 samples, 0.25%)alloc::alloc::alloc (324 samples, 0.25%)__rdl_alloc (324 samples, 0.25%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (324 samples, 0.25%)tokio::io::ready::Ready::intersection (24 samples, 0.02%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (199 samples, 0.15%)tokio::util::bit::Pack::unpack (16 samples, 0.01%)tokio::util::bit::unpack (16 samples, 0.01%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (19 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (17 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (16 samples, 0.01%)tokio::net::udp::UdpSocket::readable::{{closure}} (222 samples, 0.17%)tokio::net::udp::UdpSocket::ready::{{closure}} (222 samples, 0.17%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (50 samples, 0.04%)std::io::error::repr_bitpacked::Repr::data (14 samples, 0.01%)std::io::error::repr_bitpacked::decode_repr (14 samples, 0.01%)std::io::error::Error::kind (16 samples, 0.01%)<core::result::Result<T,E> as core::ops::try_trait::Try>::branch (14 samples, 0.01%)[unknown] (8,756 samples, 6.67%)[unknown][unknown] (8,685 samples, 6.61%)[unknown][unknown] (8,574 samples, 6.53%)[unknown][unknown] (8,415 samples, 6.41%)[unknown][unknown] (7,686 samples, 5.85%)[unknow..[unknown] (7,239 samples, 5.51%)[unknow..[unknown] (6,566 samples, 5.00%)[unkno..[unknown] (5,304 samples, 4.04%)[unk..[unknown] (4,008 samples, 3.05%)[un..[unknown] (3,571 samples, 2.72%)[u..[unknown] (2,375 samples, 1.81%)[..[unknown] (1,844 samples, 1.40%)[unknown] (1,030 samples, 0.78%)[unknown] (344 samples, 0.26%)[unknown] (113 samples, 0.09%)__libc_recvfrom (8,903 samples, 6.78%)__libc_re..__GI___pthread_disable_asynccancel (22 samples, 0.02%)std::sys::pal::unix::cvt (20 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (9,005 samples, 6.86%)tokio::ne..mio::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)mio::net:..mio::io_source::IoSource<T>::do_io (8,964 samples, 6.83%)mio::io_s..mio::sys::unix::stateless_io_source::IoSourceState::do_io (8,964 samples, 6.83%)mio::sys:..mio::net::udp::UdpSocket::recv_from::{{closure}} (8,964 samples, 6.83%)mio::net:..std::net::udp::UdpSocket::recv_from (8,964 samples, 6.83%)std::net:..std::sys_common::net::UdpSocket::recv_from (8,964 samples, 6.83%)std::sys_..std::sys::pal::unix::net::Socket::recv_from (8,964 samples, 6.83%)std::sys:..std::sys::pal::unix::net::Socket::recv_from_with_flags (8,964 samples, 6.83%)std::sys:..std::sys_common::net::sockaddr_to_addr (23 samples, 0.02%)tokio::runtime::io::registration::Registration::clear_readiness (18 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (18 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (32 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (9,967 samples, 7.59%)torrust_tr..tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (9,291 samples, 7.08%)tokio::ne..tokio::runtime::io::registration::Registration::async_io::{{closure}} (9,287 samples, 7.07%)tokio::ru..tokio::runtime::io::registration::Registration::readiness::{{closure}} (45 samples, 0.03%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (41 samples, 0.03%)__memcpy_avx512_unaligned_erms (424 samples, 0.32%)__memcpy_avx512_unaligned_erms (493 samples, 0.38%)__memcpy_avx512_unaligned_erms (298 samples, 0.23%)syscall (1,105 samples, 0.84%)[unknown] (1,095 samples, 0.83%)[unknown] (1,091 samples, 0.83%)[unknown] (1,049 samples, 0.80%)[unknown] (998 samples, 0.76%)[unknown] (907 samples, 0.69%)[unknown] (710 samples, 0.54%)[unknown] (635 samples, 0.48%)[unknown] (538 samples, 0.41%)[unknown] (358 samples, 0.27%)[unknown] (256 samples, 0.19%)[unknown] (153 samples, 0.12%)[unknown] (96 samples, 0.07%)[unknown] (81 samples, 0.06%)tokio::runtime::context::with_scheduler (36 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (31 samples, 0.02%)tokio::runtime::context::with_scheduler::{{closure}} (27 samples, 0.02%)tokio::runtime::context::scoped::Scoped<T>::with (27 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (25 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (15 samples, 0.01%)core::sync::atomic::AtomicUsize::fetch_add (340 samples, 0.26%)core::sync::atomic::atomic_add (340 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (354 samples, 0.27%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (367 samples, 0.28%)[unknown] (95 samples, 0.07%)[unknown] (93 samples, 0.07%)[unknown] (92 samples, 0.07%)[unknown] (90 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (73 samples, 0.06%)[unknown] (63 samples, 0.05%)[unknown] (44 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (35 samples, 0.03%)[unknown] (30 samples, 0.02%)[unknown] (22 samples, 0.02%)[unknown] (21 samples, 0.02%)[unknown] (20 samples, 0.02%)[unknown] (17 samples, 0.01%)tokio::runtime::driver::Handle::unpark (99 samples, 0.08%)tokio::runtime::driver::IoHandle::unpark (99 samples, 0.08%)tokio::runtime::io::driver::Handle::unpark (99 samples, 0.08%)mio::waker::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::fdbased::Waker::wake (99 samples, 0.08%)mio::sys::unix::waker::eventfd::WakerInternal::wake (99 samples, 0.08%)<&std::fs::File as std::io::Write>::write (99 samples, 0.08%)std::sys::pal::unix::fs::File::write (99 samples, 0.08%)std::sys::pal::unix::fd::FileDesc::write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)__GI___libc_write (99 samples, 0.08%)tokio::runtime::context::with_scheduler (1,615 samples, 1.23%)std::thread::local::LocalKey<T>::try_with (1,613 samples, 1.23%)tokio::runtime::context::with_scheduler::{{closure}} (1,612 samples, 1.23%)tokio::runtime::context::scoped::Scoped<T>::with (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::with_current::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task::{{closure}} (1,611 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (1,609 samples, 1.23%)tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (101 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_option_task_without_yield (1,647 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::schedule_task (1,646 samples, 1.25%)tokio::runtime::scheduler::multi_thread::worker::with_current (1,646 samples, 1.25%)tokio::util::sharded_list::ShardGuard<L,<L as tokio::util::linked_list::Link>::Target>::push (23 samples, 0.02%)tokio::util::linked_list::LinkedList<L,<L as tokio::util::linked_list::Link>::Target>::push_front (18 samples, 0.01%)tokio::runtime::task::list::OwnedTasks<S>::bind_inner (104 samples, 0.08%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::lock_shard (60 samples, 0.05%)tokio::util::sharded_list::ShardedList<L,<L as tokio::util::linked_list::Link>::Target>::shard_inner (57 samples, 0.04%)tokio::loom::std::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sync::mutex::Mutex<T>::lock (51 samples, 0.04%)std::sys::sync::mutex::futex::Mutex::lock (49 samples, 0.04%)core::sync::atomic::AtomicU32::compare_exchange (38 samples, 0.03%)core::sync::atomic::atomic_compare_exchange (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (162 samples, 0.12%)__memcpy_avx512_unaligned_erms (34 samples, 0.03%)__GI___lll_lock_wake_private (127 samples, 0.10%)[unknown] (125 samples, 0.10%)[unknown] (124 samples, 0.09%)[unknown] (119 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (106 samples, 0.08%)[unknown] (87 samples, 0.07%)[unknown] (82 samples, 0.06%)[unknown] (51 samples, 0.04%)[unknown] (27 samples, 0.02%)[unknown] (19 samples, 0.01%)[unknown] (14 samples, 0.01%)_int_free (77 samples, 0.06%)[unknown] (1,207 samples, 0.92%)[unknown] (1,146 samples, 0.87%)[unknown] (1,126 samples, 0.86%)[unknown] (1,091 samples, 0.83%)[unknown] (1,046 samples, 0.80%)[unknown] (962 samples, 0.73%)[unknown] (914 samples, 0.70%)[unknown] (848 samples, 0.65%)[unknown] (774 samples, 0.59%)[unknown] (580 samples, 0.44%)[unknown] (456 samples, 0.35%)[unknown] (305 samples, 0.23%)[unknown] (85 samples, 0.06%)__GI_mprotect (2,474 samples, 1.88%)_..[unknown] (2,457 samples, 1.87%)[..[unknown] (2,440 samples, 1.86%)[..[unknown] (2,436 samples, 1.86%)[..[unknown] (2,435 samples, 1.85%)[..[unknown] (2,360 samples, 1.80%)[..[unknown] (2,203 samples, 1.68%)[unknown] (1,995 samples, 1.52%)[unknown] (1,709 samples, 1.30%)[unknown] (1,524 samples, 1.16%)[unknown] (1,193 samples, 0.91%)[unknown] (865 samples, 0.66%)[unknown] (539 samples, 0.41%)[unknown] (259 samples, 0.20%)[unknown] (80 samples, 0.06%)[unknown] (29 samples, 0.02%)sysmalloc (3,786 samples, 2.88%)sy..grow_heap (2,509 samples, 1.91%)g.._int_malloc (4,038 samples, 3.08%)_in..unlink_chunk (31 samples, 0.02%)alloc::alloc::exchange_malloc (4,335 samples, 3.30%)all..<alloc::alloc::Global as core::alloc::Allocator>::allocate (4,329 samples, 3.30%)<al..alloc::alloc::Global::alloc_impl (4,329 samples, 3.30%)all..alloc::alloc::alloc (4,329 samples, 3.30%)all..__rdl_alloc (4,329 samples, 3.30%)__r..std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (4,329 samples, 3.30%)std..std::sys::pal::unix::alloc::aligned_malloc (4,329 samples, 3.30%)std..__posix_memalign (4,297 samples, 3.27%)__p..__posix_memalign (4,297 samples, 3.27%)__p.._mid_memalign (4,297 samples, 3.27%)_mi.._int_memalign (4,149 samples, 3.16%)_in..sysmalloc (18 samples, 0.01%)core::option::Option<T>::map (6,666 samples, 5.08%)core::..tokio::task::spawn::spawn_inner::{{closure}} (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::Handle::spawn (6,665 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (6,664 samples, 5.08%)tokio:..tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (6,661 samples, 5.07%)tokio:..tokio::runtime::task::list::OwnedTasks<S>::bind (4,692 samples, 3.57%)toki..tokio::runtime::task::new_task (4,579 samples, 3.49%)tok..tokio::runtime::task::raw::RawTask::new (4,579 samples, 3.49%)tok..tokio::runtime::task::core::Cell<T,S>::new (4,579 samples, 3.49%)tok..alloc::boxed::Box<T>::new (4,389 samples, 3.34%)all..tokio::runtime::context::current::with_current (7,636 samples, 5.82%)tokio::..std::thread::local::LocalKey<T>::try_with (7,635 samples, 5.81%)std::th..tokio::runtime::context::current::with_current::{{closure}} (7,188 samples, 5.47%)tokio::..tokio::task::spawn::spawn (7,670 samples, 5.84%)tokio::..tokio::task::spawn::spawn_inner (7,670 samples, 5.84%)tokio::..tokio::runtime::task::id::Id::next (24 samples, 0.02%)core::sync::atomic::AtomicU64::fetch_add (24 samples, 0.02%)core::sync::atomic::atomic_add (24 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (62,691 samples, 47.75%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (62,691 samples, 47.75%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (18,228 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (18,226 samples, 13.88%)torrust_tracker::serv..torrust_tracker::servers::udp::server::Udp::spawn_request_processor (7,679 samples, 5.85%)torrust..__memcpy_avx512_unaligned_erms (38 samples, 0.03%)__memcpy_avx512_unaligned_erms (407 samples, 0.31%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (411 samples, 0.31%)tokio::runtime::task::core::Core<T,S>::poll (63,150 samples, 48.10%)tokio::runtime::task::core::Core<T,S>::polltokio::runtime::task::core::Core<T,S>::drop_future_or_output (459 samples, 0.35%)tokio::runtime::task::core::Core<T,S>::set_stage (459 samples, 0.35%)__memcpy_avx512_unaligned_erms (16 samples, 0.01%)__memcpy_avx512_unaligned_erms (398 samples, 0.30%)__memcpy_avx512_unaligned_erms (325 samples, 0.25%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (330 samples, 0.25%)tokio::runtime::task::core::Core<T,S>::set_stage (731 samples, 0.56%)tokio::runtime::task::harness::poll_future (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (63,908 samples, 48.67%)std::panic::catch_unwindstd::panicking::try (63,908 samples, 48.67%)std::panicking::trystd::panicking::try::do_call (63,908 samples, 48.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (63,908 samples, 48.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()..tokio::runtime::task::harness::poll_future::{{closure}} (63,908 samples, 48.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::store_output (758 samples, 0.58%)tokio::runtime::coop::budget (65,027 samples, 49.53%)tokio::runtime::coop::budgettokio::runtime::coop::with_budget (65,027 samples, 49.53%)tokio::runtime::coop::with_budgettokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (65,009 samples, 49.51%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}}tokio::runtime::task::LocalNotified<S>::run (65,003 samples, 49.51%)tokio::runtime::task::LocalNotified<S>::runtokio::runtime::task::raw::RawTask::poll (65,003 samples, 49.51%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (64,538 samples, 49.15%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (64,493 samples, 49.12%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (63,919 samples, 48.68%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::scheduler::multi_thread::stats::Stats::start_poll (93 samples, 0.07%)syscall (2,486 samples, 1.89%)s..[unknown] (2,424 samples, 1.85%)[..[unknown] (2,416 samples, 1.84%)[..[unknown] (2,130 samples, 1.62%)[unknown] (2,013 samples, 1.53%)[unknown] (1,951 samples, 1.49%)[unknown] (1,589 samples, 1.21%)[unknown] (1,415 samples, 1.08%)[unknown] (1,217 samples, 0.93%)[unknown] (820 samples, 0.62%)[unknown] (564 samples, 0.43%)[unknown] (360 samples, 0.27%)[unknown] (244 samples, 0.19%)[unknown] (194 samples, 0.15%)tokio::runtime::scheduler::multi_thread::idle::Idle::notify_should_wakeup (339 samples, 0.26%)core::sync::atomic::AtomicUsize::fetch_add (337 samples, 0.26%)core::sync::atomic::atomic_add (337 samples, 0.26%)tokio::runtime::scheduler::multi_thread::idle::Idle::worker_to_notify (364 samples, 0.28%)[unknown] (154 samples, 0.12%)[unknown] (152 samples, 0.12%)[unknown] (143 samples, 0.11%)[unknown] (139 samples, 0.11%)[unknown] (131 samples, 0.10%)[unknown] (123 samples, 0.09%)[unknown] (110 samples, 0.08%)[unknown] (80 samples, 0.06%)[unknown] (74 samples, 0.06%)[unknown] (65 samples, 0.05%)[unknown] (64 samples, 0.05%)[unknown] (47 samples, 0.04%)[unknown] (44 samples, 0.03%)[unknown] (43 samples, 0.03%)[unknown] (40 samples, 0.03%)[unknown] (26 samples, 0.02%)[unknown] (20 samples, 0.02%)__GI___libc_write (158 samples, 0.12%)__GI___libc_write (158 samples, 0.12%)mio::sys::unix::waker::eventfd::WakerInternal::wake (159 samples, 0.12%)<&std::fs::File as std::io::Write>::write (159 samples, 0.12%)std::sys::pal::unix::fs::File::write (159 samples, 0.12%)std::sys::pal::unix::fd::FileDesc::write (159 samples, 0.12%)tokio::runtime::driver::Handle::unpark (168 samples, 0.13%)tokio::runtime::driver::IoHandle::unpark (168 samples, 0.13%)tokio::runtime::io::driver::Handle::unpark (168 samples, 0.13%)mio::waker::Waker::wake (165 samples, 0.13%)mio::sys::unix::waker::fdbased::Waker::wake (165 samples, 0.13%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (68,159 samples, 51.91%)tokio::runtime::scheduler::multi_thread::worker::Context::run_tasktokio::runtime::scheduler::multi_thread::worker::Core::transition_from_searching (3,024 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::transition_worker_from_searching (3,023 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::worker::<impl tokio::runtime::scheduler::multi_thread::handle::Handle>::notify_parked_local (3,022 samples, 2.30%)t..tokio::runtime::scheduler::multi_thread::park::Unparker::unpark (171 samples, 0.13%)tokio::runtime::scheduler::multi_thread::park::Inner::unpark (171 samples, 0.13%)core::option::Option<T>::or_else (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task::{{closure}} (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Local<T>::pop (14 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::next_local_task (18 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Core::tune_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::stats::Stats::tuned_global_queue_interval (53 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::next_task (107 samples, 0.08%)__GI___libc_free (17 samples, 0.01%)_int_free (17 samples, 0.01%)alloc::collections::btree::navigate::LazyLeafRange<alloc::collections::btree::node::marker::Dying,K,V>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::navigate::<impl alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::Leaf>,alloc::collections::btree::node::marker::Edge>>::deallocating_end (18 samples, 0.01%)alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,alloc::collections::btree::node::marker::LeafOrInternal>::deallocate_and_ascend (18 samples, 0.01%)<alloc::alloc::Global as core::alloc::Allocator>::deallocate (18 samples, 0.01%)alloc::alloc::dealloc (18 samples, 0.01%)__rdl_dealloc (18 samples, 0.01%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::dealloc (18 samples, 0.01%)alloc::collections::btree::map::IntoIter<K,V,A>::dying_next (19 samples, 0.01%)tokio::runtime::task::Task<S>::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::RawTask::shutdown (26 samples, 0.02%)tokio::runtime::task::raw::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::Harness<T,S>::shutdown (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task (26 samples, 0.02%)std::panic::catch_unwind (26 samples, 0.02%)std::panicking::try (26 samples, 0.02%)std::panicking::try::do_call (26 samples, 0.02%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (26 samples, 0.02%)core::ops::function::FnOnce::call_once (26 samples, 0.02%)tokio::runtime::task::harness::cancel_task::{{closure}} (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::drop_future_or_output (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage (26 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (26 samples, 0.02%)tokio::runtime::task::core::Core<T,S>::set_stage::{{closure}} (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker::core::Tracker> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (26 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::repository::RwLockStd<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)core::ptr::drop_in_place<std::sync::rwlock::RwLock<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>>> (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)core::mem::drop (26 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::info_hash::InfoHash,alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>>> (26 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (26 samples, 0.02%)alloc::collections::btree::node::Handle<alloc::collections::btree::node::NodeRef<alloc::collections::btree::node::marker::Dying,K,V,NodeType>,alloc::collections::btree::node::marker::KV>::drop_key_val (24 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::assume_init_drop (24 samples, 0.02%)core::ptr::drop_in_place<alloc::sync::Arc<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>>> (24 samples, 0.02%)<alloc::sync::Arc<T,A> as core::ops::drop::Drop>::drop (24 samples, 0.02%)alloc::sync::Arc<T,A>::drop_slow (21 samples, 0.02%)core::ptr::drop_in_place<std::sync::mutex::Mutex<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<core::cell::UnsafeCell<torrust_tracker_torrent_repository::entry::Torrent>> (20 samples, 0.02%)core::ptr::drop_in_place<torrust_tracker_torrent_repository::entry::Torrent> (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::BTreeMap<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::BTreeMap<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)core::mem::drop (20 samples, 0.02%)core::ptr::drop_in_place<alloc::collections::btree::map::IntoIter<torrust_tracker_primitives::peer::Id,alloc::sync::Arc<torrust_tracker_primitives::peer::Peer>>> (20 samples, 0.02%)<alloc::collections::btree::map::IntoIter<K,V,A> as core::ops::drop::Drop>::drop (20 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::pre_shutdown (33 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::close_and_shutdown_all (33 samples, 0.03%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (114 samples, 0.09%)alloc::sync::Arc<T,A>::inner (114 samples, 0.09%)core::ptr::non_null::NonNull<T>::as_ref (114 samples, 0.09%)core::iter::range::<impl core::iter::traits::iterator::Iterator for core::ops::range::Range<A>>::next (108 samples, 0.08%)<core::ops::range::Range<T> as core::iter::range::RangeIteratorImpl>::spec_next (108 samples, 0.08%)core::cmp::impls::<impl core::cmp::PartialOrd for usize>::lt (106 samples, 0.08%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (49 samples, 0.04%)alloc::sync::Arc<T,A>::inner (49 samples, 0.04%)core::ptr::non_null::NonNull<T>::as_ref (49 samples, 0.04%)core::num::<impl u32>::wrapping_sub (132 samples, 0.10%)core::sync::atomic::AtomicU64::load (40 samples, 0.03%)core::sync::atomic::atomic_load (40 samples, 0.03%)tokio::loom::std::atomic_u32::AtomicU32::unsync_load (48 samples, 0.04%)core::sync::atomic::AtomicU32::load (48 samples, 0.04%)core::sync::atomic::atomic_load (48 samples, 0.04%)<alloc::sync::Arc<T,A> as core::ops::deref::Deref>::deref (65 samples, 0.05%)alloc::sync::Arc<T,A>::inner (65 samples, 0.05%)core::ptr::non_null::NonNull<T>::as_ref (65 samples, 0.05%)core::num::<impl u32>::wrapping_sub (50 samples, 0.04%)core::sync::atomic::AtomicU32::load (55 samples, 0.04%)core::sync::atomic::atomic_load (55 samples, 0.04%)core::sync::atomic::AtomicU64::load (80 samples, 0.06%)core::sync::atomic::atomic_load (80 samples, 0.06%)tokio::runtime::scheduler::multi_thread::queue::pack (26 samples, 0.02%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (666 samples, 0.51%)tokio::runtime::scheduler::multi_thread::queue::unpack (147 samples, 0.11%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (1,036 samples, 0.79%)tokio::runtime::scheduler::multi_thread::queue::unpack (46 samples, 0.04%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_to_searching (49 samples, 0.04%)tokio::runtime::scheduler::multi_thread::idle::Idle::transition_worker_to_searching (21 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (2,414 samples, 1.84%)t..tokio::util::rand::FastRand::fastrand_n (24 samples, 0.02%)tokio::util::rand::FastRand::fastrand (24 samples, 0.02%)std::sys_common::backtrace::__rust_begin_short_backtrace (98,136 samples, 74.74%)std::sys_common::backtrace::__rust_begin_short_backtracetokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}} (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Spawner::spawn_thread::{{closure}}tokio::runtime::blocking::pool::Inner::run (98,136 samples, 74.74%)tokio::runtime::blocking::pool::Inner::runtokio::runtime::blocking::pool::Task::run (98,042 samples, 74.67%)tokio::runtime::blocking::pool::Task::runtokio::runtime::task::UnownedTask<S>::run (98,042 samples, 74.67%)tokio::runtime::task::UnownedTask<S>::runtokio::runtime::task::raw::RawTask::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::RawTask::polltokio::runtime::task::raw::poll (98,042 samples, 74.67%)tokio::runtime::task::raw::polltokio::runtime::task::harness::Harness<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::polltokio::runtime::task::harness::Harness<T,S>::poll_inner (98,042 samples, 74.67%)tokio::runtime::task::harness::Harness<T,S>::poll_innertokio::runtime::task::harness::poll_future (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_futurestd::panic::catch_unwind (98,042 samples, 74.67%)std::panic::catch_unwindstd::panicking::try (98,042 samples, 74.67%)std::panicking::trystd::panicking::try::do_call (98,042 samples, 74.67%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,042 samples, 74.67%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncetokio::runtime::task::harness::poll_future::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::harness::poll_future::{{closure}}tokio::runtime::task::core::Core<T,S>::poll (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::polltokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (98,042 samples, 74.67%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_muttokio::runtime::task::core::Core<T,S>::poll::{{closure}} (98,042 samples, 74.67%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}}<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (98,042 samples, 74.67%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::polltokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}}tokio::runtime::scheduler::multi_thread::worker::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::runtokio::runtime::context::runtime::enter_runtime (98,042 samples, 74.67%)tokio::runtime::context::runtime::enter_runtimetokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}tokio::runtime::context::set_scheduler (98,042 samples, 74.67%)tokio::runtime::context::set_schedulerstd::thread::local::LocalKey<T>::with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::withstd::thread::local::LocalKey<T>::try_with (98,042 samples, 74.67%)std::thread::local::LocalKey<T>::try_withtokio::runtime::context::set_scheduler::{{closure}} (98,042 samples, 74.67%)tokio::runtime::context::set_scheduler::{{closure}}tokio::runtime::context::scoped::Scoped<T>::set (98,042 samples, 74.67%)tokio::runtime::context::scoped::Scoped<T>::settokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}}tokio::runtime::scheduler::multi_thread::worker::Context::run (98,042 samples, 74.67%)tokio::runtime::scheduler::multi_thread::worker::Context::runstd::panic::catch_unwind (98,137 samples, 74.74%)std::panic::catch_unwindstd::panicking::try (98,137 samples, 74.74%)std::panicking::trystd::panicking::try::do_call (98,137 samples, 74.74%)std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (98,137 samples, 74.74%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_oncestd::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}} (98,137 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}::{{closure}}<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once (98,139 samples, 74.74%)<alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_oncecore::ops::function::FnOnce::call_once{{vtable.shim}} (98,139 samples, 74.74%)core::ops::function::FnOnce::call_once{{vtable.shim}}std::thread::Builder::spawn_unchecked_::{{closure}} (98,139 samples, 74.74%)std::thread::Builder::spawn_unchecked_::{{closure}}clone3 (98,205 samples, 74.79%)clone3start_thread (98,205 samples, 74.79%)start_threadstd::sys::pal::unix::thread::Thread::new::thread_start (98,158 samples, 74.76%)std::sys::pal::unix::thread::Thread::new::thread_startcore::ptr::drop_in_place<std::sys::pal::unix::stack_overflow::Handler> (19 samples, 0.01%)<std::sys::pal::unix::stack_overflow::Handler as core::ops::drop::Drop>::drop (19 samples, 0.01%)std::sys::pal::unix::stack_overflow::imp::drop_handler (19 samples, 0.01%)__GI_munmap (19 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (18 samples, 0.01%)[unknown] (17 samples, 0.01%)[unknown] (16 samples, 0.01%)core::fmt::Formatter::pad_integral (112 samples, 0.09%)core::fmt::Formatter::pad_integral::write_prefix (59 samples, 0.04%)core::fmt::Formatter::pad_integral (16 samples, 0.01%)core::fmt::write (20 samples, 0.02%)core::ptr::drop_in_place<aquatic_udp_protocol::response::Response> (19 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::Stage<torrust_tracker::servers::udp::server::Udp::process_request::{{closure}}>> (51 samples, 0.04%)rand_chacha::guts::round (18 samples, 0.01%)rand_chacha::guts::refill_wide::impl_avx2 (26 samples, 0.02%)rand_chacha::guts::refill_wide::fn_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide_impl (26 samples, 0.02%)rand_chacha::guts::refill_wide (14 samples, 0.01%)std_detect::detect::arch::x86::__is_feature_detected::avx2 (14 samples, 0.01%)std_detect::detect::check_for (14 samples, 0.01%)std_detect::detect::cache::test (14 samples, 0.01%)std_detect::detect::cache::Cache::test (14 samples, 0.01%)core::sync::atomic::AtomicUsize::load (14 samples, 0.01%)core::sync::atomic::atomic_load (14 samples, 0.01%)core::cell::RefCell<T>::borrow_mut (81 samples, 0.06%)core::cell::RefCell<T>::try_borrow_mut (81 samples, 0.06%)core::cell::BorrowRefMut::new (81 samples, 0.06%)std::sys::pal::unix::time::Timespec::now (164 samples, 0.12%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task (106 samples, 0.08%)tokio::runtime::coop::budget (105 samples, 0.08%)tokio::runtime::coop::with_budget (105 samples, 0.08%)tokio::runtime::scheduler::multi_thread::worker::Context::run_task::{{closure}} (96 samples, 0.07%)std::sys::pal::unix::time::Timespec::sub_timespec (35 samples, 0.03%)std::sys::sync::mutex::futex::Mutex::lock_contended (15 samples, 0.01%)syscall (90 samples, 0.07%)tokio::runtime::io::scheduled_io::ScheduledIo::wake (15 samples, 0.01%)tokio::runtime::scheduler::multi_thread::worker::Context::park (22 samples, 0.02%)tokio::runtime::scheduler::multi_thread::worker::Core::transition_from_parked (21 samples, 0.02%)<tokio::runtime::blocking::task::BlockingTask<T> as core::future::future::Future>::poll (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Launch::launch::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run (61 samples, 0.05%)tokio::runtime::context::runtime::enter_runtime (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}} (61 samples, 0.05%)tokio::runtime::context::set_scheduler (61 samples, 0.05%)std::thread::local::LocalKey<T>::with (61 samples, 0.05%)std::thread::local::LocalKey<T>::try_with (61 samples, 0.05%)tokio::runtime::context::set_scheduler::{{closure}} (61 samples, 0.05%)tokio::runtime::context::scoped::Scoped<T>::set (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::run::{{closure}}::{{closure}} (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Context::run (61 samples, 0.05%)tokio::runtime::scheduler::multi_thread::worker::Core::steal_work (19 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into (17 samples, 0.01%)tokio::runtime::scheduler::multi_thread::queue::Steal<T>::steal_into2 (17 samples, 0.01%)tokio::runtime::context::CONTEXT::__getit (14 samples, 0.01%)core::cell::Cell<T>::get (14 samples, 0.01%)core::ptr::drop_in_place<tokio::runtime::task::core::TaskIdGuard> (22 samples, 0.02%)<tokio::runtime::task::core::TaskIdGuard as core::ops::drop::Drop>::drop (22 samples, 0.02%)tokio::runtime::context::set_current_task_id (22 samples, 0.02%)std::thread::local::LocalKey<T>::try_with (22 samples, 0.02%)tokio::loom::std::unsafe_cell::UnsafeCell<T>::with_mut (112 samples, 0.09%)tokio::runtime::task::core::Core<T,S>::poll::{{closure}} (111 samples, 0.08%)tokio::runtime::task::harness::poll_future (125 samples, 0.10%)std::panic::catch_unwind (125 samples, 0.10%)std::panicking::try (125 samples, 0.10%)std::panicking::try::do_call (125 samples, 0.10%)<core::panic::unwind_safe::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (125 samples, 0.10%)tokio::runtime::task::harness::poll_future::{{closure}} (125 samples, 0.10%)tokio::runtime::task::core::Core<T,S>::poll (125 samples, 0.10%)tokio::runtime::task::raw::poll (157 samples, 0.12%)tokio::runtime::task::harness::Harness<T,S>::poll (135 samples, 0.10%)tokio::runtime::task::harness::Harness<T,S>::poll_inner (135 samples, 0.10%)tokio::runtime::time::Driver::park_internal (15 samples, 0.01%)torrust_tracker::bootstrap::logging::INIT (17 samples, 0.01%)__memcpy_avx512_unaligned_erms (397 samples, 0.30%)_int_free (24 samples, 0.02%)_int_malloc (132 samples, 0.10%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE::META (570 samples, 0.43%)__GI___lll_lock_wait_private (22 samples, 0.02%)futex_wait (14 samples, 0.01%)__memcpy_avx512_unaligned_erms (299 samples, 0.23%)_int_free (16 samples, 0.01%)torrust_tracker::servers::udp::logging::log_request::__CALLSITE (361 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::process_request::{{closure}} (41 samples, 0.03%)torrust_tracker::servers::udp::handlers::handle_packet::{{closure}} (23 samples, 0.02%)torrust_tracker::servers::udp::server::Udp::process_valid_request::{{closure}} (53 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::send_response::{{closure}} (14 samples, 0.01%)<tokio::runtime::io::scheduled_io::Readiness as core::future::future::Future>::poll (63 samples, 0.05%)<tokio::runtime::io::scheduled_io::Readiness as core::ops::drop::Drop>::drop (21 samples, 0.02%)__GI___libc_malloc (18 samples, 0.01%)alloc::vec::Vec<T>::with_capacity (116 samples, 0.09%)alloc::vec::Vec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::with_capacity_in (116 samples, 0.09%)alloc::raw_vec::RawVec<T,A>::try_allocate_in (116 samples, 0.09%)<alloc::alloc::Global as core::alloc::Allocator>::allocate (116 samples, 0.09%)alloc::alloc::Global::alloc_impl (116 samples, 0.09%)alloc::alloc::alloc (116 samples, 0.09%)__rdl_alloc (116 samples, 0.09%)std::sys::pal::unix::alloc::<impl core::alloc::global::GlobalAlloc for std::alloc::System>::alloc (116 samples, 0.09%)tokio::runtime::io::registration::Registration::readiness::{{closure}} (53 samples, 0.04%)tokio::runtime::io::scheduled_io::ScheduledIo::readiness::{{closure}} (53 samples, 0.04%)core::ptr::drop_in_place<tokio::runtime::io::scheduled_io::Readiness> (53 samples, 0.04%)_int_malloc (21 samples, 0.02%)[unknown] (36 samples, 0.03%)[unknown] (16 samples, 0.01%)core::mem::zeroed (27 samples, 0.02%)core::mem::maybe_uninit::MaybeUninit<T>::zeroed (27 samples, 0.02%)core::ptr::mut_ptr::<impl *mut T>::write_bytes (27 samples, 0.02%)core::intrinsics::write_bytes (27 samples, 0.02%)[unknown] (27 samples, 0.02%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}}::{{closure}} (64 samples, 0.05%)mio::net::udp::UdpSocket::recv_from (49 samples, 0.04%)mio::io_source::IoSource<T>::do_io (49 samples, 0.04%)mio::sys::unix::stateless_io_source::IoSourceState::do_io (49 samples, 0.04%)mio::net::udp::UdpSocket::recv_from::{{closure}} (49 samples, 0.04%)std::net::udp::UdpSocket::recv_from (49 samples, 0.04%)std::sys_common::net::UdpSocket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from (49 samples, 0.04%)std::sys::pal::unix::net::Socket::recv_from_with_flags (49 samples, 0.04%)torrust_tracker::servers::udp::server::Udp::receive_request::{{closure}} (271 samples, 0.21%)tokio::net::udp::UdpSocket::recv_buf_from::{{closure}} (143 samples, 0.11%)tokio::runtime::io::registration::Registration::async_io::{{closure}} (141 samples, 0.11%)tokio::runtime::io::registration::Registration::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::clear_readiness (15 samples, 0.01%)tokio::runtime::io::scheduled_io::ScheduledIo::set_readiness (15 samples, 0.01%)torrust_tracker::servers::udp::server::Udp::run_with_graceful_shutdown::{{closure}}::{{closure}} (359 samples, 0.27%)torrust_tracker::servers::udp::server::Udp::run_udp_server::{{closure}} (346 samples, 0.26%)torrust_tracker::servers::udp::server::Udp::spawn_request_processor (39 samples, 0.03%)tokio::task::spawn::spawn (39 samples, 0.03%)tokio::task::spawn::spawn_inner (39 samples, 0.03%)tokio::runtime::context::current::with_current (39 samples, 0.03%)std::thread::local::LocalKey<T>::try_with (39 samples, 0.03%)tokio::runtime::context::current::with_current::{{closure}} (39 samples, 0.03%)core::option::Option<T>::map (39 samples, 0.03%)tokio::task::spawn::spawn_inner::{{closure}} (39 samples, 0.03%)tokio::runtime::scheduler::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::spawn (39 samples, 0.03%)tokio::runtime::scheduler::multi_thread::handle::Handle::bind_new_task (39 samples, 0.03%)tokio::runtime::task::list::OwnedTasks<S>::bind (34 samples, 0.03%)all (131,301 samples, 100%)tokio-runtime-w (131,061 samples, 99.82%)tokio-runtime-w \ No newline at end of file From 832051a33cc554960d2e609ce4bda853cd26c92f Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Tue, 7 May 2024 20:01:15 +0200 Subject: [PATCH 146/217] dev: fix udp ring-buffer not looping My previous version would be limited to a single thread, as `push_overwrite` would keep on returning the last element when the ring-buffer was full. Now the ring-buffer is pre-filled and is looped over with a mutating iterator. New handles are progressively swapped-in when the old entries are finished. Note: I think that this implementation can be replaced with a standard vector with the same effect. --- src/servers/udp/server.rs | 123 ++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 66 deletions(-) diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 264afce54..7b8d3d0c9 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -22,7 +22,7 @@ use std::sync::Arc; use futures::future::BoxFuture; use futures::FutureExt; -use ringbuf::traits::{Consumer as _, Observer as _, RingBuffer as _}; +use ringbuf::traits::{Consumer as _, Observer as _, Producer as _}; use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::select; @@ -36,7 +36,6 @@ use crate::core::Tracker; use crate::servers::udp::v0::handlers; use crate::shared::handle::{Handle, Watcher}; -#[derive(Default)] struct ActiveRequests { rb: StaticRb, // the number of requests we handle at the same time. } @@ -49,6 +48,17 @@ impl std::fmt::Debug for ActiveRequests { } } +impl ActiveRequests { + /// Creates a new [`ActiveRequests`] filled with finished tasks. + fn new() -> Self { + let mut rb = StaticRb::default(); + + let () = while rb.try_push(tokio::task::spawn_blocking(|| ()).abort_handle()).is_ok() {}; + + Self { rb } + } +} + impl Drop for ActiveRequests { fn drop(&mut self) { for h in self.rb.pop_iter() { @@ -65,7 +75,6 @@ pub(super) struct Udp { tracker: Arc, socket: Arc, addr: SocketAddr, - reqs: ActiveRequests, } enum Task { @@ -85,7 +94,6 @@ impl Udp { tracker: tracker.clone(), socket: socket.clone(), handle: handle.clone(), - reqs: ActiveRequests::default(), addr, }) } @@ -115,53 +123,59 @@ impl Udp { Ok(()) } - fn main_loop<'a>(mut self) -> BoxFuture<'a, Result<(), Error>> { + fn main_loop<'a>(self) -> BoxFuture<'a, Result<(), Error>> { async move { let mut task: Task; - loop { - let tracker = self.tracker.clone(); - let socket = self.socket.clone(); - - trace!("await readable socket"); - { - task = select! { - biased; - () = self.handle.wait_shutdown() => Task::Shutdown, - r = socket.readable() => Task::Readable(r), - () = self.handle.wait_graceful_shutdown() => Task::Graceful, - }; - } + let reqs = &mut ActiveRequests::new(); + + 'main: loop { + 'ring: for h in reqs.rb.iter_mut() { + let tracker = self.tracker.clone(); + let socket = self.socket.clone(); + + trace!("await readable socket"); + { + task = select! { + biased; + () = self.handle.wait_shutdown() => Task::Shutdown, + r = socket.readable() => Task::Readable(r), + () = self.handle.wait_graceful_shutdown() => Task::Graceful, + }; + } - trace!("brake if interrupted"); - { - let () = match task { - Task::Shutdown | Task::Graceful => { - debug!("(shutting down): stop processing new requests"); - break; - } - Task::Readable(r) => r.map_err(|e| Error::UnableToGetReadableSocket { - addr: self.addr, - err: e.into(), - })?, - Task::Listening(_) => unreachable!(), - }; - } + trace!("brake if interrupted"); + { + let () = match task { + Task::Shutdown | Task::Graceful => { + debug!("(shutting down): stop processing new requests"); + break 'main; + } + Task::Readable(r) => r.map_err(|e| Error::UnableToGetReadableSocket { + addr: self.addr, + err: e.into(), + })?, + Task::Listening(_) => unreachable!(), + }; + } + + trace!("read socket and fulfil request"); + { + trace!("limit number of active jobs"); + if h.is_finished() { + trace!("receive request"); + let request = self.receive_request().await?; - trace!("read socket and fulfil request"); - { - trace!("receive request"); - let request = self.receive_request().await?; + trace!("create new job to respond to {}", request); + let respond_job = + tokio::task::spawn(fulfil_request(tracker, socket, self.addr, request, self.handle.watcher())); - trace!("create new job to respond to {}", request); - let respond_job = - tokio::task::spawn(fulfil_request(tracker, socket, self.addr, request, self.handle.watcher())); + let mut h_new = respond_job.abort_handle(); - trace!("limit number of active jobs"); - if let Some(h) = self.reqs.rb.push_overwrite(respond_job.abort_handle()) { - if !h.is_finished() { - debug!("job was still active and will be aborted"); + let () = std::mem::swap(h, &mut h_new); + } else { let () = tokio::task::yield_now().await; // yield to give it a chance... h.abort(); + break 'ring; } } } @@ -328,26 +342,3 @@ fn get_response_payload(response: &aquatic_udp_protocol::Response) -> Vec { payload } - -// match handle.listening().await { -// Some(addr) => { - -// handle.wait_graceful_shutdown() - -// shutdown_signal_with_message(rx_shutdown, format!("{message}, on socket address: {addr}")).await; - -// info!("Sending graceful shutdown signal"); -// handle.graceful_shutdown(Some(Duration::from_secs(90))); - -// println!("!! shuting down in 90 seconds !!"); - -// loop { -// tokio::time::sleep(Duration::from_secs(1)).await; - -// info!("remaining alive connections: {}", handle.connection_count()); -// } -// } -// None => handle.shutdown(), -// } -// }); -// } From 68e0327fd2be300f7d54cfd48f1390876075ebe2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:24:31 +0100 Subject: [PATCH 147/217] chore(deps): add dependency figment It will replace the custom code for configuration inyection. --- Cargo.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 ++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1795662f..4c6706d71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -521,6 +530,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" + [[package]] name = "byteorder" version = "1.5.0" @@ -1084,6 +1099,27 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "serde", + "uncased", + "version_check", +] + [[package]] name = "flate2" version = "1.0.30" @@ -3670,6 +3706,8 @@ dependencies = [ "crossbeam-skiplist", "dashmap", "derive_more", + "fern", + "figment", "futures", "hex-literal", "hyper", @@ -3922,6 +3960,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index fc26f3fdf..25c990bff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" +fern = "0" +figment = "0.10.18" futures = "0" hex-literal = "0" hyper = "1" @@ -78,7 +80,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] +ignored = ["crossbeam-skiplist", "dashmap", "parking_lot", "serde_bytes"] [dev-dependencies] local-ip-address = "0" @@ -107,5 +109,5 @@ lto = "fat" opt-level = 3 [profile.release-debug] -inherits = "release" debug = true +inherits = "release" From 954272772354fb6cd32ff6f50e4ed9907488cce5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:56:44 +0100 Subject: [PATCH 148/217] test: remove broken example in rustdoc --- packages/configuration/src/lib.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 1f39fc48f..474858f3d 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -229,6 +229,8 @@ //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` +pub mod v1; + use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; @@ -277,15 +279,6 @@ pub struct Info { impl Info { /// Build Configuration Info /// - /// # Examples - /// - /// ``` - /// use torrust_tracker_configuration::Info; - /// - /// let result = Info::new(env_var_config, env_var_path_config, default_path_config, env_var_api_admin_token); - /// assert_eq!(result, ); - /// ``` - /// /// # Errors /// /// Will return `Err` if unable to obtain a configuration. From 32b4278819ddb3320deec1929ba19985e44a836a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 12:57:54 +0100 Subject: [PATCH 149/217] chore(deps): enable figment features: env, toml, test --- Cargo.lock | 53 +++++++++++++++++++++++++++++++ packages/configuration/Cargo.toml | 1 + 2 files changed, 54 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4c6706d71..e97418e74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1115,7 +1115,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic", + "parking_lot", + "pear", "serde", + "tempfile", + "toml", "uncased", "version_check", ] @@ -1639,6 +1643,12 @@ dependencies = [ "serde", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "io-enum" version = "1.1.3" @@ -2289,6 +2299,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.66", +] + [[package]] name = "pem" version = "2.0.1" @@ -2544,6 +2577,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "version_check", + "yansi", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -3761,6 +3807,7 @@ version = "3.0.0-alpha.12-develop" dependencies = [ "config", "derive_more", + "figment", "serde", "serde_with", "thiserror", @@ -4373,6 +4420,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 102177816..e5335d416 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -17,6 +17,7 @@ version.workspace = true [dependencies] config = "0" derive_more = "0" +figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } serde_with = "3" thiserror = "1" From 5e709690b4972e32a2ce82b4323f95aeb4c18022 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 14:00:20 +0100 Subject: [PATCH 150/217] refactor: create new configuration v1 mod with figment - Clone config strcuctures into a new mod `v1`. - Introduce versioning for configuration API. - Split config sections into submodules. TODO: - Still using root mod types in production. - Not using figment to build config in production. --- .../configuration/src/v1/health_check_api.rs | 13 + packages/configuration/src/v1/http_tracker.rs | 23 + packages/configuration/src/v1/mod.rs | 433 ++++++++++++++++++ packages/configuration/src/v1/tracker_api.rs | 32 ++ packages/configuration/src/v1/udp_tracker.rs | 12 + 5 files changed, 513 insertions(+) create mode 100644 packages/configuration/src/v1/health_check_api.rs create mode 100644 packages/configuration/src/v1/http_tracker.rs create mode 100644 packages/configuration/src/v1/mod.rs create mode 100644 packages/configuration/src/v1/tracker_api.rs create mode 100644 packages/configuration/src/v1/udp_tracker.rs diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs new file mode 100644 index 000000000..f7b15249c --- /dev/null +++ b/packages/configuration/src/v1/health_check_api.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Configuration for the Health Check API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HealthCheckApi { + /// The address the API will bind to. + /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs new file mode 100644 index 000000000..4c88feb9c --- /dev/null +++ b/packages/configuration/src/v1/http_tracker.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +/// Configuration for each HTTP tracker. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpTracker { + /// Weather the HTTP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP tracker will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs new file mode 100644 index 000000000..815d74e40 --- /dev/null +++ b/packages/configuration/src/v1/mod.rs @@ -0,0 +1,433 @@ +//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! This module contains the configuration data structures for the +//! Torrust Tracker, which is a `BitTorrent` tracker server. +//! +//! The configuration is loaded from a [TOML](https://toml.io/en/) file +//! `tracker.toml` in the project root folder or from an environment variable +//! with the same content as the file. +//! +//! Configuration can not only be loaded from a file, but also from an +//! environment variable `TORRUST_TRACKER_CONFIG`. This is useful when running +//! the tracker in a Docker container or environments where you do not have a +//! persistent storage or you cannot inject a configuration file. Refer to +//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more +//! information about how to pass configuration to the tracker. +//! +//! When you run the tracker without providing the configuration via a file or +//! env var, the default configuration is used. +//! +//! # Table of contents +//! +//! - [Sections](#sections) +//! - [Port binding](#port-binding) +//! - [TSL support](#tsl-support) +//! - [Generating self-signed certificates](#generating-self-signed-certificates) +//! - [Default configuration](#default-configuration) +//! +//! ## Sections +//! +//! Each section in the toml structure is mapped to a data structure. For +//! example, the `[http_api]` section (configuration for the tracker HTTP API) +//! is mapped to the [`HttpApi`] structure. +//! +//! > **NOTICE**: some sections are arrays of structures. For example, the +//! > `[[udp_trackers]]` section is an array of [`UdpTracker`] since +//! > you can have multiple running UDP trackers bound to different ports. +//! +//! Please refer to the documentation of each structure for more information +//! about each section. +//! +//! - [`Core configuration`](crate::v1::Configuration) +//! - [`HTTP API configuration`](crate::v1::tracker_api::HttpApi) +//! - [`HTTP Tracker configuration`](crate::v1::http_tracker::HttpTracker) +//! - [`UDP Tracker configuration`](crate::v1::udp_tracker::UdpTracker) +//! - [`Health Check API configuration`](crate::v1::health_check_api::HealthCheckApi) +//! +//! ## Port binding +//! +//! For the API, HTTP and UDP trackers you can bind to a random port by using +//! port `0`. For example, if you want to bind to a random port on all +//! interfaces, use `0.0.0.0:0`. The OS will choose a random free port. +//! +//! ## TSL support +//! +//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to +//! `true` and setting the paths to the certificate and key files. +//! +//! Typically, you will have a `storage` directory like the following: +//! +//! ```text +//! storage/ +//! ├── config.toml +//! └── tracker +//! ├── etc +//! │ └── tracker.toml +//! ├── lib +//! │ ├── database +//! │ │ ├── sqlite3.db +//! │ │ └── sqlite.db +//! │ └── tls +//! │ ├── localhost.crt +//! │ └── localhost.key +//! └── log +//! ``` +//! +//! where the application stores all the persistent data. +//! +//! Alternatively, you could setup a reverse proxy like Nginx or Apache to +//! handle the SSL/TLS part and forward the requests to the tracker. If you do +//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! to `true` in the configuration file. It's out of scope for this +//! documentation to explain in detail how to setup a reverse proxy, but the +//! configuration file should be something like this: +//! +//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! server { +//! listen 80; +//! server_name tracker.torrust.com; +//! +//! return 301 https://$host$request_uri; +//! } +//! +//! server { +//! listen 443; +//! server_name tracker.torrust.com; +//! +//! ssl_certificate CERT_PATH +//! ssl_certificate_key CERT_KEY_PATH; +//! +//! location / { +//! proxy_set_header X-Forwarded-For $remote_addr; +//! proxy_pass http://127.0.0.1:6969; +//! } +//! } +//! ``` +//! +//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! RewriteEngine on +//! RewriteCond %{HTTPS} off +//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] +//! +//! +//! +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! Order allow,deny +//! Allow from all +//! +//! +//! ProxyPreserveHost On +//! ProxyRequests Off +//! AllowEncodedSlashes NoDecode +//! +//! ProxyPass / http://localhost:3000/ +//! ProxyPassReverse / http://localhost:3000/ +//! ProxyPassReverse / http://tracker.torrust.com/ +//! +//! RequestHeader set X-Forwarded-Proto "https" +//! RequestHeader set X-Forwarded-Port "443" +//! +//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log +//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined +//! +//! SSLCertificateFile CERT_PATH +//! SSLCertificateKeyFile CERT_KEY_PATH +//! +//! +//! ``` +//! +//! ## Generating self-signed certificates +//! +//! For testing purposes, you can use self-signed certificates. +//! +//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) +//! for more information. +//! +//! Running the following command will generate a certificate (`localhost.crt`) +//! and key (`localhost.key`) file in your current directory: +//! +//! ```s +//! openssl req -x509 -out localhost.crt -keyout localhost.key \ +//! -newkey rsa:2048 -nodes -sha256 \ +//! -subj '/CN=localhost' -extensions EXT -config <( \ +//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +//! ``` +//! +//! You can then use the generated files in the configuration file: +//! +//! ```s +//! [[http_trackers]] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! +//! [http_api] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! ``` +//! +//! ## Default configuration +//! +//! The default configuration is: +//! +//! ```toml +//! log_level = "info" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/tracker/lib/database/sqlite3.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! on_reverse_proxy = false +//! external_ip = "0.0.0.0" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = false +//! max_peer_timeout = 900 +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = true +//! +//! [[udp_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:6969" +//! +//! [[http_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:7070" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! [health_check_api] +//! bind_address = "127.0.0.1:1313" +//!``` +pub mod health_check_api; +pub mod http_tracker; +pub mod tracker_api; +pub mod udp_tracker; + +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; + +use self::health_check_api::HealthCheckApi; +use self::http_tracker::HttpTracker; +use self::tracker_api::HttpApi; +use self::udp_tracker::UdpTracker; +use crate::AnnouncePolicy; + +/// Core configuration for the tracker. +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct Configuration { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + pub log_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. + pub mode: TrackerMode, + + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + pub db_path: String, + + /// See [`AnnouncePolicy::interval`] + pub announce_interval: u32, + + /// See [`AnnouncePolicy::interval_min`] + pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. + pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. + pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + pub remove_peerless_torrents: bool, + + // Server jobs configuration + /// The list of UDP trackers the tracker is running. Each UDP tracker + /// represents a UDP server that the tracker is running and it has its own + /// configuration. + pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker + /// represents a HTTP server that the tracker is running and it has its own + /// configuration. + pub http_trackers: Vec, + /// The HTTP API configuration. + pub http_api: HttpApi, + /// The Health Check API configuration. + pub health_check_api: HealthCheckApi, +} + +impl Default for Configuration { + fn default() -> Self { + let announce_policy = AnnouncePolicy::default(); + + let mut configuration = Configuration { + log_level: Option::from(String::from("info")), + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, + db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + announce_interval: announce_policy.interval, + min_announce_interval: announce_policy.interval_min, + max_peer_timeout: 900, + on_reverse_proxy: false, + external_ip: Some(String::from("0.0.0.0")), + tracker_usage_statistics: true, + persistent_torrent_completed_stat: false, + inactive_peer_cleanup_interval: 600, + remove_peerless_torrents: true, + udp_trackers: Vec::new(), + http_trackers: Vec::new(), + http_api: HttpApi { + enabled: true, + bind_address: String::from("127.0.0.1:1212"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + }, + health_check_api: HealthCheckApi { + bind_address: String::from("127.0.0.1:1313"), + }, + }; + configuration.udp_trackers.push(UdpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + }); + configuration.http_trackers.push(HttpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + }); + configuration + } +} + +#[cfg(test)] +mod tests { + use figment::providers::{Format, Toml}; + use figment::Figment; + + use crate::v1::Configuration; + + #[test] + fn configuration_should_be_loaded_from_a_toml_config_file() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "Config.toml", + r#" + log_level = "info" + mode = "public" + db_driver = "Sqlite3" + db_path = "./storage/tracker/lib/database/sqlite3.db" + announce_interval = 120 + min_announce_interval = 120 + on_reverse_proxy = false + external_ip = "0.0.0.0" + tracker_usage_statistics = true + persistent_torrent_completed_stat = false + max_peer_timeout = 900 + inactive_peer_cleanup_interval = 600 + remove_peerless_torrents = true + + [[udp_trackers]] + enabled = false + bind_address = "0.0.0.0:6969" + + [[http_trackers]] + enabled = false + bind_address = "0.0.0.0:7070" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api] + enabled = true + bind_address = "127.0.0.1:1212" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api.access_tokens] + admin = "MyAccessToken" + + [health_check_api] + bind_address = "127.0.0.1:1313" + "#, + )?; + + let figment = Figment::new().merge(Toml::file("Config.toml")); + + let config: Configuration = figment.extract()?; + + assert_eq!(config, Configuration::default()); + + Ok(()) + }); + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs new file mode 100644 index 000000000..6cda9b437 --- /dev/null +++ b/packages/configuration/src/v1/tracker_api.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +pub type AccessTokens = HashMap; + +/// Configuration for the HTTP API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpApi { + /// Weather the HTTP API is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP API will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, + /// Access tokens for the HTTP API. The key is a label identifying the + /// token and the value is the token itself. The token is used to + /// authenticate the user. All tokens are valid for all endpoints and have + /// the all permissions. + pub access_tokens: AccessTokens, +} diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs new file mode 100644 index 000000000..b304054c3 --- /dev/null +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct UdpTracker { + /// Weather the UDP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +} From da4615addabea9ffe055a410e6f484bab0f5f8c3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 12:57:14 +0100 Subject: [PATCH 151/217] refactor: reexport config versioned config types This is part of the migration to Figment in the configuration. This expose new versioned types (version 1). However, those types still used the old Config crate. Replacement by Figment has not been done yet. --- packages/configuration/src/lib.rs | 580 +------------------ packages/configuration/src/v1/mod.rs | 109 +++- packages/configuration/src/v1/tracker_api.rs | 6 + packages/test-helpers/src/configuration.rs | 2 +- src/bootstrap/app.rs | 2 +- src/servers/apis/server.rs | 2 +- src/servers/apis/v1/middlewares/auth.rs | 2 +- src/servers/udp/server.rs | 6 +- src/shared/bit_torrent/tracker/udp/client.rs | 6 +- tests/servers/api/environment.rs | 2 +- tests/servers/udp/contract.rs | 10 +- 11 files changed, 144 insertions(+), 583 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 474858f3d..948877b3b 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -3,248 +3,18 @@ //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. //! -//! The configuration is loaded from a [TOML](https://toml.io/en/) file -//! `tracker.toml` in the project root folder or from an environment variable -//! with the same content as the file. -//! -//! When you run the tracker without a configuration file, a new one will be -//! created with the default values, but the tracker immediately exits. You can -//! then edit the configuration file and run the tracker again. -//! -//! Configuration can not only be loaded from a file, but also from environment -//! variable `TORRUST_TRACKER_CONFIG`. This is useful when running the tracker -//! in a Docker container or environments where you do not have a persistent -//! storage or you cannot inject a configuration file. Refer to -//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more -//! information about how to pass configuration to the tracker. -//! -//! # Table of contents -//! -//! - [Sections](#sections) -//! - [Port binding](#port-binding) -//! - [TSL support](#tsl-support) -//! - [Generating self-signed certificates](#generating-self-signed-certificates) -//! - [Default configuration](#default-configuration) -//! -//! ## Sections -//! -//! Each section in the toml structure is mapped to a data structure. For -//! example, the `[http_api]` section (configuration for the tracker HTTP API) -//! is mapped to the [`HttpApi`] structure. -//! -//! > **NOTICE**: some sections are arrays of structures. For example, the -//! > `[[udp_trackers]]` section is an array of [`UdpTracker`] since -//! > you can have multiple running UDP trackers bound to different ports. -//! -//! Please refer to the documentation of each structure for more information -//! about each section. -//! -//! - [`Core configuration`](crate::Configuration) -//! - [`HTTP API configuration`](crate::HttpApi) -//! - [`HTTP Tracker configuration`](crate::HttpTracker) -//! - [`UDP Tracker configuration`](crate::UdpTracker) -//! -//! ## Port binding -//! -//! For the API, HTTP and UDP trackers you can bind to a random port by using -//! port `0`. For example, if you want to bind to a random port on all -//! interfaces, use `0.0.0.0:0`. The OS will choose a random port but the -//! tracker will not print the port it is listening to when it starts. It just -//! says `Starting Torrust HTTP tracker server on: http://0.0.0.0:0`. It shows -//! the port used in the configuration file, and not the port the -//! tracker is actually listening to. This is a planned feature, see issue -//! [186](https://github.com/torrust/torrust-tracker/issues/186) for more -//! information. -//! -//! ## TSL support -//! -//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to -//! `true` and setting the paths to the certificate and key files. -//! -//! Typically, you will have a directory structure like this: -//! -//! ```text -//! storage/ -//! ├── database -//! │ └── data.db -//! └── tls -//! ├── localhost.crt -//! └── localhost.key -//! ``` -//! -//! where you can store all the persistent data. -//! -//! Alternatively, you could setup a reverse proxy like Nginx or Apache to -//! handle the SSL/TLS part and forward the requests to the tracker. If you do -//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) -//! to `true` in the configuration file. It's out of scope for this -//! documentation to explain in detail how to setup a reverse proxy, but the -//! configuration file should be something like this: -//! -//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): -//! -//! ```text -//! # HTTPS only (with SSL - force redirect to HTTPS) -//! -//! server { -//! listen 80; -//! server_name tracker.torrust.com; -//! -//! return 301 https://$host$request_uri; -//! } -//! -//! server { -//! listen 443; -//! server_name tracker.torrust.com; -//! -//! ssl_certificate CERT_PATH -//! ssl_certificate_key CERT_KEY_PATH; -//! -//! location / { -//! proxy_set_header X-Forwarded-For $remote_addr; -//! proxy_pass http://127.0.0.1:6969; -//! } -//! } -//! ``` -//! -//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): -//! -//! ```text -//! # HTTPS only (with SSL - force redirect to HTTPS) -//! -//! -//! ServerAdmin webmaster@tracker.torrust.com -//! ServerName tracker.torrust.com -//! -//! -//! RewriteEngine on -//! RewriteCond %{HTTPS} off -//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] -//! -//! -//! -//! -//! -//! ServerAdmin webmaster@tracker.torrust.com -//! ServerName tracker.torrust.com -//! -//! -//! Order allow,deny -//! Allow from all -//! -//! -//! ProxyPreserveHost On -//! ProxyRequests Off -//! AllowEncodedSlashes NoDecode -//! -//! ProxyPass / http://localhost:3000/ -//! ProxyPassReverse / http://localhost:3000/ -//! ProxyPassReverse / http://tracker.torrust.com/ -//! -//! RequestHeader set X-Forwarded-Proto "https" -//! RequestHeader set X-Forwarded-Port "443" -//! -//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log -//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined -//! -//! SSLCertificateFile CERT_PATH -//! SSLCertificateKeyFile CERT_KEY_PATH -//! -//! -//! ``` -//! -//! ## Generating self-signed certificates -//! -//! For testing purposes, you can use self-signed certificates. -//! -//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) -//! for more information. -//! -//! Running the following command will generate a certificate (`localhost.crt`) -//! and key (`localhost.key`) file in your current directory: -//! -//! ```s -//! openssl req -x509 -out localhost.crt -keyout localhost.key \ -//! -newkey rsa:2048 -nodes -sha256 \ -//! -subj '/CN=localhost' -extensions EXT -config <( \ -//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") -//! ``` -//! -//! You can then use the generated files in the configuration file: -//! -//! ```s -//! [[http_trackers]] -//! enabled = true -//! ... -//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" -//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -//! -//! [http_api] -//! enabled = true -//! ... -//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" -//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -//! ``` -//! -//! ## Default configuration -//! -//! The default configuration is: -//! -//! ```toml -//! announce_interval = 120 -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" -//! external_ip = "0.0.0.0" -//! inactive_peer_cleanup_interval = 600 -//! log_level = "info" -//! max_peer_timeout = 900 -//! min_announce_interval = 120 -//! mode = "public" -//! on_reverse_proxy = false -//! persistent_torrent_completed_stat = false -//! remove_peerless_torrents = true -//! tracker_usage_statistics = true -//! -//! [[udp_trackers]] -//! bind_address = "0.0.0.0:6969" -//! enabled = false -//! -//! [[http_trackers]] -//! bind_address = "0.0.0.0:7070" -//! enabled = false -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api] -//! bind_address = "127.0.0.1:1212" -//! enabled = true -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api.access_tokens] -//! admin = "MyAccessToken" -//! -//! [health_check_api] -//! bind_address = "127.0.0.1:1313" -//!``` +//! The current version for configuration is [`v1`](crate::v1). pub mod v1; use std::collections::HashMap; -use std::net::IpAddr; -use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use std::{env, fs}; -use config::{Config, ConfigError, File, FileFormat}; -use derive_more::{AsMut, AsRef, Constructor, Display}; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use config::ConfigError; +use derive_more::Constructor; use thiserror::Error; use torrust_tracker_located_error::{DynError, Located, LocatedError}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; @@ -252,15 +22,19 @@ pub const TORRENT_PEERS_LIMIT: usize = 74; /// Client Timeout pub const CLIENT_TIMEOUT_DEFAULT: Duration = Duration::from_secs(5); -/// The maximum number of bytes in a UDP packet. -pub const MAX_PACKET_SIZE: usize = 1496; - /// The a free port is dynamically chosen by the operating system. pub const PORT_ASSIGNED_BY_OS: u16 = 0; -/// A magic 64-bit integer constant defined in the protocol that is used to -/// identify the protocol. -pub const PROTOCOL_ID: i64 = 0x0417_2710_1980; +/// The maximum number of bytes in a UDP packet. +pub const UDP_MAX_PACKET_SIZE: usize = 1496; + +pub type Configuration = v1::Configuration; +pub type UdpTracker = v1::udp_tracker::UdpTracker; +pub type HttpTracker = v1::http_tracker::HttpTracker; +pub type HttpApi = v1::tracker_api::HttpApi; +pub type HealthCheckApi = v1::health_check_api::HealthCheckApi; + +pub type AccessTokens = HashMap; #[derive(Copy, Clone, Debug, PartialEq, Constructor)] pub struct TrackerPolicy { @@ -321,99 +95,6 @@ impl Info { } } -/// Configuration for each UDP tracker. -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct UdpTracker { - /// Weather the UDP tracker is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, -} - -/// Configuration for each HTTP tracker. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HttpTracker { - /// Weather the HTTP tracker is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, - /// Weather the HTTP tracker will use SSL or not. - pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, -} - -#[derive(Constructor, Serialize, Deserialize, AsRef, AsMut, PartialEq, Eq, Clone, Display)] -#[display(fmt = "AccessTokens (values hidden): {tokens:?}")] -pub struct AccessTokens { - tokens: HashMap, -} - -impl std::fmt::Debug for AccessTokens { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let keys: Vec = self.tokens.keys().cloned().collect(); - f.debug_struct("AccessTokens") - .field("tokens keys", &keys) - .finish_non_exhaustive() - } -} - -/// Configuration for the HTTP API. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HttpApi { - /// Weather the HTTP API is enabled or not. - pub enabled: bool, - /// The address the tracker will bind to. - /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, - /// Weather the HTTP API will use SSL or not. - pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, - /// Access tokens for the HTTP API. The key is a label identifying the - /// token and the value is the token itself. The token is used to - /// authenticate the user. All tokens are valid for all endpoints and have - /// the all permissions. - pub access_tokens: AccessTokens, -} - -impl HttpApi { - fn override_admin_token(&mut self, api_admin_token: &str) { - self.access_tokens - .as_mut() - .insert("admin".to_string(), api_admin_token.to_string()); - } -} - -/// Configuration for the Health Check API. -#[serde_as] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] -pub struct HealthCheckApi { - /// The address the API will bind to. - /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to - /// listen to all interfaces, use `0.0.0.0`. If you want the operating - /// system to choose a random port, use port `0`. - pub bind_address: String, -} - /// Announce policy #[derive(PartialEq, Eq, Debug, Clone, Copy, Constructor)] pub struct AnnouncePolicy { @@ -453,84 +134,6 @@ impl Default for AnnouncePolicy { } } -/// Core configuration for the tracker. -#[allow(clippy::struct_excessive_bools)] -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] -pub struct Configuration { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - #[serde(alias = "log_level")] - pub trace_level: Option, - - /// Tracker mode. See [`TrackerMode`] for more information. - pub mode: TrackerMode, - - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - pub db_driver: DatabaseDriver, - - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - pub db_path: String, - - /// See [`AnnouncePolicy::interval`] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - pub on_reverse_proxy: bool, - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. - /// If enabled, the tracker will collect statistics like the number of - /// connections handled, the number of announce requests handled, etc. - /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more - /// information about the collected metrics. - pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive - /// peers from the torrent peer list. - pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - pub remove_peerless_torrents: bool, - - // Server jobs configuration - /// The list of UDP trackers the tracker is running. Each UDP tracker - /// represents a UDP server that the tracker is running and it has its own - /// configuration. - pub udp_trackers: Vec, - /// The list of HTTP trackers the tracker is running. Each HTTP tracker - /// represents a HTTP server that the tracker is running and it has its own - /// configuration. - pub http_trackers: Vec, - /// The HTTP API configuration. - pub http_api: HttpApi, - /// The Health Check API configuration. - pub health_check_api: HealthCheckApi, -} - /// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { @@ -564,149 +167,6 @@ impl From for Error { } } -impl Default for Configuration { - fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - - let mut configuration = Configuration { - trace_level: Option::from(String::from("info")), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(String::from("0.0.0.0")), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, - udp_trackers: Vec::new(), - http_trackers: Vec::new(), - http_api: HttpApi { - enabled: true, - bind_address: String::from("127.0.0.1:1212"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - access_tokens: AccessTokens::new( - [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), - ), - }, - health_check_api: HealthCheckApi { - bind_address: String::from("127.0.0.1:1313"), - }, - }; - configuration.udp_trackers.push(UdpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:6969"), - }); - configuration.http_trackers.push(HttpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:7070"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - }); - configuration - } -} - -impl Configuration { - fn override_api_admin_token(&mut self, api_admin_token: &str) { - self.http_api.override_admin_token(api_admin_token); - } - - /// Returns the tracker public IP address id defined in the configuration, - /// and `None` otherwise. - #[must_use] - pub fn get_ext_ip(&self) -> Option { - match &self.external_ip { - None => None, - Some(external_ip) => match IpAddr::from_str(external_ip) { - Ok(external_ip) => Some(external_ip), - Err(_) => None, - }, - } - } - - /// Loads the configuration from the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `path` does not exist or has a bad configuration. - pub fn load_from_file(path: &str) -> Result { - let config_builder = Config::builder(); - - #[allow(unused_assignments)] - let mut config = Config::default(); - - config = config_builder.add_source(File::with_name(path)).build()?; - - let torrust_config: Configuration = config.try_deserialize()?; - - Ok(torrust_config) - } - - /// Saves the default configuration at the given path. - /// - /// # Errors - /// - /// Will return `Err` if `path` is not a valid path or the configuration - /// file cannot be created. - pub fn create_default_configuration_file(path: &str) -> Result { - let config = Configuration::default(); - config.save_to_file(path)?; - Ok(config) - } - - /// Loads the configuration from the `Info` struct. The whole - /// configuration in toml format is included in the `info.tracker_toml` string. - /// - /// Optionally will override the admin api token. - /// - /// # Errors - /// - /// Will return `Err` if the environment variable does not exist or has a bad configuration. - pub fn load(info: &Info) -> Result { - let config_builder = Config::builder() - .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) - .build()?; - let mut config: Configuration = config_builder.try_deserialize()?; - - if let Some(ref token) = info.api_admin_token { - config.override_api_admin_token(token); - }; - - Ok(config) - } - - /// Saves the configuration to the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `filename` does not exist or the user does not have - /// permission to read it. Will also return `Err` if the configuration is - /// not valid or cannot be encoded to TOML. - /// - /// # Panics - /// - /// Will panic if the configuration cannot be written into the file. - pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - fs::write(path, self.to_toml()).expect("Could not write to file!"); - Ok(()) - } - - /// Encodes the configuration to TOML. - fn to_toml(&self) -> String { - toml::to_string(self).expect("Could not encode TOML value") - } -} - #[cfg(test)] mod tests { use crate::Configuration; @@ -835,17 +295,7 @@ mod tests { fn http_api_configuration_should_check_if_it_contains_a_token() { let configuration = Configuration::default(); - assert!(configuration - .http_api - .access_tokens - .as_ref() - .values() - .any(|t| t == "MyAccessToken")); - assert!(!configuration - .http_api - .access_tokens - .as_ref() - .values() - .any(|t| t == "NonExistingToken")); + assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken")); + assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken")); } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 815d74e40..486d2c300 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -1,4 +1,5 @@ -//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! Version `1` for [Torrust Tracker](https://docs.rs/torrust-tracker) +//! configuration data structures. //! //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. @@ -234,6 +235,11 @@ pub mod http_tracker; pub mod tracker_api; pub mod udp_tracker; +use std::fs; +use std::net::IpAddr; +use std::str::FromStr; + +use config::{Config, File, FileFormat}; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -241,7 +247,7 @@ use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::AnnouncePolicy; +use crate::{AnnouncePolicy, Error, Info}; /// Core configuration for the tracker. #[allow(clippy::struct_excessive_bools)] @@ -368,6 +374,105 @@ impl Default for Configuration { } } +impl Configuration { + fn override_api_admin_token(&mut self, api_admin_token: &str) { + self.http_api.override_admin_token(api_admin_token); + } + + /// Returns the tracker public IP address id defined in the configuration, + /// and `None` otherwise. + #[must_use] + pub fn get_ext_ip(&self) -> Option { + match &self.external_ip { + None => None, + Some(external_ip) => match IpAddr::from_str(external_ip) { + Ok(external_ip) => Some(external_ip), + Err(_) => None, + }, + } + } + + /// Loads the configuration from the configuration file. + /// + /// # Errors + /// + /// Will return `Err` if `path` does not exist or has a bad configuration. + pub fn load_from_file(path: &str) -> Result { + // todo: use Figment + + let config_builder = Config::builder(); + + #[allow(unused_assignments)] + let mut config = Config::default(); + + config = config_builder.add_source(File::with_name(path)).build()?; + + let torrust_config: Configuration = config.try_deserialize()?; + + Ok(torrust_config) + } + + /// Saves the default configuration at the given path. + /// + /// # Errors + /// + /// Will return `Err` if `path` is not a valid path or the configuration + /// file cannot be created. + pub fn create_default_configuration_file(path: &str) -> Result { + // todo: use Figment + + let config = Configuration::default(); + config.save_to_file(path)?; + Ok(config) + } + + /// Loads the configuration from the `Info` struct. The whole + /// configuration in toml format is included in the `info.tracker_toml` string. + /// + /// Optionally will override the admin api token. + /// + /// # Errors + /// + /// Will return `Err` if the environment variable does not exist or has a bad configuration. + pub fn load(info: &Info) -> Result { + // todo: use Figment + + let config_builder = Config::builder() + .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) + .build()?; + let mut config: Configuration = config_builder.try_deserialize()?; + + if let Some(ref token) = info.api_admin_token { + config.override_api_admin_token(token); + }; + + Ok(config) + } + + /// Saves the configuration to the configuration file. + /// + /// # Errors + /// + /// Will return `Err` if `filename` does not exist or the user does not have + /// permission to read it. Will also return `Err` if the configuration is + /// not valid or cannot be encoded to TOML. + /// + /// # Panics + /// + /// Will panic if the configuration cannot be written into the file. + pub fn save_to_file(&self, path: &str) -> Result<(), Error> { + // todo: use Figment + + fs::write(path, self.to_toml()).expect("Could not write to file!"); + Ok(()) + } + + /// Encodes the configuration to TOML. + fn to_toml(&self) -> String { + toml::to_string(self).expect("Could not encode TOML value") + } +} + #[cfg(test)] mod tests { use figment::providers::{Format, Toml}; diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 6cda9b437..51f11a14d 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -30,3 +30,9 @@ pub struct HttpApi { /// the all permissions. pub access_tokens: AccessTokens, } + +impl HttpApi { + pub fn override_admin_token(&mut self, api_admin_token: &str) { + self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); + } +} diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 42812923b..49cfdd390 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -28,7 +28,7 @@ pub fn ephemeral() -> Configuration { // For example: a test for the UDP tracker should disable the API and HTTP tracker. let mut config = Configuration { - trace_level: Some("off".to_owned()), // Change to `debug` for tests debugging + log_level: Some("off".to_owned()), // Change to `debug` for tests debugging ..Default::default() }; diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index 40ed4d0dd..dce460bc9 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -32,7 +32,7 @@ pub fn config() -> (Configuration, Level) { let config = initialize_configuration(); let level: Level = config - .trace_level + .log_level .as_deref() .unwrap_or("info") .parse() diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index bface9d13..39562472d 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -132,7 +132,7 @@ impl Drop for ApiHandle { } #[derive(Constructor, Clone, Debug, Display)] -#[display(fmt = "intended_address: {addr}, with tracker, {access_tokens}, and {}", "self.have_tls()")] +#[display(fmt = "intended_address: {addr}, with tracker, tokens, and {}", "self.have_tls()")] pub struct ApiLauncher { pub tracker: Arc, pub access_tokens: Arc, diff --git a/src/servers/apis/v1/middlewares/auth.rs b/src/servers/apis/v1/middlewares/auth.rs index f2ed7f0f8..58219c7ca 100644 --- a/src/servers/apis/v1/middlewares/auth.rs +++ b/src/servers/apis/v1/middlewares/auth.rs @@ -79,7 +79,7 @@ impl IntoResponse for AuthError { } fn authenticate(token: &str, tokens: &AccessTokens) -> bool { - tokens.as_ref().values().any(|t| t == token) + tokens.values().any(|t| t == token) } /// `500` error response returned when the token is missing. diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 7b8d3d0c9..b3fb597c4 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -27,7 +27,7 @@ use ringbuf::StaticRb; use tokio::net::UdpSocket; use tokio::select; use tokio::task::AbortHandle; -use torrust_tracker_configuration::MAX_PACKET_SIZE; +use torrust_tracker_configuration::UDP_MAX_PACKET_SIZE; use tracing::{debug, error, info, instrument, trace, warn, Level}; use super::v0::UdpRequest; @@ -260,7 +260,7 @@ impl Udp { #[instrument(skip(self))] async fn receive_request(&self) -> Result { - let mut payload = Vec::with_capacity(MAX_PACKET_SIZE); + let mut payload = Vec::with_capacity(UDP_MAX_PACKET_SIZE); let (len, from) = self .socket @@ -328,7 +328,7 @@ async fn fulfil_request(tracker: Arc, socket: Arc, addr: Soc #[instrument] fn get_response_payload(response: &aquatic_udp_protocol::Response) -> Vec { - let buffer = vec![0u8; MAX_PACKET_SIZE]; + let buffer = vec![0u8; UDP_MAX_PACKET_SIZE]; let mut cursor = std::io::Cursor::new(buffer); let () = response diff --git a/src/shared/bit_torrent/tracker/udp/client.rs b/src/shared/bit_torrent/tracker/udp/client.rs index 729ff0451..7f6997e9e 100644 --- a/src/shared/bit_torrent/tracker/udp/client.rs +++ b/src/shared/bit_torrent/tracker/udp/client.rs @@ -7,7 +7,7 @@ use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Request, Response, T use derive_more::{AsRef, Constructor, From, Into}; use tokio::net::UdpSocket; use tokio::time; -use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE, PORT_ASSIGNED_BY_OS}; +use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, PORT_ASSIGNED_BY_OS, UDP_MAX_PACKET_SIZE}; use tracing::debug; use super::{source_address, Error}; @@ -200,7 +200,7 @@ impl Client { debug!(target: "UDP tracker client", "send request {request:?}"); // Write request into a buffer - let request_buffer = vec![0u8; MAX_PACKET_SIZE]; + let request_buffer = vec![0u8; UDP_MAX_PACKET_SIZE]; let mut cursor = Cursor::new(request_buffer); request @@ -222,7 +222,7 @@ impl Client { /// /// Will error if can't create response from the received payload (bytes buffer). pub async fn receive_response(&self) -> Result { - let mut response_buffer = [0u8; MAX_PACKET_SIZE]; + let mut response_buffer = [0u8; UDP_MAX_PACKET_SIZE]; let payload_size = self.receive(&mut response_buffer).await?; diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index f19332dce..0c2b69ac2 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -99,7 +99,7 @@ impl Environment> { pub fn get_connection_info(&self) -> ConnectionInfo { ConnectionInfo { bind_address: self.bind_address().to_string(), - api_token: self.config.access_tokens.as_ref().get("admin").cloned(), + api_token: self.config.access_tokens.get("admin").cloned(), } } } diff --git a/tests/servers/udp/contract.rs b/tests/servers/udp/contract.rs index 10b090725..a5973f99a 100644 --- a/tests/servers/udp/contract.rs +++ b/tests/servers/udp/contract.rs @@ -5,18 +5,18 @@ use aquatic_udp_protocol::Response; use torrust_tracker::shared::bit_torrent::tracker::udp::Client; -use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, MAX_PACKET_SIZE}; +use torrust_tracker_configuration::{CLIENT_TIMEOUT_DEFAULT, UDP_MAX_PACKET_SIZE}; use torrust_tracker_test_helpers::configuration; use crate::common::udp::Started; use crate::servers::udp::asserts::is_error_response; -fn empty_udp_request() -> [u8; MAX_PACKET_SIZE] { - [0; MAX_PACKET_SIZE] +fn empty_udp_request() -> [u8; UDP_MAX_PACKET_SIZE] { + [0; UDP_MAX_PACKET_SIZE] } -fn empty_buffer() -> [u8; MAX_PACKET_SIZE] { - [0; MAX_PACKET_SIZE] +fn empty_buffer() -> [u8; UDP_MAX_PACKET_SIZE] { + [0; UDP_MAX_PACKET_SIZE] } #[tokio::test] From 2ef25f43816a1c4edddb2714f0e6baf7a3b98c69 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 13:54:07 +0100 Subject: [PATCH 152/217] refactor: replace Config by Figment in Configuration implementation This replaces the crate `config` with `figment` in the Configuration implementation. --- packages/configuration/src/lib.rs | 13 +++++++------ packages/configuration/src/v1/mod.rs | 28 ++++++++-------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 948877b3b..b725a258e 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -11,10 +11,9 @@ use std::sync::Arc; use std::time::Duration; use std::{env, fs}; -use config::ConfigError; use derive_more::Constructor; use thiserror::Error; -use torrust_tracker_located_error::{DynError, Located, LocatedError}; +use torrust_tracker_located_error::{DynError, LocatedError}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; @@ -152,17 +151,19 @@ pub enum Error { /// Unable to load the configuration from the configuration file. #[error("Failed processing the configuration: {source}")] - ConfigError { source: LocatedError<'static, ConfigError> }, + ConfigError { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, #[error("The error for errors that can never happen.")] Infallible, } -impl From for Error { +impl From for Error { #[track_caller] - fn from(err: ConfigError) -> Self { + fn from(err: figment::Error) -> Self { Self::ConfigError { - source: Located(err).into(), + source: (Arc::new(err) as DynError).into(), } } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 486d2c300..6c044c462 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,8 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use config::{Config, File, FileFormat}; +use figment::providers::{Format, Toml}; +use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -398,18 +399,11 @@ impl Configuration { /// /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { - // todo: use Figment + let figment = Figment::new().merge(Toml::file(path)); - let config_builder = Config::builder(); + let config: Configuration = figment.extract()?; - #[allow(unused_assignments)] - let mut config = Config::default(); - - config = config_builder.add_source(File::with_name(path)).build()?; - - let torrust_config: Configuration = config.try_deserialize()?; - - Ok(torrust_config) + Ok(config) } /// Saves the default configuration at the given path. @@ -419,8 +413,6 @@ impl Configuration { /// Will return `Err` if `path` is not a valid path or the configuration /// file cannot be created. pub fn create_default_configuration_file(path: &str) -> Result { - // todo: use Figment - let config = Configuration::default(); config.save_to_file(path)?; Ok(config) @@ -435,12 +427,9 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - // todo: use Figment + let figment = Figment::new().merge(Toml::string(&info.tracker_toml)); - let config_builder = Config::builder() - .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) - .build()?; - let mut config: Configuration = config_builder.try_deserialize()?; + let mut config: Configuration = figment.extract()?; if let Some(ref token) = info.api_admin_token { config.override_api_admin_token(token); @@ -461,14 +450,13 @@ impl Configuration { /// /// Will panic if the configuration cannot be written into the file. pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - // todo: use Figment - fs::write(path, self.to_toml()).expect("Could not write to file!"); Ok(()) } /// Encodes the configuration to TOML. fn to_toml(&self) -> String { + // code-review: do we need to use Figment also to serialize into toml? toml::to_string(self).expect("Could not encode TOML value") } } From 867b5830508be93a9e0e19f1a6e410beb247901b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 15:36:27 +0100 Subject: [PATCH 153/217] chore: remove unused config dependenciy It was replaced by `figment`. --- Cargo.lock | 195 +----------------------------- Cargo.toml | 3 +- packages/configuration/Cargo.toml | 1 - 3 files changed, 2 insertions(+), 197 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e97418e74..cd6416a96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,9 +426,6 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -dependencies = [ - "serde", -] [[package]] name = "bitvec" @@ -714,61 +711,12 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -987,7 +935,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1015,15 +963,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "downcast" version = "0.11.0" @@ -1723,17 +1662,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1857,12 +1785,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2254,16 +2176,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list", - "hashbrown 0.13.2", -] - [[package]] name = "overload" version = "0.1.1" @@ -2293,12 +2205,6 @@ dependencies = [ "windows-targets 0.52.5", ] -[[package]] -name = "pathdiff" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" - [[package]] name = "pear" version = "0.2.9" @@ -2338,51 +2244,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "pest_meta" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.11.2" @@ -2867,18 +2728,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.5.0", - "serde", - "serde_derive", -] - [[package]] name = "rstest" version = "0.21.0" @@ -2923,16 +2772,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rust_decimal" version = "1.35.0" @@ -3571,15 +3410,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -3748,7 +3578,6 @@ dependencies = [ "axum-server", "chrono", "clap", - "config", "crossbeam-skiplist", "dashmap", "derive_more", @@ -3805,7 +3634,6 @@ dependencies = [ name = "torrust-tracker-configuration" version = "3.0.0-alpha.12-develop" dependencies = [ - "config", "derive_more", "figment", "serde", @@ -4001,12 +3829,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "uncased" version = "0.9.10" @@ -4037,12 +3859,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - [[package]] name = "untrusted" version = "0.9.0" @@ -4411,15 +4227,6 @@ dependencies = [ "tap", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 25c990bff..b92dc7193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } -config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" @@ -80,7 +79,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["crossbeam-skiplist", "dashmap", "parking_lot", "serde_bytes"] +ignored = ["crossbeam-skiplist", "dashmap", "figment", "parking_lot", "serde_bytes"] [dev-dependencies] local-ip-address = "0" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index e5335d416..a033dcea1 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,7 +15,6 @@ rust-version.workspace = true version.workspace = true [dependencies] -config = "0" derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } From 35bf7ef2fae5a7bb564e15730b73c7f5ba73c06b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 16:38:09 +0100 Subject: [PATCH 154/217] feat: enable overwrite Configuration values using env vars Enable Figment ability to overwrite all config options with env vars. We are currently overwriting only this value: ```toml [http_api.access_tokens] admin = "MyAccessToken" ``` With the env var `TORRUST_TRACKER_API_ADMIN_TOKEN`. The name we gave to the env var does nto follow Figment convention which is `TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN`. We have to keep both options until we remove the old one in the rest of the code. --- packages/configuration/src/v1/mod.rs | 138 ++++++++++++++++++--------- 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 6c044c462..562eb569e 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,7 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use figment::providers::{Format, Toml}; +use figment::providers::{Env, Format, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -400,6 +400,16 @@ impl Configuration { /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { let figment = Figment::new().merge(Toml::file(path)); + //.merge(Env::prefixed("TORRUST_TRACKER_")); + + // code-review: merging values from env vars makes the test + // "configuration_should_be_loaded_from_a_toml_config_file" fail. + // + // It's because this line in a new test: + // + // jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + // + // It seems env vars are shared between tests. let config: Configuration = figment.extract()?; @@ -427,7 +437,9 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::new().merge(Toml::string(&info.tracker_toml)); + let figment = Figment::new() + .merge(Toml::string(&info.tracker_toml)) + .merge(Env::prefixed("TORRUST_TRACKER_")); let mut config: Configuration = figment.extract()?; @@ -463,58 +475,67 @@ impl Configuration { #[cfg(test)] mod tests { - use figment::providers::{Format, Toml}; + use figment::providers::{Env, Format, Toml}; use figment::Figment; use crate::v1::Configuration; + #[cfg(test)] + fn default_config_toml() -> String { + let config = r#"log_level = "info" + mode = "public" + db_driver = "Sqlite3" + db_path = "./storage/tracker/lib/database/sqlite3.db" + announce_interval = 120 + min_announce_interval = 120 + on_reverse_proxy = false + external_ip = "0.0.0.0" + tracker_usage_statistics = true + persistent_torrent_completed_stat = false + max_peer_timeout = 900 + inactive_peer_cleanup_interval = 600 + remove_peerless_torrents = true + + [[udp_trackers]] + enabled = false + bind_address = "0.0.0.0:6969" + + [[http_trackers]] + enabled = false + bind_address = "0.0.0.0:7070" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api] + enabled = true + bind_address = "127.0.0.1:1212" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api.access_tokens] + admin = "MyAccessToken" + + [health_check_api] + bind_address = "127.0.0.1:1313" + "# + .lines() + .map(str::trim_start) + .collect::>() + .join("\n"); + config + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|jail| { - jail.create_file( - "Config.toml", - r#" - log_level = "info" - mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" - announce_interval = 120 - min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" - tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 - inactive_peer_cleanup_interval = 600 - remove_peerless_torrents = true - - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api.access_tokens] - admin = "MyAccessToken" - - [health_check_api] - bind_address = "127.0.0.1:1313" - "#, - )?; - - let figment = Figment::new().merge(Toml::file("Config.toml")); + jail.create_file("Config.toml", &default_config_toml())?; + + // todo: replace with Configuration method + let figment = Figment::new() + .merge(Toml::file("Config.toml")) + .merge(Env::prefixed("TORRUST_TRACKER_")); let config: Configuration = figment.extract()?; @@ -523,4 +544,27 @@ mod tests { Ok(()) }); } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { + figment::Jail::expect_with(|jail| { + jail.create_file("Config.toml", &default_config_toml())?; + + jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + + // todo: replace with Configuration method + let figment = Figment::new() + .merge(Toml::file("Config.toml")) + .merge(Env::prefixed("TORRUST_TRACKER_")); + + let config: Configuration = figment.extract()?; + + assert_eq!( + config.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } } From 3a332614cdd6918c1b94528d5c33e78345ba9732 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 17:06:06 +0100 Subject: [PATCH 155/217] refactor: move Configuration unit test to inner mods --- packages/configuration/src/lib.rs | 133 ------------------- packages/configuration/src/v1/mod.rs | 91 +++++++------ packages/configuration/src/v1/tracker_api.rs | 29 ++++ 3 files changed, 80 insertions(+), 173 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index b725a258e..5977e030e 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -167,136 +167,3 @@ impl From for Error { } } } - -#[cfg(test)] -mod tests { - use crate::Configuration; - - #[cfg(test)] - fn default_config_toml() -> String { - let config = r#"log_level = "info" - mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" - announce_interval = 120 - min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" - tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 - inactive_peer_cleanup_interval = 600 - remove_peerless_torrents = true - - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - - [http_api.access_tokens] - admin = "MyAccessToken" - - [health_check_api] - bind_address = "127.0.0.1:1313" - "# - .lines() - .map(str::trim_start) - .collect::>() - .join("\n"); - config - } - - #[test] - fn configuration_should_have_default_values() { - let configuration = Configuration::default(); - - let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); - - assert_eq!(toml, default_config_toml()); - } - - #[test] - fn configuration_should_contain_the_external_ip() { - let configuration = Configuration::default(); - - assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); - } - - #[test] - fn configuration_should_be_saved_in_a_toml_config_file() { - use std::{env, fs}; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::save_to_file - let config_file_path = temp_file; - let path = config_file_path.to_string_lossy().to_string(); - - let default_configuration = Configuration::default(); - - default_configuration - .save_to_file(&path) - .expect("Could not save configuration to file"); - - let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); - - assert_eq!(contents, default_config_toml()); - } - - #[cfg(test)] - fn create_temp_config_file_with_default_config() -> String { - use std::env; - use std::fs::File; - use std::io::Write; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::load_from_file - let config_file_path = temp_file.clone(); - let path = config_file_path.to_string_lossy().to_string(); - - // Write file contents - let mut file = File::create(temp_file).unwrap(); - writeln!(&mut file, "{}", default_config_toml()).unwrap(); - - path - } - - #[test] - fn configuration_should_be_loaded_from_a_toml_config_file() { - let config_file_path = create_temp_config_file_with_default_config(); - - let configuration = Configuration::load_from_file(&config_file_path).expect("Could not load configuration from file"); - - assert_eq!(configuration, Configuration::default()); - } - - #[test] - fn http_api_configuration_should_check_if_it_contains_a_token() { - let configuration = Configuration::default(); - - assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken")); - assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken")); - } -} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 562eb569e..4413ed7c4 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -345,17 +345,7 @@ impl Default for Configuration { remove_peerless_torrents: true, udp_trackers: Vec::new(), http_trackers: Vec::new(), - http_api: HttpApi { - enabled: true, - bind_address: String::from("127.0.0.1:1212"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), - }, + http_api: HttpApi::default(), health_check_api: HealthCheckApi { bind_address: String::from("127.0.0.1:1313"), }, @@ -399,17 +389,9 @@ impl Configuration { /// /// Will return `Err` if `path` does not exist or has a bad configuration. pub fn load_from_file(path: &str) -> Result { - let figment = Figment::new().merge(Toml::file(path)); - //.merge(Env::prefixed("TORRUST_TRACKER_")); - - // code-review: merging values from env vars makes the test - // "configuration_should_be_loaded_from_a_toml_config_file" fail. - // - // It's because this line in a new test: - // - // jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - // - // It seems env vars are shared between tests. + let figment = Figment::new() + .merge(Toml::file(path)) + .merge(Env::prefixed("TORRUST_TRACKER_")); let config: Configuration = figment.extract()?; @@ -475,8 +457,6 @@ impl Configuration { #[cfg(test)] mod tests { - use figment::providers::{Env, Format, Toml}; - use figment::Figment; use crate::v1::Configuration; @@ -527,19 +507,55 @@ mod tests { config } + #[test] + fn configuration_should_have_default_values() { + let configuration = Configuration::default(); + + let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); + + assert_eq!(toml, default_config_toml()); + } + + #[test] + fn configuration_should_contain_the_external_ip() { + let configuration = Configuration::default(); + + assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); + } + + #[test] + fn configuration_should_be_saved_in_a_toml_config_file() { + use std::{env, fs}; + + use uuid::Uuid; + + // Build temp config file path + let temp_directory = env::temp_dir(); + let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); + + // Convert to argument type for Configuration::save_to_file + let config_file_path = temp_file; + let path = config_file_path.to_string_lossy().to_string(); + + let default_configuration = Configuration::default(); + + default_configuration + .save_to_file(&path) + .expect("Could not save configuration to file"); + + let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); + + assert_eq!(contents, default_config_toml()); + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|jail| { - jail.create_file("Config.toml", &default_config_toml())?; - - // todo: replace with Configuration method - let figment = Figment::new() - .merge(Toml::file("Config.toml")) - .merge(Env::prefixed("TORRUST_TRACKER_")); + jail.create_file("tracker.toml", &default_config_toml())?; - let config: Configuration = figment.extract()?; + let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); - assert_eq!(config, Configuration::default()); + assert_eq!(configuration, Configuration::default()); Ok(()) }); @@ -548,19 +564,14 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.create_file("Config.toml", &default_config_toml())?; + jail.create_file("tracker.toml", &default_config_toml())?; jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - // todo: replace with Configuration method - let figment = Figment::new() - .merge(Toml::file("Config.toml")) - .merge(Env::prefixed("TORRUST_TRACKER_")); - - let config: Configuration = figment.extract()?; + let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); assert_eq!( - config.http_api.access_tokens.get("admin"), + configuration.http_api.access_tokens.get("admin"), Some("NewToken".to_owned()).as_ref() ); diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 51f11a14d..8749478c8 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -31,8 +31,37 @@ pub struct HttpApi { pub access_tokens: AccessTokens, } +impl Default for HttpApi { + fn default() -> Self { + Self { + enabled: true, + bind_address: String::from("127.0.0.1:1212"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + } + } +} + impl HttpApi { pub fn override_admin_token(&mut self, api_admin_token: &str) { self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); } } + +#[cfg(test)] +mod tests { + use crate::v1::tracker_api::HttpApi; + + #[test] + fn http_api_configuration_should_check_if_it_contains_a_token() { + let configuration = HttpApi::default(); + + assert!(configuration.access_tokens.values().any(|t| t == "MyAccessToken")); + assert!(!configuration.access_tokens.values().any(|t| t == "NonExistingToken")); + } +} From 6baa7fd2468a43873fbffaea651bcee0420aeae2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 8 May 2024 18:01:23 +0100 Subject: [PATCH 156/217] refactor!: remove unused method in Configuration --- packages/configuration/src/v1/mod.rs | 34 +++++++++++----------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 4413ed7c4..dbb6eb7c0 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -383,21 +383,6 @@ impl Configuration { } } - /// Loads the configuration from the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `path` does not exist or has a bad configuration. - pub fn load_from_file(path: &str) -> Result { - let figment = Figment::new() - .merge(Toml::file(path)) - .merge(Env::prefixed("TORRUST_TRACKER_")); - - let config: Configuration = figment.extract()?; - - Ok(config) - } - /// Saves the default configuration at the given path. /// /// # Errors @@ -459,6 +444,7 @@ impl Configuration { mod tests { use crate::v1::Configuration; + use crate::Info; #[cfg(test)] fn default_config_toml() -> String { @@ -550,10 +536,13 @@ mod tests { #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { - figment::Jail::expect_with(|jail| { - jail.create_file("tracker.toml", &default_config_toml())?; + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; - let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!(configuration, Configuration::default()); @@ -564,11 +553,14 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.create_file("tracker.toml", &default_config_toml())?; - jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); - let configuration = Configuration::load_from_file("tracker.toml").expect("Could not load configuration from file"); + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!( configuration.http_api.access_tokens.get("admin"), From a690bb26a4b8d5bc68ae63b23420ee1b4f7d97b8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 08:15:35 +0100 Subject: [PATCH 157/217] feat: use double underscore to split config env var names For example, the env var `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` would be the config option: ``` [http_api.access_tokens] admin = "MyAccessToken" ``` It uses `__` double underscore becuase dots are not allowed in Bash names. See: https://www.gnu.org/software/bash/manual/bash.html#Definitions ``` name A word consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. Names are used as shell variable and function names. Also referred to as an identifier. ``` --- packages/configuration/src/v1/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index dbb6eb7c0..88b9565bd 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -406,7 +406,7 @@ impl Configuration { pub fn load(info: &Info) -> Result { let figment = Figment::new() .merge(Toml::string(&info.tracker_toml)) - .merge(Env::prefixed("TORRUST_TRACKER_")); + .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); let mut config: Configuration = figment.extract()?; @@ -553,7 +553,7 @@ mod tests { #[test] fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { figment::Jail::expect_with(|jail| { - jail.set_env("TORRUST_TRACKER_HTTP_API.ACCESS_TOKENS.ADMIN", "NewToken"); + jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); let info = Info { tracker_toml: default_config_toml(), From 20b00c2f7400b58d9bdc88bcdb030dda2f48a1cb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:00:31 +0100 Subject: [PATCH 158/217] refactor: implement Default for Configuration sections --- .../configuration/src/v1/health_check_api.rs | 8 ++++++++ packages/configuration/src/v1/http_tracker.rs | 12 ++++++++++++ packages/configuration/src/v1/mod.rs | 17 +++-------------- packages/configuration/src/v1/udp_tracker.rs | 8 ++++++++ 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index f7b15249c..1c2cd073a 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -11,3 +11,11 @@ pub struct HealthCheckApi { /// system to choose a random port, use port `0`. pub bind_address: String, } + +impl Default for HealthCheckApi { + fn default() -> Self { + Self { + bind_address: String::from("127.0.0.1:1313"), + } + } +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index 4c88feb9c..c2d5928e2 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -21,3 +21,15 @@ pub struct HttpTracker { #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, } + +impl Default for HttpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + } + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 88b9565bd..07d8a7194 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -346,21 +346,10 @@ impl Default for Configuration { udp_trackers: Vec::new(), http_trackers: Vec::new(), http_api: HttpApi::default(), - health_check_api: HealthCheckApi { - bind_address: String::from("127.0.0.1:1313"), - }, + health_check_api: HealthCheckApi::default(), }; - configuration.udp_trackers.push(UdpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:6969"), - }); - configuration.http_trackers.push(HttpTracker { - enabled: false, - bind_address: String::from("0.0.0.0:7070"), - ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, - }); + configuration.udp_trackers.push(UdpTracker::default()); + configuration.http_trackers.push(HttpTracker::default()); configuration } } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index b304054c3..254272bdd 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -10,3 +10,11 @@ pub struct UdpTracker { /// system to choose a random port, use port `0`. pub bind_address: String, } +impl Default for UdpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + } + } +} From 7fbb2e2f43e8ec4c29e65d2d954dd7c1e2803662 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:07:36 +0100 Subject: [PATCH 159/217] docs: update env var name in toml config template files --- share/default/config/tracker.container.mysql.toml | 8 ++++---- share/default/config/tracker.container.sqlite3.toml | 8 ++++---- share/default/config/tracker.development.sqlite3.toml | 4 ++++ share/default/config/tracker.e2e.container.sqlite3.toml | 8 ++++---- share/default/config/tracker.udp.benchmarking.toml | 4 ++++ 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index e7714c229..f2db06228 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 4ec055c56..4a3ba03b6 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index 9304a2d51..62e5b478e 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 86ffb3ffd..3738704b5 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 70298e9dc..1e951d8fc 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] From cbca1d5d8f44dc5378dd825c3b0703c3e1daba20 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:26:46 +0100 Subject: [PATCH 160/217] tests: add test for configuration with deprecated env var name Until it's updated in this repor and the Index repo you can overwrite the admin token using the new env var name and the old one (deprecated): - New: `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` - Old (deprecated): `TORRUST_TRACKER_API_ADMIN_TOKEN` THe new one uses exactly the strcuture and atribute names in the toml file, using `__` as the separator for levels. We can remove the test when we remove the deprecated name. --- packages/configuration/src/v1/mod.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 07d8a7194..8d5c123fe 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -540,7 +540,7 @@ mod tests { } #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin() { + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_env_var() { figment::Jail::expect_with(|jail| { jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); @@ -559,4 +559,23 @@ mod tests { Ok(()) }); } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: Some("NewToken".to_owned()), + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!( + configuration.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } } From 571b98b01bdf7d619d4b7719db1ba04127692b0d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 13:47:22 +0100 Subject: [PATCH 161/217] feat: allow users not to provide config option with default values Now, you are able to run the tracler like this: ``` TORRUST_TRACKER_CONFIG="" cargo run ``` Default values will be used for the missing values in the provided configuration. In that case, none of the values have been provided, so it will use default values for all options. --- packages/configuration/src/v1/mod.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8d5c123fe..25aa587b3 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -239,7 +239,7 @@ use std::fs; use std::net::IpAddr; use std::str::FromStr; -use figment::providers::{Env, Format, Toml}; +use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; @@ -393,7 +393,7 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::new() + let figment = Figment::from(Serialized::defaults(Configuration::default())) .merge(Toml::string(&info.tracker_toml)) .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); @@ -523,6 +523,24 @@ mod tests { assert_eq!(contents, default_config_toml()); } + #[test] + fn configuration_should_use_the_default_values_when_an_empty_configuration_is_provided_by_the_user() { + figment::Jail::expect_with(|_jail| { + let empty_configuration = String::new(); + + let info = Info { + tracker_toml: empty_configuration, + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration, Configuration::default()); + + Ok(()) + }); + } + #[test] fn configuration_should_be_loaded_from_a_toml_config_file() { figment::Jail::expect_with(|_jail| { From f3e5cb532d4adc8a2476c1f7b1e8e5614af5fe23 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 9 May 2024 17:19:30 +0100 Subject: [PATCH 162/217] refactor: [#852] eenrich field types in HealthCheckApi config struct --- packages/configuration/src/v1/health_check_api.rs | 6 ++++-- packages/test-helpers/src/configuration.rs | 4 ++-- src/bootstrap/jobs/health_check_api.rs | 2 +- tests/servers/health_check_api/environment.rs | 5 +---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index 1c2cd073a..b8bfd2c1b 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -1,3 +1,5 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -9,13 +11,13 @@ pub struct HealthCheckApi { /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, } impl Default for HealthCheckApi { fn default() -> Self { Self { - bind_address: String::from("127.0.0.1:1313"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 49cfdd390..c1ee95197 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -1,6 +1,6 @@ //! Tracker configuration factories for testing. use std::env; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use torrust_tracker_configuration::Configuration; use torrust_tracker_primitives::TrackerMode; @@ -39,7 +39,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; - config.health_check_api.bind_address = format!("127.0.0.1:{}", &health_check_api_port); + config.health_check_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), health_check_api_port); // Ephemeral socket address for UDP tracker let udp_port = 0u16; diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index 1e3fa6be3..8622d69a9 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -42,7 +42,7 @@ use crate::servers::service::Service; #[allow(clippy::async_yields_async)] #[instrument(ret, fields(registar = %registar))] pub async fn start_job(config: &HealthCheckApi, registar: &Registar, version: Version) -> JoinHandle<()> { - let addr = config.bind_address.parse().expect("it should parse the binding address"); + let addr = config.bind_address; let form = registar.form(); let registry = registar.as_ref().clone(); diff --git a/tests/servers/health_check_api/environment.rs b/tests/servers/health_check_api/environment.rs index 886a24b0f..d506d2c62 100644 --- a/tests/servers/health_check_api/environment.rs +++ b/tests/servers/health_check_api/environment.rs @@ -23,10 +23,7 @@ impl Environment { pub async fn new(config: &Arc, registar: &Registar) -> Self { let registry = registar.as_ref().clone(); - let addr = config - .bind_address - .parse::() - .expect("it should have a valid http tracker bind address"); + let addr = config.bind_address; let form = registar.form(); From c4abae4a5f216b38945a8cdf054139ab2d579ab8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 11:12:53 +0100 Subject: [PATCH 163/217] refactor: [#852] eenrich field types in UdpTracker config struct --- packages/configuration/src/v1/udp_tracker.rs | 6 ++++-- packages/test-helpers/src/configuration.rs | 10 +++++----- src/bootstrap/jobs/health_check_api.rs | 6 +----- src/bootstrap/jobs/udp_tracker.rs | 5 +---- tests/common/udp/environment.rs | 5 +---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index 254272bdd..1f772164e 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -1,3 +1,5 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -8,13 +10,13 @@ pub struct UdpTracker { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, } impl Default for UdpTracker { fn default() -> Self { Self { enabled: false, - bind_address: String::from("0.0.0.0:6969"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c1ee95197..c28cf553e 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -1,6 +1,6 @@ //! Tracker configuration factories for testing. use std::env; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use torrust_tracker_configuration::Configuration; use torrust_tracker_primitives::TrackerMode; @@ -44,7 +44,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for UDP tracker let udp_port = 0u16; config.udp_trackers[0].enabled = true; - config.udp_trackers[0].bind_address = format!("127.0.0.1:{}", &udp_port); + config.udp_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port); // Ephemeral socket address for HTTP tracker let http_port = 0u16; @@ -136,10 +136,10 @@ pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { pub fn ephemeral_ipv6() -> Configuration { let mut cfg = ephemeral(); - let ipv6 = format!("[::]:{}", 0); + let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6); - cfg.http_trackers[0].bind_address.clone_from(&ipv6); + cfg.http_api.bind_address.clone_from(&ipv6.to_string()); + cfg.http_trackers[0].bind_address.clone_from(&ipv6.to_string()); cfg.udp_trackers[0].bind_address = ipv6; cfg diff --git a/src/bootstrap/jobs/health_check_api.rs b/src/bootstrap/jobs/health_check_api.rs index 8622d69a9..ea4237e96 100644 --- a/src/bootstrap/jobs/health_check_api.rs +++ b/src/bootstrap/jobs/health_check_api.rs @@ -107,11 +107,7 @@ mod tests { #[tokio::test] //#[traced_test] async fn it_should_start_and_stop_the_v0_of_the_health_check_service() { - let addr = ephemeral_mode_public() - .health_check_api - .bind_address - .parse() - .expect("it should parse the binding address"); + let addr = ephemeral_mode_public().health_check_api.bind_address; let registar = Registar::default(); let form = registar.form(); diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index abf952d4b..21de13475 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -34,10 +34,7 @@ use crate::servers::udp::Version; #[instrument(ret)] pub async fn start_job(config: &UdpTracker, tracker: Arc, form: Form, version: Version) -> Option> { if config.enabled { - let addr = config - .bind_address - .parse::() - .expect("it should have a valid http tracker bind address"); + let addr = config.bind_address; match version { Version::V0 => Some(start_v0(addr, tracker.clone(), form).await), diff --git a/tests/common/udp/environment.rs b/tests/common/udp/environment.rs index 4fe2b5e14..710999182 100644 --- a/tests/common/udp/environment.rs +++ b/tests/common/udp/environment.rs @@ -35,10 +35,7 @@ impl Environment { let config = Arc::new(configuration.udp_trackers[0].clone()); - let addr = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let addr = config.bind_address; let server = Service::new(Launcher::new(tracker.clone(), addr)); From e7827ea4d9daeb1a6048342bb92c1326db160e8f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 11:26:04 +0100 Subject: [PATCH 164/217] refactor: [#852] enrich field types in HttpTracker config struct If the next major config version the `TslConfig` shoud always contain valid file paths and the whole field should be optional in the parent strcut `HttpTracker`: ```rust pub struct HttpTracker { pub enabled: bool, pub bind_address: SocketAddr, pub ssl_enabled: bool, #[serde(flatten)] pub tsl_config: Optional, } pub struct TslConfig { pub ssl_cert_path: PathBuf, pub ssl_key_path: PathBuf, } ``` That mean, the user could provide it or not, but if it's provided file paths can't be empty. --- packages/configuration/src/lib.rs | 13 +++++++++++ packages/configuration/src/v1/http_tracker.rs | 22 +++++++++---------- packages/test-helpers/src/configuration.rs | 4 ++-- src/bootstrap/jobs/http_tracker.rs | 17 +++++++------- src/servers/http/launcher.rs | 17 +++++++------- tests/servers/http/environment.rs | 15 +++++++------ 6 files changed, 52 insertions(+), 36 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 5977e030e..85ca03de6 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -12,6 +12,8 @@ use std::time::Duration; use std::{env, fs}; use derive_more::Constructor; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; @@ -167,3 +169,14 @@ impl From for Error { } } } + +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct TslConfig { + /// Path to the SSL certificate file. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index c2d5928e2..57b2d83a1 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -1,5 +1,9 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; + +use crate::TslConfig; /// Configuration for each HTTP tracker. #[serde_as] @@ -11,25 +15,21 @@ pub struct HttpTracker { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, /// Weather the HTTP tracker will use SSL or not. pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + /// TSL config. Only used if `ssl_enabled` is true. + #[serde(flatten)] + pub tsl_config: TslConfig, } impl Default for HttpTracker { fn default() -> Self { Self { enabled: false, - bind_address: String::from("0.0.0.0:7070"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070), ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, + tsl_config: TslConfig::default(), } } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c28cf553e..e6f53f85b 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -49,7 +49,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for HTTP tracker let http_port = 0u16; config.http_trackers[0].enabled = true; - config.http_trackers[0].bind_address = format!("127.0.0.1:{}", &http_port); + config.http_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port); // Ephemeral sqlite database let temp_directory = env::temp_dir(); @@ -139,7 +139,7 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); cfg.http_api.bind_address.clone_from(&ipv6.to_string()); - cfg.http_trackers[0].bind_address.clone_from(&ipv6.to_string()); + cfg.http_trackers[0].bind_address.clone_from(&ipv6); cfg.udp_trackers[0].bind_address = ipv6; cfg diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index fb129f000..0d6156487 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -43,14 +43,15 @@ pub async fn start_job( version: Version, ) -> Option> { if config.enabled { - let socket = config - .bind_address - .parse::() - .expect("it should have a valid http tracker bind address"); - - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let socket = config.bind_address; + + let tls = make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + ) + .await + .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); match version { Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs index 2946c6fea..01a2222d3 100644 --- a/src/servers/http/launcher.rs +++ b/src/servers/http/launcher.rs @@ -91,14 +91,15 @@ mod tests { let tracker = tracker(&cfg); let config = &cfg.http_trackers[0]; - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) - .await - .map(|tls| tls.expect("tls config failed")); + let bind_to = config.bind_address; + + let tls = make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + ) + .await + .map(|tls| tls.expect("tls config failed")); let form = ®istar::Registar::default(); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 09c6a2501..46d949dc0 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -36,13 +36,14 @@ impl Environment { let config = Arc::new(configuration.http_trackers[0].clone()); - let bind_to = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) - .map(|tls| tls.expect("tls config failed")); + let bind_to = config.bind_address; + + let tls = block_on(make_rust_tls( + config.ssl_enabled, + &config.tsl_config.ssl_cert_path, + &config.tsl_config.ssl_key_path, + )) + .map(|tls| tls.expect("tls config failed")); let server = Service::new(Launcher::new(tracker.clone(), bind_to, tls)); From 032777285ba04a7550a41ee0e0ec83dd3494e17c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:00:24 +0100 Subject: [PATCH 165/217] chore(deps): add dependency camino We are using `String` to represent a filepath. We are refactoring to enrich types in configuration. Filepath should be represented with `PathBuf` but it allows non UTF-8 chars, so it can't be serialized. Since we need to serialize config options (toml, json) is valid UTF-8 strings, we need a type that represents a valid fileptah but also is a valid UTF-8 strings. That makes impossible to use non-UTF8 fielpath. It seems that's a restriction accepted for a lot of projects including Rust `cargo`. --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + cSpell.json | 1 + packages/configuration/Cargo.toml | 1 + 4 files changed, 14 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cd6416a96..564429992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -545,6 +545,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + [[package]] name = "cast" version = "0.3.0" @@ -3576,6 +3585,7 @@ dependencies = [ "axum-client-ip", "axum-extra", "axum-server", + "camino", "chrono", "clap", "crossbeam-skiplist", @@ -3634,6 +3644,7 @@ dependencies = [ name = "torrust-tracker-configuration" version = "3.0.0-alpha.12-develop" dependencies = [ + "camino", "derive_more", "figment", "serde", diff --git a/Cargo.toml b/Cargo.toml index b92dc7193..c8086953b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ axum = { version = "0", features = ["macros"] } axum-client-ip = "0" axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } +camino = { version = "1.1.6", features = ["serde"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" diff --git a/cSpell.json b/cSpell.json index b3b9a7237..f02277ec7 100644 --- a/cSpell.json +++ b/cSpell.json @@ -25,6 +25,7 @@ "Buildx", "byteorder", "callgrind", + "camino", "canonicalize", "canonicalized", "certbot", diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index a033dcea1..bac2132d5 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,6 +15,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +camino = { version = "1.1.6", features = ["serde"] } derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } From 14001f0c7249c4b573ac7be78ad70ce233d0ced5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:06:32 +0100 Subject: [PATCH 166/217] refactor: [#852] eenrich field types in TslConfig config struct --- packages/configuration/src/lib.rs | 5 +++-- src/bootstrap/jobs/http_tracker.rs | 4 ++-- src/bootstrap/jobs/mod.rs | 32 +++++++++++++++++++++++++++++- src/servers/http/launcher.rs | 4 ++-- tests/servers/http/environment.rs | 4 ++-- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 85ca03de6..7ddd10266 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use std::time::Duration; use std::{env, fs}; +use camino::Utf8PathBuf; use derive_more::Constructor; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, NoneAsEmptyString}; @@ -175,8 +176,8 @@ impl From for Error { pub struct TslConfig { /// Path to the SSL certificate file. #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, + pub ssl_cert_path: Option, /// Path to the SSL key file. #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + pub ssl_key_path: Option, } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 0d6156487..dd4c192d3 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -18,7 +18,7 @@ use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; use tracing::{info, instrument}; -use super::make_rust_tls; +use super::make_rust_tls_from_path_buf; use crate::core; use crate::servers::http::launcher::Launcher; use crate::servers::http::Version; @@ -45,7 +45,7 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls( + let tls = make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 47ac139be..8584c1a96 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -11,6 +11,7 @@ use std::panic::Location; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; +use camino::Utf8PathBuf; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; use tracing::{info, instrument}; @@ -57,7 +58,36 @@ pub async fn make_rust_tls(enabled: bool, cert: &Option, key: &Option, + key: &Option, +) -> Option> { + if !enabled { + info!("TLS not enabled"); + return None; + } + + if let (Some(cert), Some(key)) = (cert, key) { + info!("Using https: cert path: {cert}."); + info!("Using https: key path: {key}."); Some( RustlsConfig::from_pem_file(cert, key) diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs index 01a2222d3..4dc8a8992 100644 --- a/src/servers/http/launcher.rs +++ b/src/servers/http/launcher.rs @@ -81,7 +81,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; use crate::bootstrap::app::tracker; - use crate::bootstrap::jobs::make_rust_tls; + use crate::bootstrap::jobs::make_rust_tls_from_path_buf; use crate::servers::http::launcher::Launcher; use crate::servers::{registar, service}; @@ -93,7 +93,7 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls( + let tls = make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 46d949dc0..ba06160b6 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use futures::executor::block_on; use torrust_tracker::bootstrap::app::tracker; -use torrust_tracker::bootstrap::jobs::make_rust_tls; +use torrust_tracker::bootstrap::jobs::make_rust_tls_from_path_buf; use torrust_tracker::core::Tracker; use torrust_tracker::servers::http::handle::Handle; use torrust_tracker::servers::http::launcher::Launcher; @@ -38,7 +38,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls( + let tls = block_on(make_rust_tls_from_path_buf( config.ssl_enabled, &config.tsl_config.ssl_cert_path, &config.tsl_config.ssl_key_path, From 932c33bc84dec47391351985a69e27b1c5855418 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 12:49:57 +0100 Subject: [PATCH 167/217] refactor: [#852] enrich field types in HttpApi config struct --- packages/configuration/src/v1/tracker_api.rs | 23 +++++----- packages/test-helpers/src/configuration.rs | 4 +- src/bootstrap/jobs/http_tracker.rs | 12 ++---- src/bootstrap/jobs/mod.rs | 44 ++++++++++++-------- src/bootstrap/jobs/tracker_apis.rs | 7 +--- src/servers/apis/server.rs | 7 +--- tests/servers/api/environment.rs | 8 +--- tests/servers/http/environment.rs | 9 +--- 8 files changed, 52 insertions(+), 62 deletions(-) diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 8749478c8..5089c496a 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -1,7 +1,10 @@ use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; + +use crate::TslConfig; pub type AccessTokens = HashMap; @@ -15,19 +18,16 @@ pub struct HttpApi { /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. - pub bind_address: String, + pub bind_address: SocketAddr, /// Weather the HTTP API will use SSL or not. pub ssl_enabled: bool, - /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_cert_path: Option, - /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. - #[serde_as(as = "NoneAsEmptyString")] - pub ssl_key_path: Option, + /// TSL config. Only used if `ssl_enabled` is true. + #[serde(flatten)] + pub tsl_config: TslConfig, /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have - /// the all permissions. + /// all permissions. pub access_tokens: AccessTokens, } @@ -35,10 +35,9 @@ impl Default for HttpApi { fn default() -> Self { Self { enabled: true, - bind_address: String::from("127.0.0.1:1212"), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212), ssl_enabled: false, - ssl_cert_path: None, - ssl_key_path: None, + tsl_config: TslConfig::default(), access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] .iter() .cloned() diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index e6f53f85b..08f570dc6 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -35,7 +35,7 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for API let api_port = 0u16; config.http_api.enabled = true; - config.http_api.bind_address = format!("127.0.0.1:{}", &api_port); + config.http_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port); // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; @@ -138,7 +138,7 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6.to_string()); + cfg.http_api.bind_address.clone_from(&ipv6); cfg.http_trackers[0].bind_address.clone_from(&ipv6); cfg.udp_trackers[0].bind_address = ipv6; diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index dd4c192d3..f80c759b1 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -18,7 +18,7 @@ use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; use tracing::{info, instrument}; -use super::make_rust_tls_from_path_buf; +use super::make_rust_tls; use crate::core; use crate::servers::http::launcher::Launcher; use crate::servers::http::Version; @@ -45,13 +45,9 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - ) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) + .await + .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); match version { Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 8584c1a96..541f45330 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -13,6 +13,7 @@ use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use camino::Utf8PathBuf; use thiserror::Error; +use torrust_tracker_configuration::TslConfig; use torrust_tracker_located_error::{DynError, LocatedError}; use tracing::{info, instrument}; pub mod health_check_api; @@ -50,13 +51,8 @@ impl From for Error { } #[instrument(ret)] -pub async fn make_rust_tls(enabled: bool, cert: &Option, key: &Option) -> Option> { - if !enabled { - info!("TLS not enabled"); - return None; - } - - if let (Some(cert), Some(key)) = (cert, key) { +pub async fn make_rust_tls(enabled: bool, tsl_config: &TslConfig) -> Option> { + if let (Some(cert), Some(key)) = (tsl_config.ssl_cert_path.clone(), tsl_config.ssl_key_path.clone()) { info!("Using https: cert path: {cert}."); info!("Using https: key path: {key}."); @@ -106,15 +102,23 @@ pub async fn make_rust_tls_from_path_buf( #[cfg(test)] mod tests { + use camino::Utf8PathBuf; + use torrust_tracker_configuration::TslConfig; + use super::make_rust_tls; #[tokio::test] async fn it_should_error_on_bad_tls_config() { - let (bad_cert_path, bad_key_path) = (Some("bad cert path".to_string()), Some("bad key path".to_string())); - let err = make_rust_tls(true, &bad_cert_path, &bad_key_path) - .await - .expect("tls_was_enabled") - .expect_err("bad_cert_and_key_files"); + let err = make_rust_tls( + true, + &TslConfig { + ssl_cert_path: Some(Utf8PathBuf::from("bad cert path")), + ssl_key_path: Some(Utf8PathBuf::from("bad key path")), + }, + ) + .await + .expect("tls_was_enabled") + .expect_err("bad_cert_and_key_files"); assert!(err .to_string() @@ -122,11 +126,17 @@ mod tests { } #[tokio::test] - async fn it_should_error_on_missing_tls_config() { - let err = make_rust_tls(true, &None, &None) - .await - .expect("tls_was_enabled") - .expect_err("missing_config"); + async fn it_should_error_on_missing_cert_or_key_paths() { + let err = make_rust_tls( + true, + &TslConfig { + ssl_cert_path: None, + ssl_key_path: None, + }, + ) + .await + .expect("tls_was_enabled") + .expect_err("missing_config"); assert_eq!(err.to_string(), "tls config missing"); } diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index f4865e191..c0b78de7d 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -58,12 +58,9 @@ pub struct ApiServerJobStarted(); #[instrument(ret)] pub async fn start_job(config: &HttpApi, tracker: Arc, form: Form, version: Version) -> Option> { if config.enabled { - let bind_to = config - .bind_address - .parse::() - .expect("it should have a valid tracker api bind address"); + let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) .await .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 39562472d..60f4714fb 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -207,12 +207,9 @@ mod tests { let tracker = tracker(&cfg); - let addr = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let addr = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path) + let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) .await .map(|tls| tls.expect("tls config failed")); diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 0c2b69ac2..76cfcfaf5 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -37,13 +37,9 @@ impl Environment { let config = Arc::new(configuration.http_api.clone()); let access_tokens = Arc::new(config.access_tokens.clone()); - let addr = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); + let addr = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.ssl_cert_path, &config.ssl_key_path)) - .map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); let stopped = Service::new(ApiLauncher::new(tracker.clone(), access_tokens, addr, tls)); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index ba06160b6..9cacab04b 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use futures::executor::block_on; use torrust_tracker::bootstrap::app::tracker; -use torrust_tracker::bootstrap::jobs::make_rust_tls_from_path_buf; +use torrust_tracker::bootstrap::jobs::make_rust_tls; use torrust_tracker::core::Tracker; use torrust_tracker::servers::http::handle::Handle; use torrust_tracker::servers::http::launcher::Launcher; @@ -38,12 +38,7 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - )) - .map(|tls| tls.expect("tls config failed")); + let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); let server = Service::new(Launcher::new(tracker.clone(), bind_to, tls)); From ca9208d25872e007425e847d84262c9c68170c78 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 13:47:47 +0100 Subject: [PATCH 168/217] refactor: [#852] enrich field types in Configuration struct --- packages/configuration/src/lib.rs | 17 ++++++++++++++ packages/configuration/src/v1/mod.rs | 25 ++++++++------------ packages/test-helpers/src/configuration.rs | 6 ++--- src/bootstrap/app.rs | 27 ++++++++++++++-------- src/servers/apis/mod.rs | 4 ++-- src/servers/http/v1/services/announce.rs | 5 ++-- src/servers/udp/v0/handlers.rs | 2 +- 7 files changed, 54 insertions(+), 32 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 7ddd10266..51074c26a 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -181,3 +181,20 @@ pub struct TslConfig { #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, } + +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + /// A level lower than all log levels. + Off, + /// Corresponds to the `Error` log level. + Error, + /// Corresponds to the `Warn` log level. + Warn, + /// Corresponds to the `Info` log level. + Info, + /// Corresponds to the `Debug` log level. + Debug, + /// Corresponds to the `Trace` log level. + Trace, +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 25aa587b3..643235c03 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -236,8 +236,7 @@ pub mod tracker_api; pub mod udp_tracker; use std::fs; -use std::net::IpAddr; -use std::str::FromStr; +use std::net::{IpAddr, Ipv4Addr}; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; @@ -248,7 +247,7 @@ use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::{AnnouncePolicy, Error, Info}; +use crate::{AnnouncePolicy, Error, Info, LogLevel}; /// Core configuration for the tracker. #[allow(clippy::struct_excessive_bools)] @@ -256,7 +255,7 @@ use crate::{AnnouncePolicy, Error, Info}; pub struct Configuration { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, + pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. pub mode: TrackerMode, @@ -284,7 +283,7 @@ pub struct Configuration { /// is using a loopback IP address, the tracker assumes that the peer is /// in the same network as the tracker and will use the tracker's IP /// address instead. - pub external_ip: Option, + pub external_ip: Option, /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -330,7 +329,7 @@ impl Default for Configuration { let announce_policy = AnnouncePolicy::default(); let mut configuration = Configuration { - log_level: Option::from(String::from("info")), + log_level: Some(LogLevel::Info), mode: TrackerMode::Public, db_driver: DatabaseDriver::Sqlite3, db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), @@ -338,7 +337,7 @@ impl Default for Configuration { min_announce_interval: announce_policy.interval_min, max_peer_timeout: 900, on_reverse_proxy: false, - external_ip: Some(String::from("0.0.0.0")), + external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), tracker_usage_statistics: true, persistent_torrent_completed_stat: false, inactive_peer_cleanup_interval: 600, @@ -363,13 +362,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - match &self.external_ip { - None => None, - Some(external_ip) => match IpAddr::from_str(external_ip) { - Ok(external_ip) => Some(external_ip), - Err(_) => None, - }, - } + self.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -432,6 +425,8 @@ impl Configuration { #[cfg(test)] mod tests { + use std::net::{IpAddr, Ipv4Addr}; + use crate::v1::Configuration; use crate::Info; @@ -495,7 +490,7 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); + assert_eq!(configuration.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); } #[test] diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 08f570dc6..0c7cc533a 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -2,7 +2,7 @@ use std::env; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use torrust_tracker_configuration::Configuration; +use torrust_tracker_configuration::{Configuration, LogLevel}; use torrust_tracker_primitives::TrackerMode; use crate::random; @@ -28,7 +28,7 @@ pub fn ephemeral() -> Configuration { // For example: a test for the UDP tracker should disable the API and HTTP tracker. let mut config = Configuration { - log_level: Some("off".to_owned()), // Change to `debug` for tests debugging + log_level: Some(LogLevel::Off), // Change to `debug` for tests debugging ..Default::default() }; @@ -125,7 +125,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.external_ip = Some(ip.to_string()); + cfg.external_ip = Some(ip); cfg } diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index dce460bc9..39c924998 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -14,29 +14,38 @@ use std::sync::Arc; use torrust_tracker_clock::static_time; -use torrust_tracker_configuration::Configuration; -use tracing::Level; +use torrust_tracker_configuration::{Configuration, LogLevel}; +use tracing::level_filters::LevelFilter; use super::config::initialize_configuration; use crate::core::services::tracker_factory; use crate::core::Tracker; use crate::shared::crypto::ephemeral_instance_keys; +fn map_to_tracing_level_filter(log_level: &LogLevel) -> LevelFilter { + match log_level { + LogLevel::Off => LevelFilter::OFF, + LogLevel::Error => LevelFilter::ERROR, + LogLevel::Warn => LevelFilter::WARN, + LogLevel::Info => LevelFilter::INFO, + LogLevel::Debug => LevelFilter::DEBUG, + LogLevel::Trace => LevelFilter::TRACE, + } +} + /// It loads the configuration from the environment gets trace level /// /// # Panics /// /// It will panic if the tracing level is malformed in the configuration. #[must_use] -pub fn config() -> (Configuration, Level) { +pub fn config() -> (Configuration, LevelFilter) { let config = initialize_configuration(); - let level: Level = config - .log_level - .as_deref() - .unwrap_or("info") - .parse() - .expect("its should provide a valid value for the log level"); + let level: LevelFilter = match &config.log_level { + None => LevelFilter::INFO, + Some(level) => map_to_tracing_level_filter(level), + }; (config, level) } diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 2d4b3abe1..ef37026fe 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -130,8 +130,8 @@ //! > **NOTICE**: You can generate a self-signed certificate for localhost using //! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). //! That's particularly useful for testing purposes. Once you have the certificate -//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::ssl_cert_path) -//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::ssl_key_path) +//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) +//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) //! options in the configuration file with the paths to the certificate //! (`localhost.crt`) and key (`localhost.key`) files. //! diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index b37081045..5a0ae40e4 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -145,8 +145,9 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.external_ip = - Some(IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)).to_string()); + configuration.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, + ))); Tracker::new(&configuration, Some(stats_event_sender), statistics::Repo::new()).unwrap() } diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index c5c25be51..95d29301c 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -426,7 +426,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.external_ip = Some(external_ip.to_owned()); + self.configuration.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From f4304d6aaa5c930275f581954d2a97a9f093168d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 15:59:58 +0100 Subject: [PATCH 169/217] refactor: [#852] extract Core configuration type --- packages/configuration/src/lib.rs | 4 +- packages/configuration/src/v1/core.rs | 88 +++++++++++++++ packages/configuration/src/v1/mod.rs | 101 +++--------------- packages/test-helpers/src/configuration.rs | 23 ++-- src/app.rs | 6 +- src/bootstrap/app.rs | 2 +- src/bootstrap/jobs/torrent_cleanup.rs | 4 +- src/core/mod.rs | 16 +-- src/core/services/mod.rs | 2 +- .../http/v1/extractors/client_ip_sources.rs | 2 +- src/servers/http/v1/services/announce.rs | 2 +- src/servers/udp/v0/handlers.rs | 2 +- 12 files changed, 134 insertions(+), 118 deletions(-) create mode 100644 packages/configuration/src/v1/core.rs diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 51074c26a..3479dfd95 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -3,7 +3,7 @@ //! This module contains the configuration data structures for the //! Torrust Tracker, which is a `BitTorrent` tracker server. //! -//! The current version for configuration is [`v1`](crate::v1). +//! The current version for configuration is [`v1`]. pub mod v1; use std::collections::HashMap; @@ -182,7 +182,7 @@ pub struct TslConfig { pub ssl_key_path: Option, } -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] #[serde(rename_all = "lowercase")] pub enum LogLevel { /// A level lower than all log levels. diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs new file mode 100644 index 000000000..ed9074194 --- /dev/null +++ b/packages/configuration/src/v1/core.rs @@ -0,0 +1,88 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; + +use crate::{AnnouncePolicy, LogLevel}; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Core { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + pub log_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. + pub mode: TrackerMode, + + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + pub db_path: String, + + /// See [`AnnouncePolicy::interval`] + pub announce_interval: u32, + + /// See [`AnnouncePolicy::interval_min`] + pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. + pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. + pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + pub remove_peerless_torrents: bool, +} + +impl Default for Core { + fn default() -> Self { + let announce_policy = AnnouncePolicy::default(); + + Self { + log_level: Some(LogLevel::Info), + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, + db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + announce_interval: announce_policy.interval, + min_announce_interval: announce_policy.interval_min, + max_peer_timeout: 900, + on_reverse_proxy: false, + external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), + tracker_usage_statistics: true, + persistent_torrent_completed_stat: false, + inactive_peer_cleanup_interval: 600, + remove_peerless_torrents: true, + } + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 643235c03..d19fedc87 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -78,7 +78,7 @@ //! //! Alternatively, you could setup a reverse proxy like Nginx or Apache to //! handle the SSL/TLS part and forward the requests to the tracker. If you do -//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! that, you should set [`on_reverse_proxy`](crate::v1::core::Core::on_reverse_proxy) //! to `true` in the configuration file. It's out of scope for this //! documentation to explain in detail how to setup a reverse proxy, but the //! configuration file should be something like this: @@ -230,86 +230,32 @@ //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` +pub mod core; pub mod health_check_api; pub mod http_tracker; pub mod tracker_api; pub mod udp_tracker; use std::fs; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; use serde::{Deserialize, Serialize}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +use self::core::Core; use self::health_check_api::HealthCheckApi; use self::http_tracker::HttpTracker; use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; -use crate::{AnnouncePolicy, Error, Info, LogLevel}; +use crate::{Error, Info}; /// Core configuration for the tracker. -#[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - pub log_level: Option, - /// Tracker mode. See [`TrackerMode`] for more information. - pub mode: TrackerMode, - - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - pub db_driver: DatabaseDriver, - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - pub db_path: String, - - /// See [`AnnouncePolicy::interval`] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - pub on_reverse_proxy: bool, - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. - /// If enabled, the tracker will collect statistics like the number of - /// connections handled, the number of announce requests handled, etc. - /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more - /// information about the collected metrics. - pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive - /// peers from the torrent peer list. - pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - pub remove_peerless_torrents: bool, - - // Server jobs configuration + /// Core configuration. + #[serde(flatten)] + pub core: Core, /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. @@ -326,30 +272,13 @@ pub struct Configuration { impl Default for Configuration { fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - - let mut configuration = Configuration { - log_level: Some(LogLevel::Info), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, - udp_trackers: Vec::new(), - http_trackers: Vec::new(), + Self { + core: Core::default(), + udp_trackers: vec![UdpTracker::default()], + http_trackers: vec![HttpTracker::default()], http_api: HttpApi::default(), health_check_api: HealthCheckApi::default(), - }; - configuration.udp_trackers.push(UdpTracker::default()); - configuration.http_trackers.push(HttpTracker::default()); - configuration + } } } @@ -362,7 +291,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - self.external_ip.as_ref().map(|external_ip| *external_ip) + self.core.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -490,7 +419,7 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); + assert_eq!(configuration.core.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); } #[test] diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 0c7cc533a..86ed57b9e 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -27,10 +27,9 @@ pub fn ephemeral() -> Configuration { // todo: disable services that are not needed. // For example: a test for the UDP tracker should disable the API and HTTP tracker. - let mut config = Configuration { - log_level: Some(LogLevel::Off), // Change to `debug` for tests debugging - ..Default::default() - }; + let mut config = Configuration::default(); + + config.core.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; @@ -55,7 +54,7 @@ pub fn ephemeral() -> Configuration { let temp_directory = env::temp_dir(); let random_db_id = random::string(16); let temp_file = temp_directory.join(format!("data_{random_db_id}.db")); - temp_file.to_str().unwrap().clone_into(&mut config.db_path); + temp_file.to_str().unwrap().clone_into(&mut config.core.db_path); config } @@ -65,7 +64,7 @@ pub fn ephemeral() -> Configuration { pub fn ephemeral_with_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.on_reverse_proxy = true; + cfg.core.on_reverse_proxy = true; cfg } @@ -75,7 +74,7 @@ pub fn ephemeral_with_reverse_proxy() -> Configuration { pub fn ephemeral_without_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.on_reverse_proxy = false; + cfg.core.on_reverse_proxy = false; cfg } @@ -85,7 +84,7 @@ pub fn ephemeral_without_reverse_proxy() -> Configuration { pub fn ephemeral_mode_public() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Public; + cfg.core.mode = TrackerMode::Public; cfg } @@ -95,7 +94,7 @@ pub fn ephemeral_mode_public() -> Configuration { pub fn ephemeral_mode_private() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Private; + cfg.core.mode = TrackerMode::Private; cfg } @@ -105,7 +104,7 @@ pub fn ephemeral_mode_private() -> Configuration { pub fn ephemeral_mode_whitelisted() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::Listed; + cfg.core.mode = TrackerMode::Listed; cfg } @@ -115,7 +114,7 @@ pub fn ephemeral_mode_whitelisted() -> Configuration { pub fn ephemeral_mode_private_whitelisted() -> Configuration { let mut cfg = ephemeral(); - cfg.mode = TrackerMode::PrivateListed; + cfg.core.mode = TrackerMode::PrivateListed; cfg } @@ -125,7 +124,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.external_ip = Some(ip); + cfg.core.external_ip = Some(ip); cfg } diff --git a/src/app.rs b/src/app.rs index 2b13a57d8..169805c84 100644 --- a/src/app.rs +++ b/src/app.rs @@ -64,7 +64,7 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) if tracker.is_private() { warn!( "Could not start UDP tracker on: {} while in {:?}. UDP is not safe for private trackers!", - config.bind_address, tracker_config.mode + config.bind_address, tracker_config.core.mode ); } else if let Some(job) = udp_tracker::start_job(config, tracker.clone(), registar.form(), servers::udp::Version::V0).await @@ -95,8 +95,8 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) } // Start runners to remove torrents without peers, every interval - if tracker_config.inactive_peer_cleanup_interval > 0 { - jobs.push(torrent_cleanup::start_job(tracker_config, &tracker)); + if tracker_config.core.inactive_peer_cleanup_interval > 0 { + jobs.push(torrent_cleanup::start_job(&tracker_config.core, &tracker)); } // Start Health Check API, consuming the registar. diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index 39c924998..bdd51a58e 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -42,7 +42,7 @@ fn map_to_tracing_level_filter(log_level: &LogLevel) -> LevelFilter { pub fn config() -> (Configuration, LevelFilter) { let config = initialize_configuration(); - let level: LevelFilter = match &config.log_level { + let level: LevelFilter = match &config.core.log_level { None => LevelFilter::INFO, Some(level) => map_to_tracing_level_filter(level), }; diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs index 4a6464c4a..2e875839e 100644 --- a/src/bootstrap/jobs/torrent_cleanup.rs +++ b/src/bootstrap/jobs/torrent_cleanup.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use chrono::Utc; use tokio::task::JoinHandle; -use torrust_tracker_configuration::Configuration; +use torrust_tracker_configuration::v1::core::Core; use tracing::{info, instrument}; use crate::core; @@ -26,7 +26,7 @@ use crate::core; /// Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about that option. #[must_use] #[instrument(ret)] -pub fn start_job(config: &Configuration, tracker: &Arc) -> JoinHandle<()> { +pub fn start_job(config: &Core, tracker: &Arc) -> JoinHandle<()> { let weak_tracker = std::sync::Arc::downgrade(tracker); let interval = config.inactive_peer_cleanup_interval; diff --git a/src/core/mod.rs b/src/core/mod.rs index e4c4c66ac..91bcfc7e5 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -546,13 +546,13 @@ impl Tracker { stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); + let database = Arc::new(databases::driver::build(&config.core.db_driver, &config.core.db_path)?); - let mode = config.mode; + let mode = config.core.mode; Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), + announce_policy: AnnouncePolicy::new(config.core.announce_interval, config.core.min_announce_interval), mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), @@ -562,11 +562,11 @@ impl Tracker { database, external_ip: config.get_ext_ip(), policy: TrackerPolicy::new( - config.remove_peerless_torrents, - config.max_peer_timeout, - config.persistent_torrent_completed_stat, + config.core.remove_peerless_torrents, + config.core.max_peer_timeout, + config.core.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.on_reverse_proxy, + on_reverse_proxy: config.core.on_reverse_proxy, }) } @@ -1035,7 +1035,7 @@ mod tests { pub fn tracker_persisting_torrents_in_database() -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.persistent_torrent_completed_stat = true; + configuration.core.persistent_torrent_completed_stat = true; tracker_factory(&configuration) } diff --git a/src/core/services/mod.rs b/src/core/services/mod.rs index 76c6a36f6..dec143568 100644 --- a/src/core/services/mod.rs +++ b/src/core/services/mod.rs @@ -21,7 +21,7 @@ use crate::core::Tracker; #[must_use] pub fn tracker_factory(config: &Configuration) -> Tracker { // Initialize statistics - let (stats_event_sender, stats_repository) = statistics::setup::factory(config.tracker_usage_statistics); + let (stats_event_sender, stats_repository) = statistics::setup::factory(config.core.tracker_usage_statistics); // Initialize Torrust tracker match Tracker::new(&Arc::new(config), stats_event_sender, stats_repository) { diff --git a/src/servers/http/v1/extractors/client_ip_sources.rs b/src/servers/http/v1/extractors/client_ip_sources.rs index 18eff26b3..1c6cdc636 100644 --- a/src/servers/http/v1/extractors/client_ip_sources.rs +++ b/src/servers/http/v1/extractors/client_ip_sources.rs @@ -16,7 +16,7 @@ //! the tracker will use the `X-Forwarded-For` header to get the client IP //! address. //! -//! See [`torrust_tracker_configuration::Configuration::on_reverse_proxy`]. +//! See [`torrust_tracker_configuration::Configuration::core.on_reverse_proxy`]. //! //! The tracker can also be configured to run without a reverse proxy. In this //! case, the tracker will use the IP address from the connection info. diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 5a0ae40e4..9529f954c 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -145,7 +145,7 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + configuration.core.external_ip = Some(IpAddr::V6(Ipv6Addr::new( 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index 95d29301c..569c4e9cd 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -426,7 +426,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); + self.configuration.core.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From d58dc303229fd62e59af9c3e05ba519f82616890 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 10 May 2024 17:05:48 +0100 Subject: [PATCH 170/217] refactor: tracker core service only needs the core config --- src/core/mod.rs | 21 +++++------ src/core/services/mod.rs | 2 +- src/servers/http/v1/services/announce.rs | 22 +++++++++--- src/servers/http/v1/services/scrape.rs | 40 ++++++++++++++++----- src/servers/udp/v0/handlers.rs | 44 ++++++++++++++++++++---- 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/core/mod.rs b/src/core/mod.rs index 91bcfc7e5..83206d87f 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -444,7 +444,8 @@ use std::time::Duration; use derive_more::Constructor; use tokio::sync::mpsc::error::SendError; use torrust_tracker_clock::clock::Time; -use torrust_tracker_configuration::{AnnouncePolicy, Configuration, TrackerPolicy, TORRENT_PEERS_LIMIT}; +use torrust_tracker_configuration::v1::core::Core; +use torrust_tracker_configuration::{AnnouncePolicy, TrackerPolicy, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; @@ -542,17 +543,17 @@ impl Tracker { /// /// Will return a `databases::error::Error` if unable to connect to database. The `Tracker` is responsible for the persistence. pub fn new( - config: &Configuration, + config: &Core, stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.core.db_driver, &config.core.db_path)?); + let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); - let mode = config.core.mode; + let mode = config.mode; Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.core.announce_interval, config.core.min_announce_interval), + announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), @@ -560,13 +561,13 @@ impl Tracker { stats_event_sender, stats_repository, database, - external_ip: config.get_ext_ip(), + external_ip: config.external_ip, policy: TrackerPolicy::new( - config.core.remove_peerless_torrents, - config.core.max_peer_timeout, - config.core.persistent_torrent_completed_stat, + config.remove_peerless_torrents, + config.max_peer_timeout, + config.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.core.on_reverse_proxy, + on_reverse_proxy: config.on_reverse_proxy, }) } diff --git a/src/core/services/mod.rs b/src/core/services/mod.rs index dec143568..166f40df4 100644 --- a/src/core/services/mod.rs +++ b/src/core/services/mod.rs @@ -24,7 +24,7 @@ pub fn tracker_factory(config: &Configuration) -> Tracker { let (stats_event_sender, stats_repository) = statistics::setup::factory(config.core.tracker_usage_statistics); // Initialize Torrust tracker - match Tracker::new(&Arc::new(config), stats_event_sender, stats_repository) { + match Tracker::new(&Arc::new(config).core, stats_event_sender, stats_repository) { Ok(tracker) => tracker, Err(error) => { panic!("{}", error) diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 9529f954c..e3bef3973 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -135,8 +135,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let mut peer = sample_peer_using_ipv4(); @@ -149,7 +155,7 @@ mod tests { 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); - Tracker::new(&configuration, Some(stats_event_sender), statistics::Repo::new()).unwrap() + Tracker::new(&configuration.core, Some(stats_event_sender), statistics::Repo::new()).unwrap() } fn peer_with_the_ipv4_loopback_ip() -> peer::Peer { @@ -194,8 +200,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let mut peer = sample_peer_using_ipv6(); diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index 990d0f93d..1dd94a83c 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -146,8 +146,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -164,8 +170,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); @@ -217,8 +229,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -235,8 +253,14 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(()))))); let stats_event_sender = Box::new(stats_event_sender_mock); - let tracker = - Arc::new(Tracker::new(&configuration::ephemeral(), Some(stats_event_sender), statistics::Repo::new()).unwrap()); + let tracker = Arc::new( + Tracker::new( + &configuration::ephemeral().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), + ); let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index 569c4e9cd..59bf22c0a 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -506,7 +506,12 @@ mod tests { let target = sample_ipv4_socket_address(); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_connect(&tracker, &sample_connect_request(), target).await.unwrap(); } @@ -522,7 +527,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_connect(&tracker, &sample_connect_request(), sample_ipv6_target()) .await @@ -766,7 +776,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_announce( @@ -995,7 +1010,12 @@ mod tests { let stats_event_sender = Box::new(stats_event_sender_mock); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); let target = sample_ipv6_target(); @@ -1025,7 +1045,7 @@ mod tests { let configuration = Arc::new(TrackerConfigurationBuilder::default().with_external_ip("::126.0.0.1").into()); let (stats_event_sender, stats_repository) = Keeper::new_active_instance(); let tracker = - Arc::new(core::Tracker::new(&configuration, Some(stats_event_sender), stats_repository).unwrap()); + Arc::new(core::Tracker::new(&configuration.core, Some(stats_event_sender), stats_repository).unwrap()); let loopback_ipv4 = Ipv4Addr::new(127, 0, 0, 1); let loopback_ipv6 = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1); @@ -1305,7 +1325,12 @@ mod tests { let target = sample_ipv4_target(); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_scrape(&tracker, &sample_scrape_request(&target), target) @@ -1337,7 +1362,12 @@ mod tests { let target = sample_ipv6_target(); let tracker = Arc::new( - core::Tracker::new(&tracker_configuration(), Some(stats_event_sender), statistics::Repo::new()).unwrap(), + core::Tracker::new( + &tracker_configuration().core, + Some(stats_event_sender), + statistics::Repo::new(), + ) + .unwrap(), ); handle_scrape(&tracker, &sample_scrape_request(&target), target) From c6590adff1a418f31287a5d4920d4f9aed455bfb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 13 May 2024 14:10:09 +0100 Subject: [PATCH 171/217] feat: define only non-defaults in toml config templates After implementing the configuration with Figment, it's now possible to omit values if they have a default value. Therefore we don't need to add all options in templates. We only need to add values that are overwriting deffault values. --- packages/configuration/src/lib.rs | 28 ++++++- packages/configuration/src/v1/core.rs | 83 ++++++++++++++++--- .../configuration/src/v1/health_check_api.rs | 9 +- packages/configuration/src/v1/http_tracker.rs | 24 +++++- packages/configuration/src/v1/tracker_api.rs | 35 ++++++-- packages/configuration/src/v1/udp_tracker.rs | 16 +++- .../config/tracker.container.mysql.toml | 31 ------- .../config/tracker.container.sqlite3.toml | 32 ------- .../config/tracker.development.sqlite3.toml | 36 -------- .../config/tracker.e2e.container.sqlite3.toml | 28 ------- .../config/tracker.udp.benchmarking.toml | 32 ------- 11 files changed, 169 insertions(+), 185 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 3479dfd95..f443d2b44 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -130,12 +130,22 @@ pub struct AnnouncePolicy { impl Default for AnnouncePolicy { fn default() -> Self { Self { - interval: 120, - interval_min: 120, + interval: Self::default_interval(), + interval_min: Self::default_interval_min(), } } } +impl AnnouncePolicy { + fn default_interval() -> u32 { + 120 + } + + fn default_interval_min() -> u32 { + 120 + } +} + /// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { @@ -176,12 +186,26 @@ impl From for Error { pub struct TslConfig { /// Path to the SSL certificate file. #[serde_as(as = "NoneAsEmptyString")] + #[serde(default = "TslConfig::default_ssl_cert_path")] pub ssl_cert_path: Option, /// Path to the SSL key file. #[serde_as(as = "NoneAsEmptyString")] + #[serde(default = "TslConfig::default_ssl_key_path")] pub ssl_key_path: Option, } +impl TslConfig { + #[allow(clippy::unnecessary_wraps)] + fn default_ssl_cert_path() -> Option { + Some(Utf8PathBuf::new()) + } + + #[allow(clippy::unnecessary_wraps)] + fn default_ssl_key_path() -> Option { + Some(Utf8PathBuf::new()) + } +} + #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Clone)] #[serde(rename_all = "lowercase")] pub enum LogLevel { diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index ed9074194..5d00c67ab 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -10,58 +10,71 @@ use crate::{AnnouncePolicy, LogLevel}; pub struct Core { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. + #[serde(default = "Core::default_log_level")] pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. + #[serde(default = "Core::default_mode")] pub mode: TrackerMode, // Database configuration /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + #[serde(default = "Core::default_db_driver")] pub db_driver: DatabaseDriver, /// Database connection string. The format depends on the database driver. /// For `Sqlite3`, the format is `path/to/database.db`, for example: /// `./storage/tracker/lib/database/sqlite3.db`. /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for /// example: `root:password@localhost:3306/torrust`. + #[serde(default = "Core::default_db_path")] pub db_path: String, /// See [`AnnouncePolicy::interval`] + #[serde(default = "AnnouncePolicy::default_interval")] pub announce_interval: u32, /// See [`AnnouncePolicy::interval_min`] + #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, /// Weather the tracker is behind a reverse proxy or not. /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header /// sent from the proxy will be used to get the client's IP address. + #[serde(default = "Core::default_on_reverse_proxy")] pub on_reverse_proxy: bool, /// The external IP address of the tracker. If the client is using a /// loopback IP address, this IP address will be used instead. If the peer /// is using a loopback IP address, the tracker assumes that the peer is /// in the same network as the tracker and will use the tracker's IP /// address instead. + #[serde(default = "Core::default_external_ip")] pub external_ip: Option, /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more /// information about the collected metrics. + #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, /// If enabled the tracker will persist the number of completed downloads. /// That's how many times a torrent has been downloaded completely. + #[serde(default = "Core::default_persistent_torrent_completed_stat")] pub persistent_torrent_completed_stat: bool, // Cleanup job configuration /// Maximum time in seconds that a peer can be inactive before being /// considered an inactive peer. If a peer is inactive for more than this /// time, it will be removed from the torrent peer list. + #[serde(default = "Core::default_max_peer_timeout")] pub max_peer_timeout: u32, /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. + #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, /// If enabled, the tracker will remove torrents that have no peers. /// The clean up torrent job runs every `inactive_peer_cleanup_interval` /// seconds and it removes inactive peers. Eventually, the peer list of a /// torrent could be empty and the torrent will be removed if this option is /// enabled. + #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, } @@ -70,19 +83,67 @@ impl Default for Core { let announce_policy = AnnouncePolicy::default(); Self { - log_level: Some(LogLevel::Info), - mode: TrackerMode::Public, - db_driver: DatabaseDriver::Sqlite3, - db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + log_level: Self::default_log_level(), + mode: Self::default_mode(), + db_driver: Self::default_db_driver(), + db_path: Self::default_db_path(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, - max_peer_timeout: 900, - on_reverse_proxy: false, - external_ip: Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), - tracker_usage_statistics: true, - persistent_torrent_completed_stat: false, - inactive_peer_cleanup_interval: 600, - remove_peerless_torrents: true, + max_peer_timeout: Self::default_max_peer_timeout(), + on_reverse_proxy: Self::default_on_reverse_proxy(), + external_ip: Self::default_external_ip(), + tracker_usage_statistics: Self::default_tracker_usage_statistics(), + persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), + inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), + remove_peerless_torrents: Self::default_remove_peerless_torrents(), } } } + +impl Core { + #[allow(clippy::unnecessary_wraps)] + fn default_log_level() -> Option { + Some(LogLevel::Info) + } + + fn default_mode() -> TrackerMode { + TrackerMode::Public + } + + fn default_db_driver() -> DatabaseDriver { + DatabaseDriver::Sqlite3 + } + + fn default_db_path() -> String { + String::from("./storage/tracker/lib/database/sqlite3.db") + } + + fn default_on_reverse_proxy() -> bool { + false + } + + #[allow(clippy::unnecessary_wraps)] + fn default_external_ip() -> Option { + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + } + + fn default_tracker_usage_statistics() -> bool { + true + } + + fn default_persistent_torrent_completed_stat() -> bool { + false + } + + fn default_max_peer_timeout() -> u32 { + 900 + } + + fn default_inactive_peer_cleanup_interval() -> u64 { + 600 + } + + fn default_remove_peerless_torrents() -> bool { + true + } +} diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs index b8bfd2c1b..61178fa80 100644 --- a/packages/configuration/src/v1/health_check_api.rs +++ b/packages/configuration/src/v1/health_check_api.rs @@ -11,13 +11,20 @@ pub struct HealthCheckApi { /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HealthCheckApi::default_bind_address")] pub bind_address: SocketAddr, } impl Default for HealthCheckApi { fn default() -> Self { Self { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313), + bind_address: Self::default_bind_address(), } } } + +impl HealthCheckApi { + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1313) + } +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index 57b2d83a1..b1fe1437b 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -10,26 +10,44 @@ use crate::TslConfig; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { /// Weather the HTTP tracker is enabled or not. + #[serde(default = "HttpTracker::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HttpTracker::default_bind_address")] pub bind_address: SocketAddr, /// Weather the HTTP tracker will use SSL or not. + #[serde(default = "HttpTracker::default_ssl_enabled")] pub ssl_enabled: bool, /// TSL config. Only used if `ssl_enabled` is true. #[serde(flatten)] + #[serde(default = "TslConfig::default")] pub tsl_config: TslConfig, } impl Default for HttpTracker { fn default() -> Self { Self { - enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070), - ssl_enabled: false, + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), + ssl_enabled: Self::default_ssl_enabled(), tsl_config: TslConfig::default(), } } } + +impl HttpTracker { + fn default_enabled() -> bool { + false + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) + } + + fn default_ssl_enabled() -> bool { + false + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 5089c496a..c2e3e5ee9 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -13,40 +13,61 @@ pub type AccessTokens = HashMap; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpApi { /// Weather the HTTP API is enabled or not. + #[serde(default = "HttpApi::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "HttpApi::default_bind_address")] pub bind_address: SocketAddr, /// Weather the HTTP API will use SSL or not. + #[serde(default = "HttpApi::default_ssl_enabled")] pub ssl_enabled: bool, /// TSL config. Only used if `ssl_enabled` is true. #[serde(flatten)] + #[serde(default = "TslConfig::default")] pub tsl_config: TslConfig, /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have /// all permissions. + #[serde(default = "HttpApi::default_access_tokens")] pub access_tokens: AccessTokens, } impl Default for HttpApi { fn default() -> Self { Self { - enabled: true, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212), - ssl_enabled: false, + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), + ssl_enabled: Self::default_ssl_enabled(), tsl_config: TslConfig::default(), - access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] - .iter() - .cloned() - .collect(), + access_tokens: Self::default_access_tokens(), } } } impl HttpApi { + fn default_enabled() -> bool { + true + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) + } + + fn default_ssl_enabled() -> bool { + false + } + + fn default_access_tokens() -> AccessTokens { + [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect() + } + pub fn override_admin_token(&mut self, api_admin_token: &str) { self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index 1f772164e..f8387202e 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -5,18 +5,30 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { /// Weather the UDP tracker is enabled or not. + #[serde(default = "UdpTracker::default_enabled")] pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. + #[serde(default = "UdpTracker::default_bind_address")] pub bind_address: SocketAddr, } impl Default for UdpTracker { fn default() -> Self { Self { - enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969), + enabled: Self::default_enabled(), + bind_address: Self::default_bind_address(), } } } + +impl UdpTracker { + fn default_enabled() -> bool { + false + } + + fn default_bind_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969) + } +} diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index f2db06228..7678327ab 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,41 +1,10 @@ -announce_interval = 120 db_driver = "MySQL" db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - -[[udp_trackers]] -bind_address = "0.0.0.0:6969" -enabled = false [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 4a3ba03b6..da8259286 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,41 +1,9 @@ -announce_interval = 120 -db_driver = "Sqlite3" db_path = "/var/lib/torrust/tracker/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - -[[udp_trackers]] -bind_address = "0.0.0.0:6969" -enabled = false [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index 62e5b478e..bf6478492 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -1,41 +1,5 @@ -announce_interval = 120 -db_driver = "Sqlite3" -db_path = "./storage/tracker/lib/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true - [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true [[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = true -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api] -bind_address = "127.0.0.1:1212" enabled = true -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 3738704b5..e7d8fa279 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,41 +1,13 @@ -announce_interval = 120 -db_driver = "Sqlite3" db_path = "/var/lib/torrust/tracker/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 -log_level = "info" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false -remove_peerless_torrents = true -tracker_usage_statistics = true [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true [[http_trackers]] -bind_address = "0.0.0.0:7070" enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] -bind_address = "0.0.0.0:1212" -enabled = true ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "0.0.0.0:1313" diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 1e951d8fc..00f62628b 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -1,41 +1,9 @@ -announce_interval = 120 -db_driver = "Sqlite3" -db_path = "./storage/tracker/lib/database/sqlite3.db" -external_ip = "0.0.0.0" -inactive_peer_cleanup_interval = 600 log_level = "error" -max_peer_timeout = 900 -min_announce_interval = 120 -mode = "public" -on_reverse_proxy = false -persistent_torrent_completed_stat = false remove_peerless_torrents = false tracker_usage_statistics = false [[udp_trackers]] -bind_address = "0.0.0.0:6969" enabled = true -[[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = false -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - [http_api] -bind_address = "127.0.0.1:1212" enabled = false -ssl_cert_path = "" -ssl_enabled = false -ssl_key_path = "" - -[http_api.access_tokens] -# Please override the admin token setting the environmental variable: -# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` -# The old variable name is deprecated: -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -admin = "MyAccessToken" - -[health_check_api] -bind_address = "127.0.0.1:1313" From 0fe2488ca0df45b094e68057e7bdd38d635492b9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 07:35:38 +0100 Subject: [PATCH 172/217] chore(deps): update dependencies ```ouput Updating crates.io index Locking 13 packages to latest compatible versions Removing allocator-api2 v0.2.18 Updating async-channel v2.2.1 -> v2.3.0 Updating async-compression v0.4.9 -> v0.4.10 Updating brotli v5.0.0 -> v6.0.0 Updating bytemuck v1.15.0 -> v1.16.0 Updating errno v0.3.8 -> v0.3.9 Updating hashlink v0.9.0 -> v0.9.1 Updating piper v0.2.1 -> v0.2.2 Updating rustls-pki-types v1.6.0 -> v1.7.0 Updating serde v1.0.200 -> v1.0.201 Updating serde_derive v1.0.200 -> v1.0.201 Updating serde_json v1.0.116 -> v1.0.117 Updating syn v2.0.61 -> v2.0.63 Updating waker-fn v1.1.1 -> v1.2.0 ``` --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 564429992..dbd9165ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,9 +547,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] From 636713ec694157817f19f43a4f930809ecd9a5aa Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 10:22:56 +0100 Subject: [PATCH 173/217] refactor: [#855] show toml file location in Figment errors Before this commit we loaded configuration in Figment always using a Toml string even if the configuration cames from a toml file. WHen there is an error Figment does not show the file location and that's one of the main advantages of using Figment. All errors point to the primary source of the configuration option. This commit fixes that problem leting Figment to load the configuration from the file when the user provides a file. Sample error: ``` Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... thread 'main' panicked at src/bootstrap/config.rs:45:32: called `Result::unwrap()` on an `Err` value: ConfigError { source: LocatedError { source: Error { tag: Tag(Default, 2), profile: Some(Profile(Uncased { string: "default" })), metadata: Some(Metadata { name: "TOML file", source: Some(File("/home/developer/torrust/torrust-tracker/./share/default/config/tracker.development.sqlite3.toml")), provide_location: Some(Location { file: "packages/configuration/src/v1/mod.rs", line: 330, col: 18 }), interpolater: }), path: [], kind: Message("TOML parse error at line 2, column 15\n |\n2 | enabled = truee\n | ^\nexpected newline, `#`\n"), prev: None }, location: Location { file: "packages/configuration/src/v1/mod.rs", line: 334, col: 41 } } } note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` Notice how the file path is included is the error: `/home/developer/torrust/torrust-tracker/./share/default/config/tracker.development.sqlite3.toml` --- packages/configuration/src/lib.rs | 47 ++++++++---------- packages/configuration/src/v1/mod.rs | 71 +++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index f443d2b44..c4e5660a9 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -7,9 +7,9 @@ pub mod v1; use std::collections::HashMap; +use std::env; use std::sync::Arc; use std::time::Duration; -use std::{env, fs}; use camino::Utf8PathBuf; use derive_more::Constructor; @@ -48,7 +48,8 @@ pub struct TrackerPolicy { /// Information required for loading config #[derive(Debug, Default, Clone)] pub struct Info { - tracker_toml: String, + config_toml: Option, + config_toml_path: String, api_admin_token: Option, } @@ -61,38 +62,30 @@ impl Info { /// #[allow(clippy::needless_pass_by_value)] pub fn new( - env_var_config: String, - env_var_path_config: String, - default_path_config: String, + env_var_config_toml: String, + env_var_config_toml_path: String, + default_config_toml_path: String, env_var_api_admin_token: String, ) -> Result { - let tracker_toml = if let Ok(tracker_toml) = env::var(&env_var_config) { - println!("Loading configuration from env var {env_var_config} ..."); + let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { + println!("Loading configuration from environment variable {config_toml} ..."); + Some(config_toml) + } else { + None + }; - tracker_toml + let config_toml_path = if let Ok(config_toml_path) = env::var(env_var_config_toml_path) { + println!("Loading configuration from file: `{config_toml_path}` ..."); + config_toml_path } else { - let config_path = if let Ok(config_path) = env::var(env_var_path_config) { - println!("Loading configuration file: `{config_path}` ..."); - - config_path - } else { - println!("Loading default configuration file: `{default_path_config}` ..."); - - default_path_config - }; - - fs::read_to_string(config_path) - .map_err(|e| Error::UnableToLoadFromConfigFile { - source: (Arc::new(e) as DynError).into(), - })? - .parse() - .map_err(|_e: std::convert::Infallible| Error::Infallible)? + println!("Loading configuration from default configuration file: `{default_config_toml_path}` ..."); + default_config_toml_path }; - let api_admin_token = env::var(env_var_api_admin_token).ok(); Ok(Self { - tracker_toml, - api_admin_token, + config_toml, + config_toml_path, + api_admin_token: env::var(env_var_api_admin_token).ok(), }) } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d19fedc87..8e15d65ca 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -250,6 +250,11 @@ use self::tracker_api::HttpApi; use self::udp_tracker::UdpTracker; use crate::{Error, Info}; +/// Prefix for env vars that overwrite configuration options. +const CONFIG_OVERRIDE_PREFIX: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_"; +/// Path separator in env var names for nested values in configuration. +const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; + /// Core configuration for the tracker. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { @@ -315,9 +320,16 @@ impl Configuration { /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. pub fn load(info: &Info) -> Result { - let figment = Figment::from(Serialized::defaults(Configuration::default())) - .merge(Toml::string(&info.tracker_toml)) - .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); + let figment = if let Some(config_toml) = &info.config_toml { + // Config in env var has priority over config file path + Figment::from(Serialized::defaults(Configuration::default())) + .merge(Toml::string(config_toml)) + .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) + } else { + Figment::from(Serialized::defaults(Configuration::default())) + .merge(Toml::file(&info.config_toml_path)) + .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) + }; let mut config: Configuration = figment.extract()?; @@ -449,11 +461,14 @@ mod tests { #[test] fn configuration_should_use_the_default_values_when_an_empty_configuration_is_provided_by_the_user() { - figment::Jail::expect_with(|_jail| { + figment::Jail::expect_with(|jail| { + jail.create_file("tracker.toml", "")?; + let empty_configuration = String::new(); let info = Info { - tracker_toml: empty_configuration, + config_toml: Some(empty_configuration), + config_toml_path: "tracker.toml".to_string(), api_admin_token: None, }; @@ -466,28 +481,59 @@ mod tests { } #[test] - fn configuration_should_be_loaded_from_a_toml_config_file() { + fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { + let config_toml = r#" + db_path = "OVERWRITTEN DEFAULT DB PATH" + "# + .to_string(); + let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(config_toml), + config_toml_path: String::new(), api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration, Configuration::default()); + assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + + Ok(()) + }); + } + + #[test] + fn default_configuration_could_be_overwritten_from_a_toml_config_file() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "tracker.toml", + r#" + db_path = "OVERWRITTEN DEFAULT DB PATH" + "#, + )?; + + let info = Info { + config_toml: None, + config_toml_path: "tracker.toml".to_string(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); } #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_env_var() { + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_an_env_var() { figment::Jail::expect_with(|jail| { - jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); + jail.set_env("TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(default_config_toml()), + config_toml_path: String::new(), api_admin_token: None, }; @@ -506,7 +552,8 @@ mod tests { fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { figment::Jail::expect_with(|_jail| { let info = Info { - tracker_toml: default_config_toml(), + config_toml: Some(default_config_toml()), + config_toml_path: String::new(), api_admin_token: Some("NewToken".to_owned()), }; From 243346d484f8a408a16090ff4e4547ab642fb302 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 10:39:20 +0100 Subject: [PATCH 174/217] refactor: move config env vars to configuration package --- packages/configuration/src/lib.rs | 24 ++++++++++++++++++------ src/bootstrap/config.rs | 22 ++-------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index c4e5660a9..4137af335 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -30,6 +30,19 @@ pub const PORT_ASSIGNED_BY_OS: u16 = 0; /// The maximum number of bytes in a UDP packet. pub const UDP_MAX_PACKET_SIZE: usize = 1496; +// Environment variables + +/// The whole `tracker.toml` file content. It has priority over the config file. +/// Even if the file is not on the default path. +const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; + +/// The `tracker.toml` file location. +pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; + +/// Env var to overwrite API admin token. +/// Deprecated: use `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. +const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; + pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; pub type HttpTracker = v1::http_tracker::HttpTracker; @@ -61,12 +74,11 @@ impl Info { /// Will return `Err` if unable to obtain a configuration. /// #[allow(clippy::needless_pass_by_value)] - pub fn new( - env_var_config_toml: String, - env_var_config_toml_path: String, - default_config_toml_path: String, - env_var_api_admin_token: String, - ) -> Result { + pub fn new(default_config_toml_path: String) -> Result { + let env_var_config_toml = ENV_VAR_CONFIG.to_string(); + let env_var_config_toml_path = ENV_VAR_PATH_CONFIG.to_string(); + let env_var_api_admin_token = ENV_VAR_API_ADMIN_TOKEN.to_string(); + let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); Some(config_toml) diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs index 858fd59fc..03dfd9a2f 100644 --- a/src/bootstrap/config.rs +++ b/src/bootstrap/config.rs @@ -4,17 +4,6 @@ use torrust_tracker_configuration::{Configuration, Info}; -// Environment variables - -/// The whole `tracker.toml` file content. It has priority over the config file. -/// Even if the file is not on the default path. -const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; -const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; - -/// The `tracker.toml` file location. -pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; - -// Default values pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.development.sqlite3.toml"; /// It loads the application configuration from the environment. @@ -34,15 +23,8 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG`. #[must_use] pub fn initialize_configuration() -> Configuration { - let info = Info::new( - ENV_VAR_CONFIG.to_string(), - ENV_VAR_PATH_CONFIG.to_string(), - DEFAULT_PATH_CONFIG.to_string(), - ENV_VAR_API_ADMIN_TOKEN.to_string(), - ) - .unwrap(); - - Configuration::load(&info).unwrap() + let info = Info::new(DEFAULT_PATH_CONFIG.to_string()).expect("info to load configuration is not valid"); + Configuration::load(&info).expect("configuration should be loaded from provided info") } #[cfg(test)] From 97312a9f567c1af3f97562bf457ab36a9b7027fb Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 13:46:58 +0100 Subject: [PATCH 175/217] refactor: [#851] rename env vars ``` TORRUST_TRACKER_BACK_ -> TORRUST_TRACKER_ TORRUST_TRACKER_DATABASE_DRIVER -> TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER TORRUST_TRACKER_API_ADMIN_TOKEN -> TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN TORRUST_TRACKER_CONFIG -> TORRUST_TRACKER_CONFIG_TOML TORRUST_TRACKER_PATH_CONFIG -> TORRUST_TRACKER_CONFIG_TOML_PATH ``` DB_DRIVER values: `MySQL`, `Sqlite3`. Removed lowercase values `mysql` and `sqlite3` used in containers. Some enums use lowercase. This is a braking change for container but not for configuration. IN the future we could use lowercase also in the configuration. --- Containerfile | 8 ++++---- README.md | 8 ++++---- compose.yaml | 4 ++-- docs/benchmarking.md | 2 +- docs/containers.md | 14 +++++++------- docs/profiling.md | 4 ++-- packages/configuration/src/lib.rs | 15 +++++++-------- packages/configuration/src/v1/mod.rs | 2 +- packages/primitives/src/lib.rs | 4 +++- share/container/entry_script_sh | 20 ++++++++++---------- src/bootstrap/config.rs | 6 +++--- src/console/ci/e2e/runner.rs | 2 +- src/console/profiling.rs | 4 ++-- src/lib.rs | 6 +++--- 14 files changed, 50 insertions(+), 49 deletions(-) diff --git a/Containerfile b/Containerfile index 590b0a13b..79fae692f 100644 --- a/Containerfile +++ b/Containerfile @@ -95,16 +95,16 @@ FROM gcr.io/distroless/cc-debian12:debug as runtime RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/env", "/bin/"] COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec -ARG TORRUST_TRACKER_PATH_CONFIG="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_DATABASE_DRIVER="sqlite3" +ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 -ENV TORRUST_TRACKER_PATH_CONFIG=${TORRUST_TRACKER_PATH_CONFIG} -ENV TORRUST_TRACKER_DATABASE_DRIVER=${TORRUST_TRACKER_DATABASE_DRIVER} +ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/README.md b/README.md index 8431c00e4..754d2d5b7 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,14 @@ cp ./share/default/config/tracker.development.sqlite3.toml ./storage/tracker/etc vim ./storage/tracker/etc/tracker.toml # Run the tracker with the updated configuration: -TORRUST_TRACKER_PATH_CONFIG="./storage/tracker/etc/tracker.toml" cargo run +TORRUST_TRACKER_CONFIG_TOML_PATH="./storage/tracker/etc/tracker.toml" cargo run ``` _Optionally, you may choose to supply the entire configuration as an environmental variable:_ ```sh # Use a configuration supplied on an environmental variable: -TORRUST_TRACKER_CONFIG=$(cat "./storage/tracker/etc/tracker.toml") cargo run +TORRUST_TRACKER_CONFIG_TOML=$(cat "./storage/tracker/etc/tracker.toml") cargo run ``` _For deployment, you **should** override the `api_admin_token` by using an environmental variable:_ @@ -102,8 +102,8 @@ gpg --armor --gen-random 1 10 | tee ./storage/tracker/lib/tracker_api_admin_toke chmod go-rwx ./storage/tracker/lib/tracker_api_admin_token.secret # Override secret in configuration using an environmental variable: -TORRUST_TRACKER_CONFIG=$(cat "./storage/tracker/etc/tracker.toml") \ - TORRUST_TRACKER_API_ADMIN_TOKEN=$(cat "./storage/tracker/lib/tracker_api_admin_token.secret") \ +TORRUST_TRACKER_CONFIG_TOML=$(cat "./storage/tracker/etc/tracker.toml") \ + TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=$(cat "./storage/tracker/lib/tracker_api_admin_token.secret") \ cargo run ``` diff --git a/compose.yaml b/compose.yaml index 672ca6d0f..1d425c743 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,8 +4,8 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_DATABASE_DRIVER=${TORRUST_TRACKER_DATABASE_DRIVER:-mysql} - - TORRUST_TRACKER_API_ADMIN_TOKEN=${TORRUST_TRACKER_API_ADMIN_TOKEN:-MyAccessToken} + - TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side ports: diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 7c82df14c..2a3f1f8b0 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -39,7 +39,7 @@ Build and run the tracker: ```console cargo build --release -TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" ./target/release/torrust-tracker +TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" ./target/release/torrust-tracker ``` Run the load test with: diff --git a/docs/containers.md b/docs/containers.md index 6622e29b2..a0ba59d4b 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -147,10 +147,10 @@ Environmental variables are loaded through the `--env`, in the format `--env VAR The following environmental variables can be set: -- `TORRUST_TRACKER_PATH_CONFIG` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). -- `TORRUST_TRACKER_API_ADMIN_TOKEN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_DATABASE_DRIVER` - The database type used for the container, (options: `sqlite3`, `mysql`, default `sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. -- `TORRUST_TRACKER_CONFIG` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG=$(cat tracker-tracker.toml)`). +- `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). +- `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). - `HTTP_PORT` - The port for the HTTP tracker. This should match the port used in the configuration, (default `7070`). @@ -205,7 +205,7 @@ mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ ## Run Torrust Tracker Container Image docker run -it \ - --env TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + --env TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ --env USER_ID="$(id -u)" \ --publish 0.0.0.0:7070:7070/tcp \ --publish 0.0.0.0:6969:6969/udp \ @@ -227,7 +227,7 @@ mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ ## Run Torrust Tracker Container Image podman run -it \ - --env TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + --env TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ --env USER_ID="$(id -u)" \ --publish 0.0.0.0:7070:7070/tcp \ --publish 0.0.0.0:6969:6969/udp \ @@ -255,7 +255,7 @@ docker build --target release --tag torrust-tracker:release --file Containerfile mkdir -p ./storage/tracker/lib/ ./storage/tracker/log/ ./storage/tracker/etc/ USER_ID=$(id -u) \ - TORRUST_TRACKER_API_ADMIN_TOKEN="MySecretToken" \ + TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN="MySecretToken" \ docker compose up --build ``` diff --git a/docs/profiling.md b/docs/profiling.md index 406560f3c..8038f9e77 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -35,7 +35,7 @@ To generate the graph you will need to: ```console cargo build --profile=release-debug --bin=profiling ./target/release/aquatic_udp_load_test -c "load-test-config.toml" -sudo TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 +sudo TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" /home/USER/.cargo/bin/flamegraph -- ./target/release-debug/profiling 60 ``` __NOTICE__: You need to install the `aquatic_udp_load_test` program. @@ -92,7 +92,7 @@ Build and the binary for profiling: ```console RUSTFLAGS='-g' cargo build --release --bin profiling \ - && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ + && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ && valgrind \ --tool=callgrind \ --callgrind-out-file=callgrind.out \ diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 4137af335..9dc4987e6 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -34,14 +34,13 @@ pub const UDP_MAX_PACKET_SIZE: usize = 1496; /// The whole `tracker.toml` file content. It has priority over the config file. /// Even if the file is not on the default path. -const ENV_VAR_CONFIG: &str = "TORRUST_TRACKER_CONFIG"; +const ENV_VAR_CONFIG_TOML: &str = "TORRUST_TRACKER_CONFIG_TOML"; /// The `tracker.toml` file location. -pub const ENV_VAR_PATH_CONFIG: &str = "TORRUST_TRACKER_PATH_CONFIG"; +pub const ENV_VAR_CONFIG_TOML_PATH: &str = "TORRUST_TRACKER_CONFIG_TOML_PATH"; /// Env var to overwrite API admin token. -/// Deprecated: use `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. -const ENV_VAR_API_ADMIN_TOKEN: &str = "TORRUST_TRACKER_API_ADMIN_TOKEN"; +const ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN"; pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; @@ -75,9 +74,9 @@ impl Info { /// #[allow(clippy::needless_pass_by_value)] pub fn new(default_config_toml_path: String) -> Result { - let env_var_config_toml = ENV_VAR_CONFIG.to_string(); - let env_var_config_toml_path = ENV_VAR_PATH_CONFIG.to_string(); - let env_var_api_admin_token = ENV_VAR_API_ADMIN_TOKEN.to_string(); + let env_var_config_toml = ENV_VAR_CONFIG_TOML.to_string(); + let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); + let env_var_api_admin_token = ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); @@ -156,7 +155,7 @@ impl AnnouncePolicy { pub enum Error { /// Unable to load the configuration from the environment variable. /// This error only occurs if there is no configuration file and the - /// `TORRUST_TRACKER_CONFIG` environment variable is not set. + /// `TORRUST_TRACKER_CONFIG_TOML` environment variable is not set. #[error("Unable to load from Environmental Variable: {source}")] UnableToLoadFromEnvironmentVariable { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8e15d65ca..b9d75c71d 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -9,7 +9,7 @@ //! with the same content as the file. //! //! Configuration can not only be loaded from a file, but also from an -//! environment variable `TORRUST_TRACKER_CONFIG`. This is useful when running +//! environment variable `TORRUST_TRACKER_CONFIG_TOML`. This is useful when running //! the tracker in a Docker container or environments where you do not have a //! persistent storage or you cannot inject a configuration file. Refer to //! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index aeb4d0d4e..47a837a9b 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -52,7 +52,9 @@ pub struct NumberOfBytes(pub i64); /// For more information about persistence. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] pub enum DatabaseDriver { - // TODO: Move to the database crate once that gets its own crate. + // TODO: + // - Move to the database crate once that gets its own crate. + // - Rename serialized values to lowercase: `sqlite3` and `mysql`. /// The Sqlite3 database driver. Sqlite3, /// The `MySQL` database driver. diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 4f98e6622..8c704ea67 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,29 +26,29 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_DATABASE_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_DATABASE_DRIVER" "sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "Sqlite3"; then - # Select sqlite3 empty database + # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" - # Select sqlite3 default configuration + # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_DATABASE_DRIVER" "mysql"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "MySQL"; then - # (no database file needed for mysql) + # (no database file needed for MySQL) - # Select default mysql configuration + # Select default MySQL configuration default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_DATABASE_DRIVER\"." - echo "Please Note: Supported Database Types: \"sqlite3\", \"mysql\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\"." + echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_DATABASE_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs index 03dfd9a2f..6b607bd6f 100644 --- a/src/bootstrap/config.rs +++ b/src/bootstrap/config.rs @@ -1,6 +1,6 @@ //! Initialize configuration from file or env var. //! -//! All environment variables are prefixed with `TORRUST_TRACKER_BACK_`. +//! All environment variables are prefixed with `TORRUST_TRACKER_`. use torrust_tracker_configuration::{Configuration, Info}; @@ -11,7 +11,7 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// There are two methods to inject the configuration: /// /// 1. By using a config file: `tracker.toml`. -/// 2. Environment variable: `TORRUST_TRACKER_CONFIG`. The variable contains the same contents as the `tracker.toml` file. +/// 2. Environment variable: `TORRUST_TRACKER_CONFIG_TOML`. The variable contains the same contents as the `tracker.toml` file. /// /// Environment variable has priority over the config file. /// @@ -20,7 +20,7 @@ pub const DEFAULT_PATH_CONFIG: &str = "./share/default/config/tracker.developmen /// # Panics /// /// Will panic if it can't load the configuration from either -/// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG`. +/// `./tracker.toml` file or the env var `TORRUST_TRACKER_CONFIG_TOML`. #[must_use] pub fn initialize_configuration() -> Configuration { let info = Info::new(DEFAULT_PATH_CONFIG.to_string()).expect("info to load configuration is not valid"); diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index 28f1c4a18..be83b5650 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -46,7 +46,7 @@ pub fn run() { // Besides, if we don't use port 0 we should get the port numbers from the tracker configuration. // We could not use docker, but the intention was to create E2E tests including containerization. let options = RunOptions { - env_vars: vec![("TORRUST_TRACKER_CONFIG".to_string(), tracker_config.to_string())], + env_vars: vec![("TORRUST_TRACKER_CONFIG_TOML".to_string(), tracker_config.to_string())], ports: vec![ "6969:6969/udp".to_string(), "7070:7070/tcp".to_string(), diff --git a/src/console/profiling.rs b/src/console/profiling.rs index 9bf085950..42ee1ed5d 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -27,7 +27,7 @@ //! //! ```text //! RUSTFLAGS='-g' cargo build --release --bin profiling \ -//! && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ +//! && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ //! && valgrind \ //! --tool=callgrind \ //! --callgrind-out-file=callgrind.out \ @@ -40,7 +40,7 @@ //! //! ```text //! RUSTFLAGS='-g' cargo build --release --bin profiling \ -//! && export TORRUST_TRACKER_PATH_CONFIG="./share/default/config/tracker.udp.benchmarking.toml" \ +//! && export TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.udp.benchmarking.toml" \ //! && valgrind \ //! --tool=callgrind \ //! --callgrind-out-file=callgrind.out \ diff --git a/src/lib.rs b/src/lib.rs index 22bc133e1..6fd5da15f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,10 +213,10 @@ //! documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). //! //! Alternatively to the `tracker.toml` file you can use one environment -//! variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: +//! variable `TORRUST_TRACKER_CONFIG_TOML` to pass the configuration to the tracker: //! //! ```text -//! TORRUST_TRACKER_CONFIG=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker +//! TORRUST_TRACKER_CONFIG_TOML=$(cat ./share/default/config/tracker.development.sqlite3.toml) ./target/release/torrust-tracker //! ``` //! //! In the previous example you are just setting the env var with the contents @@ -225,7 +225,7 @@ //! The env var contains the same data as the `tracker.toml`. It's particularly //! useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). //! -//! > NOTICE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `tracker.toml` file. +//! > NOTICE: The `TORRUST_TRACKER_CONFIG_TOML` env var has priority over the `tracker.toml` file. //! //! By default, if you don’t specify any `tracker.toml` file, the application //! will use `./share/default/config/tracker.development.sqlite3.toml`. From e43b71f90ea660cbaf6a7e1080c04b90f558b732 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 15:27:37 +0100 Subject: [PATCH 176/217] feat!: remove deprecated env var The env var `TORRUST_TRACKER_API_ADMIN_TOKEN` was replaced with `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN`. After the migration to Figment all configuration options can be overwritten. --- packages/configuration/src/lib.rs | 6 ----- packages/configuration/src/v1/mod.rs | 34 +--------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 9dc4987e6..96d1091a1 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -39,9 +39,6 @@ const ENV_VAR_CONFIG_TOML: &str = "TORRUST_TRACKER_CONFIG_TOML"; /// The `tracker.toml` file location. pub const ENV_VAR_CONFIG_TOML_PATH: &str = "TORRUST_TRACKER_CONFIG_TOML_PATH"; -/// Env var to overwrite API admin token. -const ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN"; - pub type Configuration = v1::Configuration; pub type UdpTracker = v1::udp_tracker::UdpTracker; pub type HttpTracker = v1::http_tracker::HttpTracker; @@ -62,7 +59,6 @@ pub struct TrackerPolicy { pub struct Info { config_toml: Option, config_toml_path: String, - api_admin_token: Option, } impl Info { @@ -76,7 +72,6 @@ impl Info { pub fn new(default_config_toml_path: String) -> Result { let env_var_config_toml = ENV_VAR_CONFIG_TOML.to_string(); let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); - let env_var_api_admin_token = ENV_VAR_HTTP_API_ACCESS_TOKENS_ADMIN.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { println!("Loading configuration from environment variable {config_toml} ..."); @@ -96,7 +91,6 @@ impl Info { Ok(Self { config_toml, config_toml_path, - api_admin_token: env::var(env_var_api_admin_token).ok(), }) } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index b9d75c71d..8d45270b8 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -288,10 +288,6 @@ impl Default for Configuration { } impl Configuration { - fn override_api_admin_token(&mut self, api_admin_token: &str) { - self.http_api.override_admin_token(api_admin_token); - } - /// Returns the tracker public IP address id defined in the configuration, /// and `None` otherwise. #[must_use] @@ -331,11 +327,7 @@ impl Configuration { .merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR)) }; - let mut config: Configuration = figment.extract()?; - - if let Some(ref token) = info.api_admin_token { - config.override_api_admin_token(token); - }; + let config: Configuration = figment.extract()?; Ok(config) } @@ -469,7 +461,6 @@ mod tests { let info = Info { config_toml: Some(empty_configuration), config_toml_path: "tracker.toml".to_string(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -491,7 +482,6 @@ mod tests { let info = Info { config_toml: Some(config_toml), config_toml_path: String::new(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -515,7 +505,6 @@ mod tests { let info = Info { config_toml: None, config_toml_path: "tracker.toml".to_string(), - api_admin_token: None, }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); @@ -534,27 +523,6 @@ mod tests { let info = Info { config_toml: Some(default_config_toml()), config_toml_path: String::new(), - api_admin_token: None, - }; - - let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - - assert_eq!( - configuration.http_api.access_tokens.get("admin"), - Some("NewToken".to_owned()).as_ref() - ); - - Ok(()) - }); - } - - #[test] - fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { - figment::Jail::expect_with(|_jail| { - let info = Info { - config_toml: Some(default_config_toml()), - config_toml_path: String::new(), - api_admin_token: Some("NewToken".to_owned()), }; let configuration = Configuration::load(&info).expect("Could not load configuration from file"); From df10a19cfc9582f3a4ecce04cc227134b02da9f8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 17:27:08 +0100 Subject: [PATCH 177/217] docs: update README - Fix missing line break after badgets. - Add roadmap draft. --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 754d2d5b7..ebd7c357b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Torrust Tracker -[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf]**Torrust Tracker** is a [BitTorrent][bittorrent] Tracker that matchmakes peers and collects statistics. Written in [Rust Language][rust] with the [Axum] web framework. _**This tracker aims to be respectful to established standards, (both [formal][BEP 00] and [otherwise][torrent_source_felid]).___ +[![container_wf_b]][container_wf] [![coverage_wf_b]][coverage_wf] [![deployment_wf_b]][deployment_wf] [![testing_wf_b]][testing_wf] + +**Torrust Tracker** is a [BitTorrent][bittorrent] Tracker that matchmakes peers and collects statistics. Written in [Rust Language][rust] with the [Axum] web framework. **This tracker aims to be respectful to established standards, (both [formal][BEP 00] and [otherwise][torrent_source_felid]).** > This is a [Torrust][torrust] project and is in active development. It is community supported as well as sponsored by [Nautilus Cyberneering][nautilus]. @@ -17,6 +19,44 @@ - [x] Support [newTrackon][newtrackon] checks. - [x] Persistent `SQLite3` or `MySQL` Databases. +## Roadmap + +Persistence: + +- [ ] Support other databases. + +Integrations: + +- [ ] Webhooks. + +Administration: + +- [ ] Improve categories and tag management. +- [ ] User management: list, search and ban users. +- [ ] Full-private mode. +- [ ] User statistics. + +Users: + +- [ ] Reset or change the password. +- [ ] User profile. +- [ ] Invitation system. +- [ ] User moderation. +- [ ] Add torrent providing only the info-hash. +- [ ] Improve search. + +Torrents: + +- [ ] Change the source field. +- [ ] Change the creator field. +- [ ] Implement BEP 19: WebSeed - HTTP/FTP Seeding (GetRight style). +- [ ] Implement BEP 32: BitTorrent DHT Extensions for IPv6. + +Others: + +- [ ] Multi-tracker +- [ ] Multi-language + ## Implemented BitTorrent Enhancement Proposals (BEPs) > > _[Learn more about BitTorrent Enhancement Proposals][BEP 00]_ From 66ca9046fb8c2f2e41d7cb46795818f5ca4cbaf6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 14 May 2024 18:01:47 +0100 Subject: [PATCH 178/217] docs: udpate roadmap in README I copied the Index roadmap isntead of the tracker one by mistake. --- README.md | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ebd7c357b..f83dbd936 100644 --- a/README.md +++ b/README.md @@ -21,41 +21,39 @@ ## Roadmap +Core: + +- [ ] New option `want_ip_from_query_string`. See . +- [ ] Permanent keys. See . +- [ ] Peer and torrents specific statistics. See . + Persistence: -- [ ] Support other databases. +- [ ] Support other databases like PostgreSQL. -Integrations: +Performance: -- [ ] Webhooks. +- [ ] More optimizations. See . -Administration: +Protocols: -- [ ] Improve categories and tag management. -- [ ] User management: list, search and ban users. -- [ ] Full-private mode. -- [ ] User statistics. +- [ ] WebTorrent. -Users: +Integrations: -- [ ] Reset or change the password. -- [ ] User profile. -- [ ] Invitation system. -- [ ] User moderation. -- [ ] Add torrent providing only the info-hash. -- [ ] Improve search. +- [ ] Monitoring (Prometheus). -Torrents: +Utils: -- [ ] Change the source field. -- [ ] Change the creator field. -- [ ] Implement BEP 19: WebSeed - HTTP/FTP Seeding (GetRight style). -- [ ] Implement BEP 32: BitTorrent DHT Extensions for IPv6. +- [ ] Tracker client. +- [ ] Tracker checker. Others: -- [ ] Multi-tracker -- [ ] Multi-language +- [ ] Support for Windows. +- [ ] Docker images for other architectures. + + ## Implemented BitTorrent Enhancement Proposals (BEPs) > From 788731906fed39ce2c4552b24162f7da74ed0606 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:25:58 +0100 Subject: [PATCH 179/217] chore(deps): add cargo dependencies needed for axum timeouts We want to add timeouts for the Axum server configuration, in order to close HTTP connections when the client doesn't send any request after opening a HTTP connection. --- Cargo.lock | 5 +++++ Cargo.toml | 5 +++++ cSpell.json | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index dbd9165ac..2d7907e8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3594,14 +3594,18 @@ dependencies = [ "fern", "figment", "futures", + "futures-util", "hex-literal", + "http-body", "hyper", + "hyper-util", "lazy_static", "local-ip-address", "mockall", "multimap", "parking_lot", "percent-encoding", + "pin-project-lite", "r2d2", "r2d2_mysql", "r2d2_sqlite", @@ -3623,6 +3627,7 @@ dependencies = [ "torrust-tracker-primitives", "torrust-tracker-test-helpers", "torrust-tracker-torrent-repository", + "tower", "tower-http", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index c8086953b..08a5f071d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,12 +46,16 @@ derive_more = "0" fern = "0" figment = "0.10.18" futures = "0" +futures-util = "0.3.30" hex-literal = "0" +http-body = "1.0.0" hyper = "1" +hyper-util = { version = "0.1.3", features = ["http1", "http2", "tokio"] } lazy_static = "1" multimap = "0" parking_lot = "0.12.1" percent-encoding = "2" +pin-project-lite = "0.2.14" r2d2 = "0" r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } @@ -71,6 +75,7 @@ torrust-tracker-contrib-bencode = { version = "3.0.0-alpha.12-develop", path = " torrust-tracker-located-error = { version = "3.0.0-alpha.12-develop", path = "packages/located-error" } torrust-tracker-primitives = { version = "3.0.0-alpha.12-develop", path = "packages/primitives" } torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path = "packages/torrent-repository" } +tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } tracing = "0" tracing-subscriber = "0" diff --git a/cSpell.json b/cSpell.json index f02277ec7..c138bf5b7 100644 --- a/cSpell.json +++ b/cSpell.json @@ -51,6 +51,7 @@ "downloadedi", "dtolnay", "elif", + "Eray", "filesd", "flamegraph", "Freebox", @@ -74,6 +75,7 @@ "intervali", "Joakim", "kallsyms", + "Karatay", "kcachegrind", "kexec", "keyout", @@ -108,6 +110,7 @@ "Pando", "peekable", "peerlist", + "programatik", "proot", "proto", "Quickstart", @@ -138,6 +141,7 @@ "sharktorrent", "SHLVL", "skiplist", + "slowloris", "socketaddr", "sqllite", "subsec", From 4cf32aae5b08854f2fcd8e0ddddb6e6caadfb852 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:27:50 +0100 Subject: [PATCH 180/217] fix: [#612] add timeout for time waiting for the first API requests Adds a timeout for the Tracker API for the time the server waits for the first request from the client after openning a new HTTP connection. --- src/servers/apis/server.rs | 7 +- src/servers/custom_axum_server.rs | 275 ++++++++++++++++++++++++++++++ src/servers/mod.rs | 1 + 3 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 src/servers/custom_axum_server.rs diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 60f4714fb..d79d2b69d 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -35,6 +35,7 @@ use tracing::info; use super::routes::router; use crate::core::Tracker; +use crate::servers::custom_axum_server::{self, TimeoutAcceptor}; use crate::servers::registar::{self, HealthCheckFactory, HeathCheckFuture, HeathCheckResult}; use crate::servers::service::{self, AddrFuture, Error, TaskFuture}; use crate::servers::signals::Halted; @@ -168,14 +169,16 @@ impl service::Launcher for ApiLauncher { router(self.tracker, self.access_tokens, &addr).into_make_service_with_connect_info::(); match self.tls.clone() { - Some(tls) => axum_server::from_tcp_rustls(listener, tls) + Some(tls) => custom_axum_server::from_tcp_rustls_with_timeouts(listener, tls) .handle(handle.axum_handle.clone()) + .acceptor(TimeoutAcceptor) .serve(make_service) .map_err(|e| Error::UnableToServe { err: e.into() }) .boxed(), - None => axum_server::from_tcp(listener) + None => custom_axum_server::from_tcp_with_timeouts(listener) .handle(handle.axum_handle.clone()) + .acceptor(TimeoutAcceptor) .serve(make_service) .map_err(|e| Error::UnableToServe { err: e.into() }) .boxed(), diff --git a/src/servers/custom_axum_server.rs b/src/servers/custom_axum_server.rs new file mode 100644 index 000000000..5705ef24e --- /dev/null +++ b/src/servers/custom_axum_server.rs @@ -0,0 +1,275 @@ +//! Wrapper for Axum server to add timeouts. +//! +//! Copyright (c) Eray Karatay ([@programatik29](https://github.com/programatik29)). +//! +//! See: . +//! +//! If a client opens a HTTP connection and it does not send any requests, the +//! connection is closed after a timeout. You can test it with: +//! +//! ```text +//! telnet 127.0.0.1 1212 +//! Trying 127.0.0.1... +//! Connected to 127.0.0.1. +//! Escape character is '^]'. +//! Connection closed by foreign host. +//! ``` +//! +//! If you want to know more about Axum and timeouts see . +use std::future::Ready; +use std::io::ErrorKind; +use std::net::TcpListener; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +use axum_server::accept::Accept; +use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig}; +use axum_server::Server; +use futures_util::{ready, Future}; +use http_body::{Body, Frame}; +use hyper::Response; +use hyper_util::rt::TokioTimer; +use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use tokio::time::{Instant, Sleep}; +use tower::Service; + +const HTTP1_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(5); +const HTTP2_KEEP_ALIVE_TIMEOUT: Duration = Duration::from_secs(5); +const HTTP2_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(5); + +#[must_use] +pub fn from_tcp_with_timeouts(socket: TcpListener) -> Server { + add_timeouts(axum_server::from_tcp(socket)) +} + +#[must_use] +pub fn from_tcp_rustls_with_timeouts(socket: TcpListener, tls: RustlsConfig) -> Server { + add_timeouts(axum_server::from_tcp_rustls(socket, tls)) +} + +fn add_timeouts(mut server: Server) -> Server { + server.http_builder().http1().timer(TokioTimer::new()); + server.http_builder().http2().timer(TokioTimer::new()); + + server.http_builder().http1().header_read_timeout(HTTP1_HEADER_READ_TIMEOUT); + server + .http_builder() + .http2() + .keep_alive_timeout(HTTP2_KEEP_ALIVE_TIMEOUT) + .keep_alive_interval(HTTP2_KEEP_ALIVE_INTERVAL); + + server +} + +#[derive(Clone)] +pub struct TimeoutAcceptor; + +impl Accept for TimeoutAcceptor { + type Stream = TimeoutStream; + type Service = TimeoutService; + type Future = Ready>; + + fn accept(&self, stream: I, service: S) -> Self::Future { + let (tx, rx) = mpsc::unbounded_channel(); + + let stream = TimeoutStream::new(stream, HTTP1_HEADER_READ_TIMEOUT, rx); + let service = TimeoutService::new(service, tx); + + std::future::ready(Ok((stream, service))) + } +} + +#[derive(Clone)] +pub struct TimeoutService { + inner: S, + sender: UnboundedSender, +} + +impl TimeoutService { + fn new(inner: S, sender: UnboundedSender) -> Self { + Self { inner, sender } + } +} + +impl Service for TimeoutService +where + S: Service>, +{ + type Response = Response>; + type Error = S::Error; + type Future = TimeoutServiceFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + // send timer wait signal + let _ = self.sender.send(TimerSignal::Wait); + + TimeoutServiceFuture::new(self.inner.call(req), self.sender.clone()) + } +} + +pin_project! { + pub struct TimeoutServiceFuture { + #[pin] + inner: F, + sender: Option>, + } +} + +impl TimeoutServiceFuture { + fn new(inner: F, sender: UnboundedSender) -> Self { + Self { + inner, + sender: Some(sender), + } + } +} + +impl Future for TimeoutServiceFuture +where + F: Future, E>>, +{ + type Output = Result>, E>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + this.inner.poll(cx).map(|result| { + result.map(|response| { + response.map(|body| TimeoutBody::new(body, this.sender.take().expect("future polled after ready"))) + }) + }) + } +} + +enum TimerSignal { + Wait, + Reset, +} + +pin_project! { + pub struct TimeoutBody { + #[pin] + inner: B, + sender: UnboundedSender, + } +} + +impl TimeoutBody { + fn new(inner: B, sender: UnboundedSender) -> Self { + Self { inner, sender } + } +} + +impl Body for TimeoutBody { + type Data = B::Data; + type Error = B::Error; + + fn poll_frame(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll, Self::Error>>> { + let this = self.project(); + let option = ready!(this.inner.poll_frame(cx)); + + if option.is_none() { + let _ = this.sender.send(TimerSignal::Reset); + } + + Poll::Ready(option) + } + + fn is_end_stream(&self) -> bool { + let is_end_stream = self.inner.is_end_stream(); + + if is_end_stream { + let _ = self.sender.send(TimerSignal::Reset); + } + + is_end_stream + } + + fn size_hint(&self) -> http_body::SizeHint { + self.inner.size_hint() + } +} + +pub struct TimeoutStream { + inner: IO, + // hyper requires unpin + sleep: Pin>, + duration: Duration, + waiting: bool, + receiver: UnboundedReceiver, + finished: bool, +} + +impl TimeoutStream { + fn new(inner: IO, duration: Duration, receiver: UnboundedReceiver) -> Self { + Self { + inner, + sleep: Box::pin(tokio::time::sleep(duration)), + duration, + waiting: false, + receiver, + finished: false, + } + } +} + +impl AsyncRead for TimeoutStream { + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { + if !self.finished { + match Pin::new(&mut self.receiver).poll_recv(cx) { + // reset the timer + Poll::Ready(Some(TimerSignal::Reset)) => { + self.waiting = false; + + let deadline = Instant::now() + self.duration; + self.sleep.as_mut().reset(deadline); + } + // enter waiting mode (for response body last chunk) + Poll::Ready(Some(TimerSignal::Wait)) => self.waiting = true, + Poll::Ready(None) => self.finished = true, + Poll::Pending => (), + } + } + + if !self.waiting { + // return error if timer is elapsed + if let Poll::Ready(()) = self.sleep.as_mut().poll(cx) { + return Poll::Ready(Err(std::io::Error::new(ErrorKind::TimedOut, "request header read timed out"))); + } + } + + Pin::new(&mut self.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for TimeoutStream { + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { + Pin::new(&mut self.inner).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } +} diff --git a/src/servers/mod.rs b/src/servers/mod.rs index bd2e90ba6..a7eb1f311 100644 --- a/src/servers/mod.rs +++ b/src/servers/mod.rs @@ -1,5 +1,6 @@ //! Servers. Services that can be started and stopped. pub mod apis; +pub mod custom_axum_server; pub mod health_check_api; pub mod http; pub mod registar; From c52d72b0b0003a26d92c8b81ebe513d93f21973e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 12:51:25 +0100 Subject: [PATCH 181/217] feat: [#612] tower middleware to apply timeouts to requests --- src/servers/apis/routes.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs index 8d5f4e013..0121f4067 100644 --- a/src/servers/apis/routes.rs +++ b/src/servers/apis/routes.rs @@ -9,12 +9,15 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; +use axum::error_handling::HandleErrorLayer; use axum::http::HeaderName; use axum::response::Response; use axum::routing::get; -use axum::{middleware, Router}; -use hyper::Request; +use axum::{middleware, BoxError, Router}; +use hyper::{Request, StatusCode}; use torrust_tracker_configuration::AccessTokens; +use tower::timeout::TimeoutLayer; +use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; @@ -26,6 +29,8 @@ use super::v1::context::health_check::handlers::health_check_handler; use super::v1::middlewares::auth::State; use crate::core::Tracker; +const TIMEOUT: Duration = Duration::from_secs(5); + /// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc, access_tokens: Arc, &_: &SocketAddr) -> Router { @@ -74,4 +79,11 @@ pub fn router(tracker: Arc, access_tokens: Arc, &_: &Sock }), ) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer( + ServiceBuilder::new() + // this middleware goes above `TimeoutLayer` because it will receive + // errors returned by `TimeoutLayer` + .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) + .layer(TimeoutLayer::new(TIMEOUT)), + ) } From 1f9012f9ff4faef182254d742fc6bad5e9dd849c Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 15 May 2024 15:31:15 +0100 Subject: [PATCH 182/217] fix: [#613] add timeout for time waiting for the first HTTP tracker request Adds a timeout to the HTTP tracker for the time the server waits for the first request from the client after openning a new HTTP connection. It also adds a tower middleware for timeouts in requests. --- src/servers/http/launcher.rs | 9 ++++++--- src/servers/http/v1/routes.rs | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs index 4dc8a8992..ec72ccafd 100644 --- a/src/servers/http/launcher.rs +++ b/src/servers/http/launcher.rs @@ -8,7 +8,8 @@ use super::check::check_builder; use super::handle::Handle; use super::v1::routes::router; use crate::core::Tracker; -use crate::servers::{registar, service}; +use crate::servers::custom_axum_server::TimeoutAcceptor; +use crate::servers::{custom_axum_server, registar, service}; #[derive(Constructor, Clone, Debug, Display)] #[display(fmt = "intended_address: {addr}, with tracker, and {}", "self.have_tls()")] @@ -54,14 +55,16 @@ impl service::Launcher for Launcher { let make_service = router(self.tracker, &addr).into_make_service_with_connect_info::(); match self.tls.clone() { - Some(tls) => axum_server::from_tcp_rustls(listener, tls) + Some(tls) => custom_axum_server::from_tcp_rustls_with_timeouts(listener, tls) .handle(handle.axum_handle.clone()) + .acceptor(TimeoutAcceptor) .serve(make_service) .map_err(|e| service::Error::UnableToServe { err: e.into() }) .boxed(), - None => axum_server::from_tcp(listener) + None => custom_axum_server::from_tcp_with_timeouts(listener) .handle(handle.axum_handle.clone()) + .acceptor(TimeoutAcceptor) .serve(make_service) .map_err(|e| service::Error::UnableToServe { err: e.into() }) .boxed(), diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index a04fb280f..f97afc491 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -3,12 +3,15 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; +use axum::error_handling::HandleErrorLayer; use axum::http::HeaderName; use axum::response::Response; use axum::routing::get; -use axum::Router; +use axum::{BoxError, Router}; use axum_client_ip::SecureClientIpSource; -use hyper::Request; +use hyper::{Request, StatusCode}; +use tower::timeout::TimeoutLayer; +use tower::ServiceBuilder; use tower_http::compression::CompressionLayer; use tower_http::propagate_header::PropagateHeaderLayer; use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer}; @@ -18,6 +21,8 @@ use tracing::{Level, Span}; use super::handlers::{announce, health_check, scrape}; use crate::core::Tracker; +const TIMEOUT: Duration = Duration::from_secs(5); + /// It adds the routes to the router. /// /// > **NOTICE**: it's added a layer to get the client IP from the connection @@ -69,4 +74,11 @@ pub fn router(tracker: Arc, &addr: &SocketAddr) -> Router { }), ) .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid)) + .layer( + ServiceBuilder::new() + // this middleware goes above `TimeoutLayer` because it will receive + // errors returned by `TimeoutLayer` + .layer(HandleErrorLayer::new(|_: BoxError| async { StatusCode::REQUEST_TIMEOUT })) + .layer(TimeoutLayer::new(TIMEOUT)), + ) } From 4d59486a521c312741ed25e285f12e374c706b46 Mon Sep 17 00:00:00 2001 From: Gabriel Grondin Date: Sun, 19 May 2024 19:34:08 +0200 Subject: [PATCH 183/217] Fix REAADME HTTP port --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f83dbd936..306a8620c 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ The following services are provided by the default configuration: - UDP _(tracker)_ - `udp://127.0.0.1:6969/announce`. - HTTP _(tracker)_ - - `http://127.0.0.1:6969/announce`. + - `http://127.0.0.1:7070/announce`. - API _(management)_ - `http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken`. From f0e57c8cdd7a3379ee72d95e199d25d7dfd85da8 Mon Sep 17 00:00:00 2001 From: Gabriel GRONDIN Date: Mon, 20 May 2024 18:12:54 +0200 Subject: [PATCH 184/217] Fix and improved bootstrap jobs module test. On O.S. with different language than english the BadTlsConfig source error message will differ from the compared one. This will fix this issue by matching the enum itself instead of relying on a string compare. --- src/bootstrap/jobs/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 541f45330..790b3d22a 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -105,7 +105,7 @@ mod tests { use camino::Utf8PathBuf; use torrust_tracker_configuration::TslConfig; - use super::make_rust_tls; + use super::{make_rust_tls, Error}; #[tokio::test] async fn it_should_error_on_bad_tls_config() { @@ -120,9 +120,7 @@ mod tests { .expect("tls_was_enabled") .expect_err("bad_cert_and_key_files"); - assert!(err - .to_string() - .contains("bad tls config: No such file or directory (os error 2)")); + assert!(matches!(err, Error::BadTlsConfig { source: _ })); } #[tokio::test] @@ -138,6 +136,6 @@ mod tests { .expect("tls_was_enabled") .expect_err("missing_config"); - assert_eq!(err.to_string(), "tls config missing"); + assert!(matches!(err, Error::MissingTlsConfig { location: _ })); } } From a743f01d81af64eb2ea55daf123d14cd76c3aa0a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 15:44:08 +0100 Subject: [PATCH 185/217] feat: [#870] add privacy methods to the TrackerMode The tracker mode can be: - Public (Non-whitelisted) - Listed (Whitelisted) - Private (Non-whitelisted) - PrivateListed (Whitelisted) They should have been two different flags (in my opinion): - Visibility: public or private - Whitelisted: true or false So we would have the same four convinations: - Not whitelisted: - Public - Private - Whitelisted - Public - Private That's a pending refactor. For this commits, the goal is just to align this enum with what we added to the Index so we can use this enum in the Index via the primmitives crate. --- packages/primitives/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index 47a837a9b..aa3af27e3 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -85,3 +85,21 @@ pub enum TrackerMode { #[serde(rename = "private_listed")] PrivateListed, } + +impl Default for TrackerMode { + fn default() -> Self { + Self::Public + } +} + +impl TrackerMode { + #[must_use] + pub fn is_open(&self) -> bool { + matches!(self, TrackerMode::Public | TrackerMode::Listed) + } + + #[must_use] + pub fn is_close(&self) -> bool { + !self.is_open() + } +} From a2e60c2c17ee83233d82e2b9b77a920d68b670b0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 16:05:39 +0100 Subject: [PATCH 186/217] feat: [#870] remove Copy trait from TrackerMode --- packages/primitives/src/lib.rs | 2 +- src/core/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index aa3af27e3..eccd220f9 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -67,7 +67,7 @@ pub type PersistentTorrents = BTreeMap; /// /// Refer to [Torrust Tracker Configuration](https://docs.rs/torrust-tracker-configuration) /// to know how to configure the tracker to run in each mode. -#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub enum TrackerMode { /// Will track every new info hash and serve every peer. #[serde(rename = "public")] diff --git a/src/core/mod.rs b/src/core/mod.rs index 83206d87f..227f0cb29 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -549,7 +549,7 @@ impl Tracker { ) -> Result { let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); - let mode = config.mode; + let mode = config.mode.clone(); Ok(Tracker { //config, From 79bfb4a47c1863e006b5f90babef7a9ca5e1106a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 17 May 2024 16:19:20 +0100 Subject: [PATCH 187/217] feat: [#870] implement traits Dispaly and FromStr for TrackerMode --- packages/primitives/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index eccd220f9..454635e8d 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -5,6 +5,8 @@ //! by the tracker server crate, but also by other crates in the Torrust //! ecosystem. use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; use std::time::Duration; use info_hash::InfoHash; @@ -92,6 +94,32 @@ impl Default for TrackerMode { } } +impl fmt::Display for TrackerMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display_str = match self { + TrackerMode::Public => "public", + TrackerMode::Listed => "listed", + TrackerMode::Private => "private", + TrackerMode::PrivateListed => "private_listed", + }; + write!(f, "{display_str}") + } +} + +impl FromStr for TrackerMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "public" => Ok(TrackerMode::Public), + "listed" => Ok(TrackerMode::Listed), + "private" => Ok(TrackerMode::Private), + "private_listed" => Ok(TrackerMode::PrivateListed), + _ => Err(format!("Unknown tracker mode: {s}")), + } + } +} + impl TrackerMode { #[must_use] pub fn is_open(&self) -> bool { From 54c513f9efe10a53ca4ed549f24b8c470aa5a654 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 May 2024 09:23:18 +0100 Subject: [PATCH 188/217] chore(deps): update dependencies ```output $ cargo update Updating crates.io index Updating addr2line v0.21.0 -> v0.22.0 (latest: v0.23.0) Updating anyhow v1.0.83 -> v1.0.86 Updating async-channel v2.3.0 -> v2.3.1 Updating async-compression v0.4.10 -> v0.4.11 Updating async-executor v1.11.0 -> v1.12.0 Updating backtrace v0.3.71 -> v0.3.72 Updating blocking v1.6.0 -> v1.6.1 Updating brotli-decompressor v4.0.0 -> v4.0.1 Updating camino v1.1.6 -> v1.1.7 Updating cc v1.0.97 -> v1.0.98 Updating clang-sys v1.7.0 -> v1.8.1 Updating crc32fast v1.4.0 -> v1.4.2 Updating crossbeam-channel v0.5.12 -> v0.5.13 Updating crossbeam-utils v0.8.19 -> v0.8.20 Updating darling v0.20.8 -> v0.20.9 Updating darling_core v0.20.8 -> v0.20.9 Updating darling_macro v0.20.8 -> v0.20.9 Updating either v1.11.0 -> v1.12.0 Updating event-listener v5.3.0 -> v5.3.1 Updating figment v0.10.18 -> v0.10.19 Updating gimli v0.28.1 -> v0.29.0 (latest: v0.30.0) Updating h2 v0.4.4 -> v0.4.5 Updating hyper-util v0.1.3 -> v0.1.5 Updating instant v0.1.12 -> v0.1.13 Updating libc v0.2.154 -> v0.2.155 Updating libz-sys v1.1.16 -> v1.1.18 Updating linux-raw-sys v0.4.13 -> v0.4.14 (latest: v0.6.4) Updating miniz_oxide v0.7.2 -> v0.7.3 Updating native-tls v0.2.11 -> v0.2.12 Updating object v0.32.2 -> v0.35.0 (latest: v0.36.0) Updating parking_lot v0.12.2 -> v0.12.3 Updating plotters v0.3.5 -> v0.3.6 Updating plotters-backend v0.3.5 -> v0.3.6 Updating plotters-svg v0.3.5 -> v0.3.6 Updating proc-macro2 v1.0.82 -> v1.0.84 Updating ringbuf v0.4.0 -> v0.4.1 Updating rstest v0.19.0 -> v0.20.0 Updating rstest_macros v0.19.0 -> v0.20.0 Updating rustversion v1.0.16 -> v1.0.17 Updating serde v1.0.201 -> v1.0.203 Updating serde_derive v1.0.201 -> v1.0.203 Updating serde_spanned v0.6.5 -> v0.6.6 Removing strsim v0.10.0 Updating syn v2.0.63 -> v2.0.66 Updating thiserror v1.0.60 -> v1.0.61 Updating thiserror-impl v1.0.60 -> v1.0.61 Updating tokio v1.37.0 -> v1.38.0 Updating tokio-macros v2.2.0 -> v2.3.0 Updating toml v0.8.12 -> v0.8.13 Updating toml_datetime v0.6.5 -> v0.6.6 Updating toml_edit v0.22.12 -> v0.22.13 Updating winnow v0.6.8 -> v0.6.9 ``` --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d7907e8f..36c9ed1d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3237,9 +3237,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" From 6df2fedd3b4ce61e959ea2956026369dbdac4f03 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 31 May 2024 09:50:57 +0100 Subject: [PATCH 189/217] fix: clippy errors --- cSpell.json | 1 + packages/clock/src/lib.rs | 6 +- packages/test-helpers/src/configuration.rs | 6 +- src/bootstrap/jobs/http_tracker.rs | 2 +- src/bootstrap/jobs/tracker_apis.rs | 8 +- src/bootstrap/jobs/udp_tracker.rs | 4 +- src/console/profiling.rs | 8 +- src/core/databases/mod.rs | 4 +- src/core/mod.rs | 10 +- src/core/torrent/mod.rs | 6 +- src/lib.rs | 6 +- src/servers/apis/mod.rs | 22 ++-- .../apis/v1/context/auth_key/handlers.rs | 2 +- src/servers/apis/v1/context/auth_key/mod.rs | 10 +- .../v1/context/torrent/resources/torrent.rs | 4 +- .../apis/v1/context/whitelist/handlers.rs | 4 +- src/servers/apis/v1/context/whitelist/mod.rs | 8 +- src/servers/apis/v1/mod.rs | 2 +- src/servers/http/mod.rs | 54 ++++----- .../http/v1/extractors/authentication_key.rs | 6 +- src/servers/http/v1/requests/announce.rs | 8 +- src/servers/http/v1/responses/error.rs | 4 +- src/servers/http/v1/routes.rs | 2 +- src/servers/http/v1/services/announce.rs | 4 +- src/servers/http/v1/services/scrape.rs | 4 +- src/servers/udp/mod.rs | 108 +++++++++--------- src/servers/udp/peer_builder.rs | 3 +- src/servers/udp/server.rs | 3 +- src/servers/udp/v0/cookie.rs | 10 +- src/servers/udp/v0/handlers.rs | 3 +- 30 files changed, 159 insertions(+), 163 deletions(-) diff --git a/cSpell.json b/cSpell.json index c138bf5b7..1ab0871ed 100644 --- a/cSpell.json +++ b/cSpell.json @@ -65,6 +65,7 @@ "Hydranode", "hyperthread", "Icelake", + "iiiiiiiiiiiiiiiiiiiid", "imdl", "impls", "incompletei", diff --git a/packages/clock/src/lib.rs b/packages/clock/src/lib.rs index 9fc67cb54..295d22c16 100644 --- a/packages/clock/src/lib.rs +++ b/packages/clock/src/lib.rs @@ -17,11 +17,11 @@ //! ``` //! //! > **NOTICE**: internally the `Duration` is stores it's main unit as seconds in a `u64` and it will -//! overflow in 584.9 billion years. +//! > overflow in 584.9 billion years. //! //! > **NOTICE**: the timestamp does not depend on the time zone. That gives you -//! the ability to use the clock regardless of the underlying system time zone -//! configuration. See [Unix time Wikipedia entry](https://en.wikipedia.org/wiki/Unix_time). +//! > the ability to use the clock regardless of the underlying system time zone +//! > configuration. See [Unix time Wikipedia entry](https://en.wikipedia.org/wiki/Unix_time). pub mod clock; pub mod conv; diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 86ed57b9e..15ecd5280 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -13,11 +13,11 @@ use crate::random; /// > **NOTICE**: This configuration is not meant to be used in production. /// /// > **NOTICE**: Port 0 is used for ephemeral ports, which means that the OS -/// will assign a random free port for the tracker to use. +/// > will assign a random free port for the tracker to use. /// /// > **NOTICE**: You can change the log level to `debug` to see the logs of the -/// tracker while running the tests. That can be particularly useful when -/// debugging tests. +/// > tracker while running the tests. That can be particularly useful when +/// > debugging tests. /// /// # Panics /// diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index f80c759b1..b1193b7bf 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -3,7 +3,7 @@ //! The function [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) starts a new HTTP tracker server. //! //! > **NOTICE**: the application can launch more than one HTTP tracker on different ports. -//! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. +//! > Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. //! //! The [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) function spawns a new asynchronous task, //! that tasks is the "**launcher**". The "**launcher**" starts the actual server and sends a message back to the main application. diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index c0b78de7d..4aa3fd1f3 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -4,8 +4,8 @@ //! function starts a the HTTP tracker REST API. //! //! > **NOTICE**: that even thought there is only one job the API has different -//! versions. API consumers can choose which version to use. The API version is -//! part of the URL, for example: `http://localhost:1212/api/v1/stats`. +//! > versions. API consumers can choose which version to use. The API version is +//! > part of the URL, for example: `http://localhost:1212/api/v1/stats`. //! //! The [`tracker_apis::start_job`](crate::bootstrap::jobs::tracker_apis::start_job) //! function spawns a new asynchronous task, that tasks is the "**launcher**". @@ -39,8 +39,8 @@ use crate::servers::service::Service; /// application process to notify the API server was successfully started. /// /// > **NOTICE**: it does not mean the API server is ready to receive requests. -/// It only means the new server started. It might take some time to the server -/// to be ready to accept request. +/// > It only means the new server started. It might take some time to the server +/// > to be ready to accept request. #[derive(Debug)] pub struct ApiServerJobStarted(); diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index 21de13475..0bb5a7881 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -4,8 +4,8 @@ //! function starts a new UDP tracker server. //! //! > **NOTICE**: that the application can launch more than one UDP tracker -//! on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) -//! for the configuration options. +//! > on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) +//! > for the configuration options. use std::net::SocketAddr; use std::sync::Arc; diff --git a/src/console/profiling.rs b/src/console/profiling.rs index 42ee1ed5d..139c88933 100644 --- a/src/console/profiling.rs +++ b/src/console/profiling.rs @@ -12,9 +12,9 @@ //! ``` //! //! > NOTICE: valgrind executes the program you wan to profile and waits until -//! it ends. Since the tracker is a service and does not end the profiling -//! binary accepts an arguments with the duration you want to run the tracker, -//! so that it terminates automatically after that period of time. +//! > it ends. Since the tracker is a service and does not end the profiling +//! > binary accepts an arguments with the duration you want to run the tracker, +//! > so that it terminates automatically after that period of time. //! //! # Run profiling //! @@ -81,7 +81,7 @@ //! ``` //! //! > NOTICE: We are using an specific tracker configuration for profiling that -//! removes all features except the UDP tracker and sets the logging level to `error`. +//! > removes all features except the UDP tracker and sets the logging level to `error`. //! //! Build the aquatic UDP load test command: //! diff --git a/src/core/databases/mod.rs b/src/core/databases/mod.rs index 60c92b0e3..959444420 100644 --- a/src/core/databases/mod.rs +++ b/src/core/databases/mod.rs @@ -8,7 +8,7 @@ //! - [`Sqlite`](crate::core::databases::sqlite::Sqlite) //! //! > **NOTICE**: There are no database migrations. If there are any changes, -//! we will implemented them or provide a script to migrate to the new schema. +//! > we will implemented them or provide a script to migrate to the new schema. //! //! The persistent objects are: //! @@ -25,7 +25,7 @@ //! `completed` | 20 | The number of peers that have ever completed downloading the torrent associated to this entry. See [`Entry`](torrust_tracker_torrent_repository::entry::Entry) for more information. //! //! > **NOTICE**: The peer list for a torrent is not persisted. Since peer have to re-announce themselves on intervals, the data is be -//! regenerated again after some minutes. +//! > regenerated again after some minutes. //! //! # Torrent whitelist //! diff --git a/src/core/mod.rs b/src/core/mod.rs index 227f0cb29..effabb558 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -219,7 +219,7 @@ //! The torrent entry has two attributes: //! //! - `completed`: which is hte number of peers that have completed downloading the torrent file/s. As they have completed downloading, -//! they have a full version of the torrent data, and they can provide the full data to other peers. That's why they are also known as "seeders". +//! they have a full version of the torrent data, and they can provide the full data to other peers. That's why they are also known as "seeders". //! - `peers`: an indexed and orderer list of peer for the torrent. Each peer contains the data received from the peer in the `announce` request. //! //! The [`torrent`] module not only contains the original data obtained from peer via `announce` requests, it also contains @@ -401,7 +401,7 @@ //! - `scrapes_handled`: number of `scrape` handled requests by the tracker //! //! > **NOTICE**: as the HTTP tracker does not have an specific `connection` request like the UDP tracker, `connections_handled` are -//! increased on every `announce` and `scrape` requests. +//! > increased on every `announce` and `scrape` requests. //! //! The tracker exposes an event sender API that allows the tracker users to send events. When a higher application service handles a //! `connection` , `announce` or `scrape` requests, it notifies the `Tracker` by sending statistics events. @@ -467,10 +467,10 @@ use crate::CurrentClock; /// authentication and other services. /// /// > **NOTICE**: the `Tracker` is not responsible for handling the network layer. -/// Typically, the `Tracker` is used by a higher application service that handles -/// the network layer. -/// +/// > Typically, the `Tracker` is used by a higher application service that handles +/// > the network layer. #[derive(Debug)] + pub struct Tracker { announce_policy: AnnouncePolicy, /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite) diff --git a/src/core/torrent/mod.rs b/src/core/torrent/mod.rs index 286a7e047..38311864b 100644 --- a/src/core/torrent/mod.rs +++ b/src/core/torrent/mod.rs @@ -20,10 +20,10 @@ //! //! - The number of peers that have completed downloading the torrent since the tracker started collecting metrics. //! - The number of peers that have completed downloading the torrent and are still active, that means they are actively participating in the network, -//! by announcing themselves periodically to the tracker. Since they have completed downloading they have a full copy of the torrent data. Peers with a -//! full copy of the data are called "seeders". +//! by announcing themselves periodically to the tracker. Since they have completed downloading they have a full copy of the torrent data. Peers with a +//! full copy of the data are called "seeders". //! - The number of peers that have NOT completed downloading the torrent and are still active, that means they are actively participating in the network. -//! Peer that don not have a full copy of the torrent data are called "leechers". +//! Peer that don not have a full copy of the torrent data are called "leechers". //! use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; diff --git a/src/lib.rs b/src/lib.rs index 6fd5da15f..39d0b5b3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,8 +103,8 @@ //! ``` //! //! > **NOTICE**: those are the commands for `Ubuntu`. If you are using a -//! different OS, you will need to install the equivalent packages. Please -//! refer to the documentation of your OS. +//! > different OS, you will need to install the equivalent packages. Please +//! > refer to the documentation of your OS. //! //! With the default configuration you will need to create the `storage` directory: //! @@ -231,7 +231,7 @@ //! will use `./share/default/config/tracker.development.sqlite3.toml`. //! //! > IMPORTANT: Every time you change the configuration you need to restart the -//! service. +//! > service. //! //! # Usage //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index ef37026fe..47d40c654 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -1,7 +1,7 @@ //! The tracker REST API with all its versions. //! //! > **NOTICE**: This API should not be exposed directly to the internet, it is -//! intended for internal use only. +//! > intended for internal use only. //! //! Endpoints for the latest API: [v1]. //! @@ -124,16 +124,16 @@ //! ``` //! //! > **NOTICE**: If you are using a reverse proxy like NGINX, you can skip this -//! step and use NGINX for the SSL instead. See -//! [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) +//! > step and use NGINX for the SSL instead. See +//! > [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) //! //! > **NOTICE**: You can generate a self-signed certificate for localhost using -//! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). -//! That's particularly useful for testing purposes. Once you have the certificate -//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) -//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) -//! options in the configuration file with the paths to the certificate -//! (`localhost.crt`) and key (`localhost.key`) files. +//! > OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). +//! > That's particularly useful for testing purposes. Once you have the certificate +//! > you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_cert_path) +//! > and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::tsl_config.ssl_key_path) +//! > options in the configuration file with the paths to the certificate +//! > (`localhost.crt`) and key (`localhost.key`) files. //! //! # Versioning //! @@ -153,8 +153,8 @@ //! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). //! //! > **NOTICE**: we are using [curl](https://curl.se/) in the API examples. -//! And you have to use quotes around the URL in order to avoid unexpected -//! errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. +//! > And you have to use quotes around the URL in order to avoid unexpected +//! > errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. pub mod routes; pub mod server; pub mod v1; diff --git a/src/servers/apis/v1/context/auth_key/handlers.rs b/src/servers/apis/v1/context/auth_key/handlers.rs index a6c8bf812..792d9507e 100644 --- a/src/servers/apis/v1/context/auth_key/handlers.rs +++ b/src/servers/apis/v1/context/auth_key/handlers.rs @@ -46,7 +46,7 @@ pub async fn generate_auth_key_handler(State(tracker): State>, Path /// /// - `POST /api/v1/key/120`. It will generate a new key valid for two minutes. /// - `DELETE /api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. It will delete the -/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. +/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. /// /// > **NOTICE**: this may change in the future, in the [API v2](https://github.com/torrust/torrust-tracker/issues/144). #[derive(Deserialize)] diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index 11bc8a43f..330249b58 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -51,9 +51,9 @@ //! ``` //! //! > **NOTICE**: `valid_until` and `expiry_time` represent the same time. -//! `valid_until` is the number of seconds since the Unix epoch -//! ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` -//! is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). +//! > `valid_until` is the number of seconds since the Unix epoch +//! > ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` +//! > is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). //! //! **Resource** //! @@ -96,8 +96,8 @@ //! ``` //! //! > **NOTICE**: a `500` status code will be returned and the body is not a -//! valid JSON. It's a text body containing the serialized-to-display error -//! message. +//! > valid JSON. It's a text body containing the serialized-to-display error +//! > message. //! //! # Reload authentication keys //! diff --git a/src/servers/apis/v1/context/torrent/resources/torrent.rs b/src/servers/apis/v1/context/torrent/resources/torrent.rs index 2f1ace5c9..0d65b3eb6 100644 --- a/src/servers/apis/v1/context/torrent/resources/torrent.rs +++ b/src/servers/apis/v1/context/torrent/resources/torrent.rs @@ -2,8 +2,8 @@ //! //! - `Torrent` is the full torrent resource. //! - `ListItem` is a list item resource on a torrent list. `ListItem` does -//! include a `peers` field but it is always `None` in the struct and `null` in -//! the JSON response. +//! include a `peers` field but it is always `None` in the struct and `null` in +//! the JSON response. use serde::{Deserialize, Serialize}; use crate::core::services::torrent::{BasicInfo, Info}; diff --git a/src/servers/apis/v1/context/whitelist/handlers.rs b/src/servers/apis/v1/context/whitelist/handlers.rs index c88f8cc1d..32e434918 100644 --- a/src/servers/apis/v1/context/whitelist/handlers.rs +++ b/src/servers/apis/v1/context/whitelist/handlers.rs @@ -42,7 +42,7 @@ pub async fn add_torrent_to_whitelist_handler( /// /// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. /// - `500` with serialized error in debug format if the torrent couldn't be -/// removed from the whitelisted. +/// removed from the whitelisted. /// /// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#remove-a-torrent-from-the-whitelist) /// for more information about this endpoint. @@ -65,7 +65,7 @@ pub async fn remove_torrent_from_whitelist_handler( /// /// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. /// - `500` with serialized error in debug format if the torrent whitelist -/// couldn't be reloaded from the database. +/// couldn't be reloaded from the database. /// /// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#reload-the-whitelist) /// for more information about this endpoint. diff --git a/src/servers/apis/v1/context/whitelist/mod.rs b/src/servers/apis/v1/context/whitelist/mod.rs index 2bb35ef65..79da43fdc 100644 --- a/src/servers/apis/v1/context/whitelist/mod.rs +++ b/src/servers/apis/v1/context/whitelist/mod.rs @@ -11,12 +11,12 @@ //! torrents in the whitelist. The whitelist can be updated using the API. //! //! > **NOTICE**: the whitelist is only used when the tracker is configured to -//! in `listed` or `private_listed` modes. Refer to the -//! [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) -//! to know how to enable the those modes. +//! > in `listed` or `private_listed` modes. Refer to the +//! > [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) +//! > to know how to enable the those modes. //! //! > **NOTICE**: if the tracker is not running in `listed` or `private_listed` -//! modes the requests to the whitelist API will be ignored. +//! > modes the requests to the whitelist API will be ignored. //! //! # Endpoints //! diff --git a/src/servers/apis/v1/mod.rs b/src/servers/apis/v1/mod.rs index 213ee9335..372ae0ff9 100644 --- a/src/servers/apis/v1/mod.rs +++ b/src/servers/apis/v1/mod.rs @@ -12,7 +12,7 @@ //! > **NOTICE**: //! - The authentication keys are only used by the HTTP tracker. //! - The whitelist is only used when the tracker is running in `listed` or -//! `private_listed` mode. +//! `private_listed` mode. //! //! Refer to the [authentication middleware](crate::servers::apis::v1::middlewares::auth) //! for more information about the authentication process. diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index 4680e99ed..28a5ad1d7 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -39,7 +39,7 @@ //! **Query parameters** //! //! > **NOTICE**: you can click on the parameter name to see a full description -//! after extracting and parsing the parameter from the URL query component. +//! > after extracting and parsing the parameter from the URL query component. //! //! Parameter | Type | Description | Required | Default | Example //! ---|---|---|---|---|--- @@ -58,40 +58,40 @@ //! request for more information about the parameters. //! //! > **NOTICE**: the [BEP 03](https://www.bittorrent.org/beps/bep_0003.html) -//! defines only the `ip` and `event` parameters as optional. However, the -//! tracker assigns default values to the optional parameters if they are not -//! provided. +//! > defines only the `ip` and `event` parameters as optional. However, the +//! > tracker assigns default values to the optional parameters if they are not +//! > provided. //! //! > **NOTICE**: the `peer_addr` parameter is not part of the original -//! specification. But the peer IP was added in the -//! [UDP Tracker protocol](https://www.bittorrent.org/beps/bep_0015.html). It is -//! used to provide the peer's IP address to the tracker, but it is ignored by -//! the tracker. The tracker uses the IP address of the peer that sent the -//! request or the right-most-ip in the `X-Forwarded-For` header if the tracker -//! is behind a reverse proxy. +//! > specification. But the peer IP was added in the +//! > [UDP Tracker protocol](https://www.bittorrent.org/beps/bep_0015.html). It is +//! > used to provide the peer's IP address to the tracker, but it is ignored by +//! > the tracker. The tracker uses the IP address of the peer that sent the +//! > request or the right-most-ip in the `X-Forwarded-For` header if the tracker +//! > is behind a reverse proxy. //! //! > **NOTICE**: the maximum number of peers that the tracker can return is -//! `74`. Defined with a hardcoded const [`TORRENT_PEERS_LIMIT`](torrust_tracker_configuration::TORRENT_PEERS_LIMIT). -//! Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) -//! for more information about this limitation. +//! > `74`. Defined with a hardcoded const [`TORRENT_PEERS_LIMIT`](torrust_tracker_configuration::TORRENT_PEERS_LIMIT). +//! > Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) +//! > for more information about this limitation. //! //! > **NOTICE**: the `info_hash` parameter is NOT a `URL` encoded string param. -//! It is percent encode of the raw `info_hash` bytes (40 bytes). URL `GET` params -//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a -//! 20-byte SHA1. Check the [`percent_encoding`] -//! module to know more about the encoding. +//! > It is percent encode of the raw `info_hash` bytes (40 bytes). URL `GET` params +//! > can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! > 20-byte SHA1. Check the [`percent_encoding`] +//! > module to know more about the encoding. //! //! > **NOTICE**: the `peer_id` parameter is NOT a `URL` encoded string param. -//! It is percent encode of the raw peer ID bytes (20 bytes). URL `GET` params -//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a -//! 20-byte SHA1. Check the [`percent_encoding`] -//! module to know more about the encoding. +//! > It is percent encode of the raw peer ID bytes (20 bytes). URL `GET` params +//! > can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! > 20-byte SHA1. Check the [`percent_encoding`] +//! > module to know more about the encoding. //! //! > **NOTICE**: by default, the tracker returns the non-compact peer list when -//! no `compact` parameter is provided or is empty. The -//! [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) suggests to do the -//! opposite. The tracker should return the compact peer list by default and -//! return the non-compact peer list if the `compact` parameter is `0`. +//! > no `compact` parameter is provided or is empty. The +//! > [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) suggests to do the +//! > opposite. The tracker should return the compact peer list by default and +//! > return the non-compact peer list if the `compact` parameter is `0`. //! //! **Sample announce URL** //! @@ -223,7 +223,7 @@ //! [`info_hash`](crate::servers::http::v1::requests::scrape::Scrape::info_hashes) | percent encoded of 20-byte array | The `Info Hash` of the torrent. | Yes | No | `%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00` //! //! > **NOTICE**: you can scrape multiple torrents at the same time by passing -//! multiple `info_hash` parameters. +//! > multiple `info_hash` parameters. //! //! Refer to the [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) //! request for more information about the parameters. @@ -238,7 +238,7 @@ //! `info_hash` parameters: `info_hash=%81%00%0...00%00%00&info_hash=%82%00%0...00%00%00` //! //! > **NOTICE**: the maximum number of torrents you can scrape at the same time -//! is `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! > is `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). //! //! **Sample response** //! diff --git a/src/servers/http/v1/extractors/authentication_key.rs b/src/servers/http/v1/extractors/authentication_key.rs index b8d3e7d50..985e32371 100644 --- a/src/servers/http/v1/extractors/authentication_key.rs +++ b/src/servers/http/v1/extractors/authentication_key.rs @@ -39,9 +39,9 @@ //! ``` //! //! > **NOTICE**: the returned HTTP status code is always `200` for authentication errors. -//! Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) -//! nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html) -//! specifications specify any HTTP status code for authentication errors. +//! > Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! > nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html) +//! > specifications specify any HTTP status code for authentication errors. use std::panic::Location; use axum::async_trait; diff --git a/src/servers/http/v1/requests/announce.rs b/src/servers/http/v1/requests/announce.rs index 39a6c1846..83cc7ddf9 100644 --- a/src/servers/http/v1/requests/announce.rs +++ b/src/servers/http/v1/requests/announce.rs @@ -51,12 +51,12 @@ const COMPACT: &str = "compact"; /// ``` /// /// > **NOTICE**: The [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) -/// specifies that only the peer `IP` and `event`are optional. However, the -/// tracker defines default values for some of the mandatory params. +/// > specifies that only the peer `IP` and `event`are optional. However, the +/// > tracker defines default values for some of the mandatory params. /// /// > **NOTICE**: The struct does not contain the `IP` of the peer. It's not -/// mandatory and it's not used by the tracker. The `IP` is obtained from the -/// request itself. +/// > mandatory and it's not used by the tracker. The `IP` is obtained from the +/// > request itself. #[derive(Debug, PartialEq)] pub struct Announce { // Mandatory params diff --git a/src/servers/http/v1/responses/error.rs b/src/servers/http/v1/responses/error.rs index 1cc31ad4e..c406c797a 100644 --- a/src/servers/http/v1/responses/error.rs +++ b/src/servers/http/v1/responses/error.rs @@ -9,8 +9,8 @@ //! why the query failed, and no other keys are required."_ //! //! > **NOTICE**: error responses are bencoded and always have a `200 OK` status -//! code. The official `BitTorrent` specification does not specify the status -//! code. +//! > code. The official `BitTorrent` specification does not specify the status +//! > code. use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::Serialize; diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index f97afc491..fb3c7b285 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -26,7 +26,7 @@ const TIMEOUT: Duration = Duration::from_secs(5); /// It adds the routes to the router. /// /// > **NOTICE**: it's added a layer to get the client IP from the connection -/// info. The tracker could use the connection info to get the client IP. +/// > info. The tracker could use the connection info to get the client IP. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc, &addr: &SocketAddr) -> Router { Router::new() diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index e3bef3973..253140fbc 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -24,8 +24,8 @@ use crate::core::{statistics, AnnounceData, Tracker}; /// - The number of TCP `announce` requests handled by the HTTP tracker. /// /// > **NOTICE**: as the HTTP tracker does not requires a connection request -/// like the UDP tracker, the number of TCP connections is incremented for -/// each `announce` request. +/// > like the UDP tracker, the number of TCP connections is incremented for +/// > each `announce` request. pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut peer::Peer) -> AnnounceData { let original_peer_ip = peer.peer_addr.ip(); diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index 1dd94a83c..807e9bff4 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -23,8 +23,8 @@ use crate::core::{statistics, ScrapeData, Tracker}; /// - The number of TCP `scrape` requests handled by the HTTP tracker. /// /// > **NOTICE**: as the HTTP tracker does not requires a connection request -/// like the UDP tracker, the number of TCP connections is incremented for -/// each `scrape` request. +/// > like the UDP tracker, the number of TCP connections is incremented for +/// > each `scrape` request. pub async fn invoke(tracker: &Arc, info_hashes: &[InfoHash], original_peer_ip: &IpAddr) -> ScrapeData { let scrape_data = tracker.scrape(info_hashes).await; diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index e6036d2bc..8ba8b8401 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -5,7 +5,7 @@ //! The UDP tracker is a simple UDP server that responds to these requests: //! //! - `Connect`: used to get a connection ID which must be provided on each -//! request in order to avoid spoofing the source address of the UDP packets. +//! request in order to avoid spoofing the source address of the UDP packets. //! - `Announce`: used to announce the presence of a peer to the tracker. //! - `Scrape`: used to get information about a torrent. //! @@ -22,10 +22,10 @@ //! for more information about the UDP tracker protocol. //! //! > **NOTICE**: [BEP-41](https://www.bittorrent.org/beps/bep_0041.html) is not -//! implemented yet. +//! > implemented yet. //! //! > **NOTICE**: we are using the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) -//! crate so requests and responses are handled by it. +//! > crate so requests and responses are handled by it. //! //! > **NOTICE**: all values are send in network byte order ([big endian](https://en.wikipedia.org/wiki/Endianness)). //! @@ -83,23 +83,23 @@ //! spoofing can be explained as follows: //! //! 1. No connection state: Unlike TCP, UDP is a connectionless protocol, -//! meaning that it does not establish a connection between two endpoints before -//! exchanging data. As a result, it is more susceptible to IP spoofing, where -//! an attacker sends packets with a forged source IP address, tricking the -//! receiver into believing that they are coming from a legitimate source. +//! meaning that it does not establish a connection between two endpoints before +//! exchanging data. As a result, it is more susceptible to IP spoofing, where +//! an attacker sends packets with a forged source IP address, tricking the +//! receiver into believing that they are coming from a legitimate source. //! //! 2. Mitigating IP spoofing: To mitigate IP spoofing in the UDP tracker -//! protocol, a connection ID is used. When a client wants to interact with a -//! tracker, it sends a "connect" request to the tracker, which, in turn, -//! responds with a unique connection ID. This connection ID must be included in -//! all subsequent requests from the client to the tracker. +//! protocol, a connection ID is used. When a client wants to interact with a +//! tracker, it sends a "connect" request to the tracker, which, in turn, +//! responds with a unique connection ID. This connection ID must be included in +//! all subsequent requests from the client to the tracker. //! //! 3. Validating requests: By requiring the connection ID, the tracker can -//! verify that the requests are coming from the same client that initially sent -//! the "connect" request. If an attacker attempts to spoof the client's IP -//! address, they would also need to know the valid connection ID to be accepted -//! by the tracker. This makes it significantly more challenging for an attacker -//! to spoof IP addresses and disrupt the P2P network. +//! verify that the requests are coming from the same client that initially sent +//! the "connect" request. If an attacker attempts to spoof the client's IP +//! address, they would also need to know the valid connection ID to be accepted +//! by the tracker. This makes it significantly more challenging for an attacker +//! to spoof IP addresses and disrupt the P2P network. //! //! There are different ways to generate a connection ID. The most common way is //! to generate a time bound secret. The secret is generated using a time based @@ -161,9 +161,9 @@ //! 8 | [`i32`](std::i64) | `connection_id` | Generated by the tracker to authenticate the client. | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` //! //! > **NOTICE**: the `connection_id` is used when further information is -//! exchanged with the tracker, to identify the client. This `connection_id` can -//! be reused for multiple requests, but if it's cached for too long, it will -//! not be valid anymore. +//! > exchanged with the tracker, to identify the client. This `connection_id` can +//! > be reused for multiple requests, but if it's cached for too long, it will +//! > not be valid anymore. //! //! > **NOTICE**: `Hex` column is a signed 2's complement. //! @@ -243,41 +243,41 @@ //! circumstances might include: //! //! 1. Network Address Translation (NAT): In cases where a peer is behind a NAT, -//! the private IP address of the peer is not directly routable over the -//! internet. The NAT device translates the private IP address to a public one -//! when sending packets to the tracker. The public IP address is what the -//! tracker sees as the source IP of the incoming request. However, if the peer -//! provides its private IP address in the announce request, the tracker can use -//! this information to facilitate communication between peers in the same -//! private network. +//! the private IP address of the peer is not directly routable over the +//! internet. The NAT device translates the private IP address to a public one +//! when sending packets to the tracker. The public IP address is what the +//! tracker sees as the source IP of the incoming request. However, if the peer +//! provides its private IP address in the announce request, the tracker can use +//! this information to facilitate communication between peers in the same +//! private network. //! //! 2. Proxy or VPN usage: If a peer uses a proxy or VPN service to connect to -//! the tracker, the source IP address seen by the tracker will be the one -//! assigned by the proxy or VPN server. In this case, if the peer provides its -//! actual IP address in the announce request, the tracker can use it to -//! establish a direct connection with other peers, bypassing the proxy or VPN -//! server. This might improve performance or help in cases where some peers -//! cannot connect to the proxy or VPN server. +//! the tracker, the source IP address seen by the tracker will be the one +//! assigned by the proxy or VPN server. In this case, if the peer provides its +//! actual IP address in the announce request, the tracker can use it to +//! establish a direct connection with other peers, bypassing the proxy or VPN +//! server. This might improve performance or help in cases where some peers +//! cannot connect to the proxy or VPN server. //! //! 3. Tracker is behind a NAT, firewall, proxy, VPN, or load balancer: In cases -//! where the tracker is behind a NAT, firewall, proxy, VPN, or load balancer, -//! the source IP address of the incoming request will be the public IP address -//! of the NAT, firewall, proxy, VPN, or load balancer. If the peer provides its -//! private IP address in the announce request, the tracker can use this -//! information to establish a direct connection with the peer. +//! where the tracker is behind a NAT, firewall, proxy, VPN, or load balancer, +//! the source IP address of the incoming request will be the public IP address +//! of the NAT, firewall, proxy, VPN, or load balancer. If the peer provides its +//! private IP address in the announce request, the tracker can use this +//! information to establish a direct connection with the peer. //! //! It's important to note that using the provided IP address can pose security //! risks, as malicious peers might spoof their IP addresses in the announce //! request to perform various types of attacks. //! //! > **NOTICE**: The current tracker behavior is to ignore the IP address -//! provided by the peer, and use the source IP address of the incoming request, -//! when the tracker is not running behind a proxy, and to use the right-most IP -//! address in the `X-Forwarded-For` header when the tracker is running behind a -//! proxy. +//! > provided by the peer, and use the source IP address of the incoming request, +//! > when the tracker is not running behind a proxy, and to use the right-most IP +//! > address in the `X-Forwarded-For` header when the tracker is running behind a +//! > proxy. //! //! > **NOTICE**: The tracker also changes the peer IP address to the tracker -//! external IP when the peer is using a loopback IP address. +//! > external IP when the peer is using a loopback IP address. //! //! **Sample announce request (UDP packet)** //! @@ -317,11 +317,11 @@ //! 101 | N bytes | | | | //! //! > **NOTICE**: bytes after offset 98 are part of the [BEP-41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html). -//! There are three options defined for byte 98: `0x0` (`EndOfOptions`), `0x1` (`NOP`) and `0x2` (`URLData`). +//! > There are three options defined for byte 98: `0x0` (`EndOfOptions`), `0x1` (`NOP`) and `0x2` (`URLData`). //! //! > **NOTICE**: `num_want` is being ignored by the tracker. Refer to -//! [issue 262](https://github.com/torrust/torrust-tracker/issues/262) for more -//! information. +//! > [issue 262](https://github.com/torrust/torrust-tracker/issues/262) for more +//! > information. //! //! **Announce request (parsed struct)** //! @@ -342,7 +342,7 @@ //! `port` | [`Port`](aquatic_udp_protocol::common::Port) | `17548` //! //! > **NOTICE**: the `peers_wanted` field is the `num_want` field in the UDP -//! packet. +//! > packet. //! //! We are using a wrapper struct for the aquatic [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest) //! struct, because we have our internal [`InfoHash`](torrust_tracker_primitives::info_hash::InfoHash) @@ -374,7 +374,7 @@ //! > **NOTICE**: `Hex` column is a signed 2's complement. //! //! > **NOTICE**: `IP address` should always be set to 0 when the peer is using -//! `IPv6`. +//! > `IPv6`. //! //! **Sample announce response (UDP packet)** //! @@ -413,7 +413,7 @@ //! ``` //! //! > **NOTICE**: there are 6 bytes per peer (4 bytes for the `IPv4` address and -//! 2 bytes for the TCP port). +//! > 2 bytes for the TCP port). //! //! UDP packet fields (`IPv4` peer list): //! @@ -433,7 +433,7 @@ //! ``` //! //! > **NOTICE**: there are 18 bytes per peer (16 bytes for the `IPv6` address and -//! 2 bytes for the TCP port). +//! > 2 bytes for the TCP port). //! //! UDP packet fields (`IPv6` peer list): //! @@ -446,7 +446,7 @@ //! > **NOTICE**: `Hex` column is a signed 2's complement. //! //! > **NOTICE**: the peer list does not include the peer that sent the announce -//! request. +//! > request. //! //! **Announce response (struct)** //! @@ -478,10 +478,10 @@ //! - [incomplete](torrust_tracker_primitives::swarm_metadata::SwarmMetadata::incomplete) //! //! > **NOTICE**: up to about 74 torrents can be scraped at once. A full scrape -//! can't be done with this protocol. This is a limitation of the UDP protocol. -//! Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). -//! Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) -//! for more information about this limitation. +//! > can't be done with this protocol. This is a limitation of the UDP protocol. +//! > Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! > Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) +//! > for more information about this limitation. //! //! #### Scrape Request //! diff --git a/src/servers/udp/peer_builder.rs b/src/servers/udp/peer_builder.rs index 104f42a73..e54a23443 100644 --- a/src/servers/udp/peer_builder.rs +++ b/src/servers/udp/peer_builder.rs @@ -14,8 +14,7 @@ use crate::CurrentClock; /// # Arguments /// /// * `announce_wrapper` - The announce request to extract the peer info from. -/// * `peer_ip` - The real IP address of the peer, not the one in the announce -/// request. +/// * `peer_ip` - The real IP address of the peer, not the one in the announce request. #[must_use] pub fn from_request(announce_wrapper: &AnnounceWrapper, peer_ip: &IpAddr) -> peer::Peer { let announce_event = match aquatic_udp_protocol::AnnounceEvent::from(announce_wrapper.announce_request.event) { diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index b3fb597c4..14f3ec46f 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -2,8 +2,7 @@ //! //! There are two main types in this module: //! -//! - [`UdpServer`]: a controller to -//! start and stop the server. +//! - [`UdpServer`]: a controller to start and stop the server. //! - [`Udp`]: the server launcher. //! //! The `UdpServer` is an state machine for a given configuration. This struct diff --git a/src/servers/udp/v0/cookie.rs b/src/servers/udp/v0/cookie.rs index bbfc139ef..9000242ce 100644 --- a/src/servers/udp/v0/cookie.rs +++ b/src/servers/udp/v0/cookie.rs @@ -46,10 +46,10 @@ //! Peer C connects at timestamp 180 slot 1 -> connection ID will be valid from timestamp 180 to 360 //! ``` //! > **NOTICE**: connection ID is always the same for a given peer -//! (socket address) and time slot. +//! > (socket address) and time slot. //! //! > **NOTICE**: connection ID will be valid for two time extents, **not two -//! minutes**. It'll be valid for the the current time extent and the next one. +//! > minutes**. It'll be valid for the the current time extent and the next one. //! //! Refer to [`Connect`](crate::servers::udp#connect) for more information about //! the connection process. @@ -62,10 +62,8 @@ //! //! ## Disadvantages //! -//! - It's not very flexible. The connection ID is only valid for a certain -//! amount of time. -//! - It's not very accurate. The connection ID is valid for more than two -//! minutes. +//! - It's not very flexible. The connection ID is only valid for a certain amount of time. +//! - It's not very accurate. The connection ID is valid for more than two minutes. use std::net::SocketAddr; use std::panic::Location; diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index 59bf22c0a..f1d16cb33 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -30,8 +30,7 @@ use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; /// It's responsible for: /// /// - Parsing the incoming packet. -/// - Delegating the request to the correct handler depending on the request -/// type. +/// - Delegating the request to the correct handler depending on the request type. /// /// It will return an `Error` response if the request is invalid. pub(crate) async fn handle_packet(tracker: &Arc, addr: SocketAddr, udp_request: &UdpRequest) -> Response { From 223da7140e0ad1abd855db1cabb4789c0328a48e Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 20 May 2024 17:22:03 +0200 Subject: [PATCH 190/217] feat: [#670] new JSON serialization for connect and error aquatic responses --- src/console/clients/udp/app.rs | 15 ++++++++++--- src/console/clients/udp/responses.rs | 32 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 3633555c0..2b30e5430 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -61,14 +61,14 @@ use std::num::NonZeroU16; use std::time::Duration; use anyhow::Context; -use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Scrape}; +use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Connect, Error, Scrape}; use aquatic_udp_protocol::{Port, TransactionId}; use clap::{Parser, Subcommand}; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::Level; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::{AnnounceResponseDto, ScrapeResponseDto}; +use crate::console::clients::udp::responses::{AnnounceResponseDto, ConnectResponseDto, ErrorResponseDto, ScrapeResponseDto}; use crate::console::clients::{parse_info_hash, parse_socket_addr, DEFAULT_TIMEOUT_SEC}; const RANDOM_TRANSACTION_ID: i32 = -888_840_697; @@ -151,6 +151,11 @@ async fn handle_scrape(addr: &SocketAddr, timeout: &Duration, info_hashes: &[Inf fn print_response(response: Response) -> anyhow::Result<()> { match response { + Connect(response) => { + let pretty_json = serde_json::to_string_pretty(&ConnectResponseDto::from(response)) + .context("connect response JSON serialization")?; + println!("{pretty_json}"); + } AnnounceIpv4(response) => { let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) .context("announce IPv4 response JSON serialization")?; @@ -166,7 +171,11 @@ fn print_response(response: Response) -> anyhow::Result<()> { serde_json::to_string_pretty(&ScrapeResponseDto::from(response)).context("scrape response JSON serialization")?; println!("{pretty_json}"); } - _ => println!("{response:#?}"), // todo: serialize to JSON all aquatic responses. + Error(response) => { + let pretty_json = + serde_json::to_string_pretty(&ErrorResponseDto::from(response)).context("error response JSON serialization")?; + println!("{pretty_json}"); + } }; Ok(()) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 8ea1a978b..eb6b386fd 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,9 +1,24 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use aquatic_udp_protocol::{AnnounceResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; +use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; +#[derive(Serialize)] +pub struct ConnectResponseDto { + transaction_id: i32, + connection_id: i64, +} + +impl From for ConnectResponseDto { + fn from(connect: ConnectResponse) -> Self { + Self { + transaction_id: connect.transaction_id.0.into(), + connection_id: connect.connection_id.0.into(), + } + } +} + #[derive(Serialize)] pub struct AnnounceResponseDto { transaction_id: i32, @@ -68,6 +83,21 @@ impl From for ScrapeResponseDto { } } +#[derive(Serialize)] +pub struct ErrorResponseDto { + transaction_id: i32, + message: String, +} + +impl From for ErrorResponseDto { + fn from(error: ErrorResponse) -> Self { + Self { + transaction_id: error.transaction_id.0.into(), + message: error.message.to_string(), + } + } +} + #[derive(Serialize)] struct TorrentStats { seeders: i32, From cb6742c3ba2b44b87e23e97fe81c97bccdcb60a0 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 15:54:56 +0200 Subject: [PATCH 191/217] refactor: [#670] new trait for printing responses in JSON format and enum for Dto wrapper --- src/console/clients/udp/responses.rs | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index eb6b386fd..5a39a04d4 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -1,9 +1,46 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; +use anyhow::Context; +use aquatic_udp_protocol::Response::{self}; use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; +pub trait DtoToJson { + fn print_response(&self) -> anyhow::Result<()> + where + Self: Serialize, + { + let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; + println!("{pretty_json}"); + + Ok(()) + } +} + +#[derive(Serialize)] +pub enum ResponseDto { + Connect(ConnectResponseDto), + AnnounceIpv4(AnnounceResponseDto), + AnnounceIpv6(AnnounceResponseDto), + Scrape(ScrapeResponseDto), + Error(ErrorResponseDto), +} + +impl From for ResponseDto { + fn from(response: Response) -> Self { + match response { + Response::Connect(response) => ResponseDto::Connect(ConnectResponseDto::from(response)), + Response::AnnounceIpv4(response) => ResponseDto::AnnounceIpv4(AnnounceResponseDto::from(response)), + Response::AnnounceIpv6(response) => ResponseDto::AnnounceIpv6(AnnounceResponseDto::from(response)), + Response::Scrape(response) => ResponseDto::Scrape(ScrapeResponseDto::from(response)), + Response::Error(response) => ResponseDto::Error(ErrorResponseDto::from(response)), + } + } +} + +impl DtoToJson for ResponseDto {} + #[derive(Serialize)] pub struct ConnectResponseDto { transaction_id: i32, From 58516c50c168d1fedf37f1017fe962a4832f6497 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 15:59:10 +0200 Subject: [PATCH 192/217] refactor: [#670] new print_response function from trait implemented --- src/console/clients/udp/app.rs | 42 +++++----------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 2b30e5430..9fb363bde 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -61,16 +61,17 @@ use std::num::NonZeroU16; use std::time::Duration; use anyhow::Context; -use aquatic_udp_protocol::Response::{self, AnnounceIpv4, AnnounceIpv6, Connect, Error, Scrape}; -use aquatic_udp_protocol::{Port, TransactionId}; +use aquatic_udp_protocol::{Port, Response, TransactionId}; use clap::{Parser, Subcommand}; use torrust_tracker_primitives::info_hash::InfoHash; use tracing::Level; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::{AnnounceResponseDto, ConnectResponseDto, ErrorResponseDto, ScrapeResponseDto}; + use crate::console::clients::{parse_info_hash, parse_socket_addr, DEFAULT_TIMEOUT_SEC}; +use super::responses::{DtoToJson as _, ResponseDto}; + const RANDOM_TRANSACTION_ID: i32 = -888_840_697; #[derive(Parser, Debug)] @@ -116,7 +117,8 @@ pub async fn run() -> anyhow::Result<()> { Command::Scrape { info_hashes } => handle_scrape(&args.addr, &timeout, &info_hashes).await?, }; - print_response(response) + let response_dto: ResponseDto = response.into(); + response_dto.print_response() } async fn handle_announce(addr: &SocketAddr, timeout: &Duration, info_hash: &InfoHash) -> anyhow::Result { @@ -148,35 +150,3 @@ async fn handle_scrape(addr: &SocketAddr, timeout: &Duration, info_hashes: &[Inf .await .context("failed to handle scrape") } - -fn print_response(response: Response) -> anyhow::Result<()> { - match response { - Connect(response) => { - let pretty_json = serde_json::to_string_pretty(&ConnectResponseDto::from(response)) - .context("connect response JSON serialization")?; - println!("{pretty_json}"); - } - AnnounceIpv4(response) => { - let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) - .context("announce IPv4 response JSON serialization")?; - println!("{pretty_json}"); - } - AnnounceIpv6(response) => { - let pretty_json = serde_json::to_string_pretty(&AnnounceResponseDto::from(response)) - .context("announce IPv6 response JSON serialization")?; - println!("{pretty_json}"); - } - Scrape(response) => { - let pretty_json = - serde_json::to_string_pretty(&ScrapeResponseDto::from(response)).context("scrape response JSON serialization")?; - println!("{pretty_json}"); - } - Error(response) => { - let pretty_json = - serde_json::to_string_pretty(&ErrorResponseDto::from(response)).context("error response JSON serialization")?; - println!("{pretty_json}"); - } - }; - - Ok(()) -} From 3c84732c40d5a78901c5710d45eb95e8f825264f Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 27 May 2024 17:29:17 +0200 Subject: [PATCH 193/217] refactor: [#670] added error message for pint_response function --- src/console/clients/udp/responses.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses.rs index 5a39a04d4..83e4da506 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses.rs @@ -7,6 +7,10 @@ use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv use serde::Serialize; pub trait DtoToJson { + /// # Errors + /// + /// Will return an error if serialization fails. + /// fn print_response(&self) -> anyhow::Result<()> where Self: Serialize, From 19a3c5efe17530c9bbe825a920ccfb678ad23e90 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 00:43:25 +0200 Subject: [PATCH 194/217] refactor: [#670] new mod for responses logic and refactors to json serialization trait --- src/console/clients/udp/app.rs | 11 +++++---- .../udp/{responses.rs => responses/dto.rs} | 19 --------------- src/console/clients/udp/responses/json.rs | 24 +++++++++++++++++++ src/console/clients/udp/responses/mod.rs | 2 ++ 4 files changed, 33 insertions(+), 23 deletions(-) rename src/console/clients/udp/{responses.rs => responses/dto.rs} (90%) create mode 100644 src/console/clients/udp/responses/json.rs create mode 100644 src/console/clients/udp/responses/mod.rs diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 9fb363bde..50dcbf943 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -67,11 +67,10 @@ use torrust_tracker_primitives::info_hash::InfoHash; use tracing::Level; use crate::console::clients::udp::checker; - +use crate::console::clients::udp::responses::dto::ResponseDto; +use crate::console::clients::udp::responses::json::ToJson; use crate::console::clients::{parse_info_hash, parse_socket_addr, DEFAULT_TIMEOUT_SEC}; -use super::responses::{DtoToJson as _, ResponseDto}; - const RANDOM_TRANSACTION_ID: i32 = -888_840_697; #[derive(Parser, Debug)] @@ -118,7 +117,11 @@ pub async fn run() -> anyhow::Result<()> { }; let response_dto: ResponseDto = response.into(); - response_dto.print_response() + let response_json = response_dto.to_json_string()?; + + print!("{response_json}"); + + Ok(()) } async fn handle_announce(addr: &SocketAddr, timeout: &Duration, info_hash: &InfoHash) -> anyhow::Result { diff --git a/src/console/clients/udp/responses.rs b/src/console/clients/udp/responses/dto.rs similarity index 90% rename from src/console/clients/udp/responses.rs rename to src/console/clients/udp/responses/dto.rs index 83e4da506..989231061 100644 --- a/src/console/clients/udp/responses.rs +++ b/src/console/clients/udp/responses/dto.rs @@ -1,27 +1,10 @@ //! Aquatic responses are not serializable. These are the serializable wrappers. use std::net::{Ipv4Addr, Ipv6Addr}; -use anyhow::Context; use aquatic_udp_protocol::Response::{self}; use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv4AddrBytes, Ipv6AddrBytes, ScrapeResponse}; use serde::Serialize; -pub trait DtoToJson { - /// # Errors - /// - /// Will return an error if serialization fails. - /// - fn print_response(&self) -> anyhow::Result<()> - where - Self: Serialize, - { - let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; - println!("{pretty_json}"); - - Ok(()) - } -} - #[derive(Serialize)] pub enum ResponseDto { Connect(ConnectResponseDto), @@ -43,8 +26,6 @@ impl From for ResponseDto { } } -impl DtoToJson for ResponseDto {} - #[derive(Serialize)] pub struct ConnectResponseDto { transaction_id: i32, diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs new file mode 100644 index 000000000..1e25acac2 --- /dev/null +++ b/src/console/clients/udp/responses/json.rs @@ -0,0 +1,24 @@ +use anyhow::Context; +use serde::Serialize; + +use super::dto::ResponseDto; + +pub trait ToJson { + /// + /// Returns a string with the JSON serialized version of the response + /// + /// # Errors + /// + /// Will return an error if serialization fails. + /// + fn to_json_string(&self) -> anyhow::Result + where + Self: Serialize, + { + let pretty_json = serde_json::to_string_pretty(self).context("response JSON serialization")?; + + Ok(pretty_json) + } +} + +impl ToJson for ResponseDto {} diff --git a/src/console/clients/udp/responses/mod.rs b/src/console/clients/udp/responses/mod.rs new file mode 100644 index 000000000..e6d2e5e51 --- /dev/null +++ b/src/console/clients/udp/responses/mod.rs @@ -0,0 +1,2 @@ +pub mod dto; +pub mod json; From 5299b35a824ae761cc514e056fd4add6c472fbec Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 12:32:35 +0200 Subject: [PATCH 195/217] refactor: [#670] changed DTOs and variable names --- src/console/clients/udp/app.rs | 6 ++-- src/console/clients/udp/responses/dto.rs | 42 +++++++++++------------ src/console/clients/udp/responses/json.rs | 4 +-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs index 50dcbf943..de977ba4b 100644 --- a/src/console/clients/udp/app.rs +++ b/src/console/clients/udp/app.rs @@ -67,7 +67,7 @@ use torrust_tracker_primitives::info_hash::InfoHash; use tracing::Level; use crate::console::clients::udp::checker; -use crate::console::clients::udp::responses::dto::ResponseDto; +use crate::console::clients::udp::responses::dto::SerializableResponse; use crate::console::clients::udp::responses::json::ToJson; use crate::console::clients::{parse_info_hash, parse_socket_addr, DEFAULT_TIMEOUT_SEC}; @@ -116,8 +116,8 @@ pub async fn run() -> anyhow::Result<()> { Command::Scrape { info_hashes } => handle_scrape(&args.addr, &timeout, &info_hashes).await?, }; - let response_dto: ResponseDto = response.into(); - let response_json = response_dto.to_json_string()?; + let response: SerializableResponse = response.into(); + let response_json = response.to_json_string()?; print!("{response_json}"); diff --git a/src/console/clients/udp/responses/dto.rs b/src/console/clients/udp/responses/dto.rs index 989231061..93320b0f7 100644 --- a/src/console/clients/udp/responses/dto.rs +++ b/src/console/clients/udp/responses/dto.rs @@ -6,33 +6,33 @@ use aquatic_udp_protocol::{AnnounceResponse, ConnectResponse, ErrorResponse, Ipv use serde::Serialize; #[derive(Serialize)] -pub enum ResponseDto { - Connect(ConnectResponseDto), - AnnounceIpv4(AnnounceResponseDto), - AnnounceIpv6(AnnounceResponseDto), - Scrape(ScrapeResponseDto), - Error(ErrorResponseDto), +pub enum SerializableResponse { + Connect(ConnectSerializableResponse), + AnnounceIpv4(AnnounceSerializableResponse), + AnnounceIpv6(AnnounceSerializableResponse), + Scrape(ScrapeSerializableResponse), + Error(ErrorSerializableResponse), } -impl From for ResponseDto { +impl From for SerializableResponse { fn from(response: Response) -> Self { match response { - Response::Connect(response) => ResponseDto::Connect(ConnectResponseDto::from(response)), - Response::AnnounceIpv4(response) => ResponseDto::AnnounceIpv4(AnnounceResponseDto::from(response)), - Response::AnnounceIpv6(response) => ResponseDto::AnnounceIpv6(AnnounceResponseDto::from(response)), - Response::Scrape(response) => ResponseDto::Scrape(ScrapeResponseDto::from(response)), - Response::Error(response) => ResponseDto::Error(ErrorResponseDto::from(response)), + Response::Connect(response) => SerializableResponse::Connect(ConnectSerializableResponse::from(response)), + Response::AnnounceIpv4(response) => SerializableResponse::AnnounceIpv4(AnnounceSerializableResponse::from(response)), + Response::AnnounceIpv6(response) => SerializableResponse::AnnounceIpv6(AnnounceSerializableResponse::from(response)), + Response::Scrape(response) => SerializableResponse::Scrape(ScrapeSerializableResponse::from(response)), + Response::Error(response) => SerializableResponse::Error(ErrorSerializableResponse::from(response)), } } } #[derive(Serialize)] -pub struct ConnectResponseDto { +pub struct ConnectSerializableResponse { transaction_id: i32, connection_id: i64, } -impl From for ConnectResponseDto { +impl From for ConnectSerializableResponse { fn from(connect: ConnectResponse) -> Self { Self { transaction_id: connect.transaction_id.0.into(), @@ -42,7 +42,7 @@ impl From for ConnectResponseDto { } #[derive(Serialize)] -pub struct AnnounceResponseDto { +pub struct AnnounceSerializableResponse { transaction_id: i32, announce_interval: i32, leechers: i32, @@ -50,7 +50,7 @@ pub struct AnnounceResponseDto { peers: Vec, } -impl From> for AnnounceResponseDto { +impl From> for AnnounceSerializableResponse { fn from(announce: AnnounceResponse) -> Self { Self { transaction_id: announce.fixed.transaction_id.0.into(), @@ -66,7 +66,7 @@ impl From> for AnnounceResponseDto { } } -impl From> for AnnounceResponseDto { +impl From> for AnnounceSerializableResponse { fn from(announce: AnnounceResponse) -> Self { Self { transaction_id: announce.fixed.transaction_id.0.into(), @@ -83,12 +83,12 @@ impl From> for AnnounceResponseDto { } #[derive(Serialize)] -pub struct ScrapeResponseDto { +pub struct ScrapeSerializableResponse { transaction_id: i32, torrent_stats: Vec, } -impl From for ScrapeResponseDto { +impl From for ScrapeSerializableResponse { fn from(scrape: ScrapeResponse) -> Self { Self { transaction_id: scrape.transaction_id.0.into(), @@ -106,12 +106,12 @@ impl From for ScrapeResponseDto { } #[derive(Serialize)] -pub struct ErrorResponseDto { +pub struct ErrorSerializableResponse { transaction_id: i32, message: String, } -impl From for ErrorResponseDto { +impl From for ErrorSerializableResponse { fn from(error: ErrorResponse) -> Self { Self { transaction_id: error.transaction_id.0.into(), diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs index 1e25acac2..74558c8f5 100644 --- a/src/console/clients/udp/responses/json.rs +++ b/src/console/clients/udp/responses/json.rs @@ -1,7 +1,7 @@ use anyhow::Context; use serde::Serialize; -use super::dto::ResponseDto; +use super::dto::SerializableResponse; pub trait ToJson { /// @@ -21,4 +21,4 @@ pub trait ToJson { } } -impl ToJson for ResponseDto {} +impl ToJson for SerializableResponse {} From 4b1bcd5e7e1f9e6fbb32b73d59ad496080ff6eba Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 4 Jun 2024 13:16:47 +0200 Subject: [PATCH 196/217] refactor:[#670] fix clippy errors --- src/console/clients/udp/responses/json.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/console/clients/udp/responses/json.rs b/src/console/clients/udp/responses/json.rs index 74558c8f5..5d2bd6b89 100644 --- a/src/console/clients/udp/responses/json.rs +++ b/src/console/clients/udp/responses/json.rs @@ -3,6 +3,7 @@ use serde::Serialize; use super::dto::SerializableResponse; +#[allow(clippy::module_name_repetitions)] pub trait ToJson { /// /// Returns a string with the JSON serialized version of the response From c171a4a048709f3fd309e8a1d192d563df953cd9 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 07:50:42 +0100 Subject: [PATCH 197/217] docs: add benchmarking to torrent repo README --- packages/torrent-repository/README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/torrent-repository/README.md b/packages/torrent-repository/README.md index 98d7d922b..ffc71f1d7 100644 --- a/packages/torrent-repository/README.md +++ b/packages/torrent-repository/README.md @@ -1,6 +1,27 @@ -# Torrust Tracker Configuration +# Torrust Tracker Torrent Repository -A library to provide torrent repository to the [Torrust Tracker](https://github.com/torrust/torrust-tracker). +A library to provide a torrent repository to the [Torrust Tracker](https://github.com/torrust/torrust-tracker). + +## Benchmarking + +```console +cargo bench -p torrust-tracker-torrent-repository +``` + +Example partial output: + +```output + Running benches/repository_benchmark.rs (target/release/deps/repository_benchmark-a9b0013c8d09c3c3) +add_one_torrent/RwLockStd + time: [63.057 ns 63.242 ns 63.506 ns] +Found 12 outliers among 100 measurements (12.00%) + 2 (2.00%) low severe + 2 (2.00%) low mild + 2 (2.00%) high mild + 6 (6.00%) high severe +add_one_torrent/RwLockStdMutexStd + time: [62.505 ns 63.077 ns 63.817 ns] +``` ## Documentation From 68d8344a79fd5c278ce30661dbd0233d5e26bc0f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 15:12:08 +0100 Subject: [PATCH 198/217] chore(deps): add cargo dependency tracing We will move from `log` to `tracing` crate. --- Cargo.lock | 13 +++++++++++++ Cargo.toml | 3 +-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36c9ed1d4..b0f425ea2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3808,6 +3808,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -3815,11 +3825,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "nu-ansi-term", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 08a5f071d..3090e6da4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,7 @@ torrust-tracker-torrent-repository = { version = "3.0.0-alpha.12-develop", path tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } tracing = "0" -tracing-subscriber = "0" -# tracing-test = "0" +tracing-subscriber = { version = "0", features = ["json"] } url = "2" uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" From 307801f4f216547d7e3ffee4c2105263632e652f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 15:30:25 +0100 Subject: [PATCH 199/217] refactor: [#884] move from log to tracing crate --- src/console/ci/e2e/logs_parser.rs | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 6d3349196..2d215a569 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,9 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "[UDP TRACKER][INFO] Starting on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "[HTTP TRACKER][INFO] Starting on: "; -const HEALTH_CHECK_PATTERN: &str = "[HEALTH CHECK API][INFO] Starting on: "; +const UDP_TRACKER_PATTERN: &str = "INFO UDP TRACKER: Starting on: udp://"; +const HTTP_TRACKER_PATTERN: &str = "INFO HTTP TRACKER: Starting on: "; +const HEALTH_CHECK_PATTERN: &str = "INFO HEALTH CHECK API: Starting on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -18,17 +18,17 @@ impl RunningServices { /// For example, from this logs: /// /// ```text - /// Loading default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-01-24T16:36:14.614898789+00:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized. - /// 2024-01-24T16:36:14.615586025+00:00 [UDP TRACKER][INFO] Starting on: udp://0.0.0.0:6969 - /// 2024-01-24T16:36:14.615623705+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled - /// 2024-01-24T16:36:14.615694484+00:00 [HTTP TRACKER][INFO] Starting on: http://0.0.0.0:7070 - /// 2024-01-24T16:36:14.615710534+00:00 [HTTP TRACKER][INFO] Started on: http://0.0.0.0:7070 - /// 2024-01-24T16:36:14.615716574+00:00 [torrust_tracker::bootstrap::jobs][INFO] TLS not enabled - /// 2024-01-24T16:36:14.615764904+00:00 [API][INFO] Starting on http://127.0.0.1:1212 - /// 2024-01-24T16:36:14.615767264+00:00 [API][INFO] Started on http://127.0.0.1:1212 - /// 2024-01-24T16:36:14.615777574+00:00 [HEALTH CHECK API][INFO] Starting on: http://127.0.0.1:1313 - /// 2024-01-24T16:36:14.615791124+00:00 [HEALTH CHECK API][INFO] Started on: http://127.0.0.1:1313 + /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... + /// 2024-06-10T14:26:10.040894Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T14:26:10.041363Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T14:26:10.041386Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:26:10.041420Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T14:26:10.041516Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T14:26:10.041521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:26:10.041611Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T14:26:10.041614Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T14:26:10.041623Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T14:26:10.041657Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -86,9 +86,9 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { let logs = "\ - [UDP TRACKER][INFO] Starting on: udp://0.0.0.0:8080\n\ - [HTTP TRACKER][INFO] Starting on: 0.0.0.0:9090\n\ - [HEALTH CHECK API][INFO] Starting on: 0.0.0.0:10010"; + INFO UDP TRACKER: Starting on: udp://0.0.0.0:8080\n\ + INFO HTTP TRACKER: Starting on: 0.0.0.0:9090\n\ + INFO HEALTH CHECK API: Starting on: 0.0.0.0:10010"; let running_services = RunningServices::parse_from_logs(logs); assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]); From 4e7b7730de8c0ce5cca77e903dab0d05824c3d14 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:01:40 +0100 Subject: [PATCH 200/217] refactor: [#884] move from log to tracing crate --- packages/configuration/src/lib.rs | 2 +- .../config/tracker.e2e.container.sqlite3.toml | 4 + src/console/ci/e2e/docker.rs | 2 + src/console/ci/e2e/logs_parser.rs | 73 +++++++++++++------ src/console/ci/e2e/runner.rs | 5 ++ src/console/ci/e2e/tracker_container.rs | 4 +- 6 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 96d1091a1..83d02a79d 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -74,7 +74,7 @@ impl Info { let env_var_config_toml_path = ENV_VAR_CONFIG_TOML_PATH.to_string(); let config_toml = if let Ok(config_toml) = env::var(env_var_config_toml) { - println!("Loading configuration from environment variable {config_toml} ..."); + println!("Loading configuration from environment variable:\n {config_toml}"); Some(config_toml) } else { None diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index e7d8fa279..767b56116 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -11,3 +11,7 @@ ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [http_api] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + +[health_check_api] +# Must be bound to wildcard IP to be accessible from outside the container +bind_address = "0.0.0.0:1313" diff --git a/src/console/ci/e2e/docker.rs b/src/console/ci/e2e/docker.rs index 26b7f7708..32a0c3e56 100644 --- a/src/console/ci/e2e/docker.rs +++ b/src/console/ci/e2e/docker.rs @@ -176,6 +176,8 @@ impl Docker { let output_str = String::from_utf8_lossy(&output.stdout); + info!("Waiting until container is healthy: {:?}", output_str); + if output_str.contains("(healthy)") { return true; } diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 2d215a569..a9277524e 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,9 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "INFO UDP TRACKER: Starting on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "INFO HTTP TRACKER: Starting on: "; -const HEALTH_CHECK_PATTERN: &str = "INFO HEALTH CHECK API: Starting on: "; +const UDP_TRACKER_PATTERN: &str = "UDP TRACKER: Started on: udp://"; +const HTTP_TRACKER_PATTERN: &str = "HTTP TRACKER: Started on: "; +const HEALTH_CHECK_PATTERN: &str = "HEALTH CHECK API: Started on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -19,16 +19,17 @@ impl RunningServices { /// /// ```text /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-06-10T14:26:10.040894Z INFO torrust_tracker::bootstrap::logging: logging initialized. - /// 2024-06-10T14:26:10.041363Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - /// 2024-06-10T14:26:10.041386Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:26:10.041420Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - /// 2024-06-10T14:26:10.041516Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - /// 2024-06-10T14:26:10.041521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:26:10.041611Z INFO API: Starting on http://127.0.0.1:1212 - /// 2024-06-10T14:26:10.041614Z INFO API: Started on http://127.0.0.1:1212 - /// 2024-06-10T14:26:10.041623Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - /// 2024-06-10T14:26:10.041657Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + /// 2024-06-10T14:59:57.973525Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T14:59:57.974306Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T14:59:57.974316Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + /// 2024-06-10T14:59:57.974332Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:59:57.974366Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T14:59:57.974513Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T14:59:57.974521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T14:59:57.974615Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T14:59:57.974618Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T14:59:57.974643Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T14:59:57.974760Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -46,6 +47,9 @@ impl RunningServices { /// ] /// } /// ``` + /// + /// NOTICE: Using colors in the console output could affect this method + /// due to the hidden control chars. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { let mut udp_trackers: Vec = Vec::new(); @@ -85,20 +89,45 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let logs = "\ - INFO UDP TRACKER: Starting on: udp://0.0.0.0:8080\n\ - INFO HTTP TRACKER: Starting on: 0.0.0.0:9090\n\ - INFO HEALTH CHECK API: Starting on: 0.0.0.0:10010"; - let running_services = RunningServices::parse_from_logs(logs); + let log = r#" + Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" + + [[udp_trackers]] + enabled = true + + [[http_trackers]] + enabled = true + ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" + ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + + [http_api] + ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" + ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" + ... + Loading configuration from file: `/etc/torrust/tracker/tracker.toml` ... + 2024-06-10T15:09:54.411031Z INFO torrust_tracker::bootstrap::logging: logging initialized. + 2024-06-10T15:09:54.415084Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T15:09:54.415091Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + 2024-06-10T15:09:54.415104Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T15:09:54.415130Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + 2024-06-10T15:09:54.415266Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + 2024-06-10T15:09:54.415275Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T15:09:54.415403Z INFO API: Starting on http://127.0.0.1:1212 + 2024-06-10T15:09:54.415411Z INFO API: Started on http://127.0.0.1:1212 + 2024-06-10T15:09:54.415430Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + 2024-06-10T15:09:54.415472Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + "#; + + let running_services = RunningServices::parse_from_logs(log); - assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:8080"]); - assert_eq!(running_services.http_trackers, vec!["127.0.0.1:9090"]); - assert_eq!(running_services.health_checks, vec!["127.0.0.1:10010/health_check"]); + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); + assert_eq!(running_services.http_trackers, vec!["http://127.0.0.1:7070"]); + assert_eq!(running_services.health_checks, vec!["http://127.0.0.1:1313/health_check"]); } #[test] fn it_should_ignore_logs_with_no_matching_lines() { - let logs = "[Other Service][INFO] Starting on: 0.0.0.0:7070"; + let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; let running_services = RunningServices::parse_from_logs(logs); assert!(running_services.udp_trackers.is_empty()); diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index be83b5650..d83552599 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -59,6 +59,11 @@ pub fn run() { let running_services = tracker_container.running_services(); + info!( + "Running services:\n {}", + serde_json::to_string_pretty(&running_services).expect("running services to be serializable to JSON") + ); + assert_there_is_at_least_one_service_per_type(&running_services); let tracker_checker_config = diff --git a/src/console/ci/e2e/tracker_container.rs b/src/console/ci/e2e/tracker_container.rs index 0cb4fec7c..dc7036faa 100644 --- a/src/console/ci/e2e/tracker_container.rs +++ b/src/console/ci/e2e/tracker_container.rs @@ -2,7 +2,7 @@ use std::time::Duration; use rand::distributions::Alphanumeric; use rand::Rng; -use tracing::{debug, error, info}; +use tracing::{error, info}; use super::docker::{RunOptions, RunningContainer}; use super::logs_parser::RunningServices; @@ -72,7 +72,7 @@ impl TrackerContainer { pub fn running_services(&self) -> RunningServices { let logs = Docker::logs(&self.name).expect("Logs should be captured from running container"); - debug!("Parsing running services from logs. Logs :\n{logs}"); + info!("Parsing running services from logs. Logs :\n{logs}"); RunningServices::parse_from_logs(&logs) } From 08233b039335814fe31265d0319096c8d4d58829 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:23:55 +0100 Subject: [PATCH 201/217] test: [#884] add test for parsing array of services from app logs The tracker can run multiple UDP or HTTP trackers. We parse the services from app output but there was not test for multiple services of the same type (UDP or HTTP tracker). --- src/console/ci/e2e/logs_parser.rs | 72 +++++++++++++++++++------------ 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index a9277524e..2a1876a11 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -19,17 +19,19 @@ impl RunningServices { /// /// ```text /// Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... - /// 2024-06-10T14:59:57.973525Z INFO torrust_tracker::bootstrap::logging: logging initialized. - /// 2024-06-10T14:59:57.974306Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - /// 2024-06-10T14:59:57.974316Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 - /// 2024-06-10T14:59:57.974332Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:59:57.974366Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - /// 2024-06-10T14:59:57.974513Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - /// 2024-06-10T14:59:57.974521Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - /// 2024-06-10T14:59:57.974615Z INFO API: Starting on http://127.0.0.1:1212 - /// 2024-06-10T14:59:57.974618Z INFO API: Started on http://127.0.0.1:1212 - /// 2024-06-10T14:59:57.974643Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - /// 2024-06-10T14:59:57.974760Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + /// 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. + /// 2024-06-10T16:07:39.990205Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868 + /// 2024-06-10T16:07:39.990215Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6868 + /// 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + /// 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + /// 2024-06-10T16:07:39.990261Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T16:07:39.990303Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + /// 2024-06-10T16:07:39.990439Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + /// 2024-06-10T16:07:39.990448Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + /// 2024-06-10T16:07:39.990563Z INFO API: Starting on http://127.0.0.1:1212 + /// 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 + /// 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + /// 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 /// ``` /// /// It would extract these services: @@ -48,7 +50,7 @@ impl RunningServices { /// } /// ``` /// - /// NOTICE: Using colors in the console output could affect this method + /// NOTICE: Using colors in the console output could affect this method /// due to the hidden control chars. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { @@ -89,7 +91,7 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let log = r#" + let logs = r#" Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] @@ -103,22 +105,22 @@ mod tests { [http_api] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - ... - Loading configuration from file: `/etc/torrust/tracker/tracker.toml` ... - 2024-06-10T15:09:54.411031Z INFO torrust_tracker::bootstrap::logging: logging initialized. - 2024-06-10T15:09:54.415084Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 - 2024-06-10T15:09:54.415091Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 - 2024-06-10T15:09:54.415104Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - 2024-06-10T15:09:54.415130Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 - 2024-06-10T15:09:54.415266Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 - 2024-06-10T15:09:54.415275Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled - 2024-06-10T15:09:54.415403Z INFO API: Starting on http://127.0.0.1:1212 - 2024-06-10T15:09:54.415411Z INFO API: Started on http://127.0.0.1:1212 - 2024-06-10T15:09:54.415430Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 - 2024-06-10T15:09:54.415472Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 + + Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... + 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. + 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990261Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T16:07:39.990303Z INFO HTTP TRACKER: Starting on: http://0.0.0.0:7070 + 2024-06-10T16:07:39.990439Z INFO HTTP TRACKER: Started on: http://0.0.0.0:7070 + 2024-06-10T16:07:39.990448Z INFO torrust_tracker::bootstrap::jobs: TLS not enabled + 2024-06-10T16:07:39.990563Z INFO API: Starting on http://127.0.0.1:1212 + 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 + 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 + 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 "#; - let running_services = RunningServices::parse_from_logs(log); + let running_services = RunningServices::parse_from_logs(logs); assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); assert_eq!(running_services.http_trackers, vec!["http://127.0.0.1:7070"]); @@ -128,6 +130,7 @@ mod tests { #[test] fn it_should_ignore_logs_with_no_matching_lines() { let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; + let running_services = RunningServices::parse_from_logs(logs); assert!(running_services.udp_trackers.is_empty()); @@ -135,6 +138,21 @@ mod tests { assert!(running_services.health_checks.is_empty()); } + #[test] + fn it_should_parse_multiple_services() { + let logs = " + 2024-06-10T16:07:39.990205Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6868 + 2024-06-10T16:07:39.990215Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6868 + + 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 + 2024-06-10T16:07:39.990255Z INFO UDP TRACKER: Started on: udp://0.0.0.0:6969 + "; + + let running_services = RunningServices::parse_from_logs(logs); + + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6868", "127.0.0.1:6969"]); + } + #[test] fn it_should_replace_wildcard_ip_with_localhost() { let address = "0.0.0.0:8080"; From 95e0d13ea0e44b06ae4f228d280931425514a77b Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 17:24:25 +0100 Subject: [PATCH 202/217] chore(deps): remove unused dependencies log and fern We have moved from crate log to tracing. --- Cargo.lock | 10 ---------- Cargo.toml | 1 - 2 files changed, 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0f425ea2..d138cb356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1047,15 +1047,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "fern" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" -dependencies = [ - "log", -] - [[package]] name = "figment" version = "0.10.19" @@ -3591,7 +3582,6 @@ dependencies = [ "crossbeam-skiplist", "dashmap", "derive_more", - "fern", "figment", "futures", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 3090e6da4..a45c0c9e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" -fern = "0" figment = "0.10.18" futures = "0" futures-util = "0.3.30" From 1816a381e98cbac0894f3d546bed41091ffb69c4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 10 Jun 2024 18:35:10 +0100 Subject: [PATCH 203/217] refactor: [#659] use clap and anyhow in E2E test runner You can execute the E2E runner with: ```bash cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" ``` Or: ```bash TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.e2e.container.sqlite3.toml" cargo run --bin e2e_tests_runner ``` Or: ```bash TORRUST_TRACKER_CONFIG_TOML=$(cat "./share/default/config/tracker.e2e.container.sqlite3.toml") cargo run --bin e2e_tests_runner ``` --- .github/workflows/testing.yaml | 2 +- src/bin/e2e_tests_runner.rs | 4 +- src/console/ci/e2e/runner.rs | 91 ++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 620670f97..abe6f0a60 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -152,4 +152,4 @@ jobs: - id: test name: Run E2E Tests - run: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml + run: cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" diff --git a/src/bin/e2e_tests_runner.rs b/src/bin/e2e_tests_runner.rs index b21459d2e..eb91c0d86 100644 --- a/src/bin/e2e_tests_runner.rs +++ b/src/bin/e2e_tests_runner.rs @@ -1,6 +1,6 @@ //! Program to run E2E tests. use torrust_tracker::console::ci::e2e; -fn main() { - e2e::runner::run(); +fn main() -> anyhow::Result<()> { + e2e::runner::run() } diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index d83552599..a80b65ce2 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -1,9 +1,28 @@ //! Program to run E2E tests. //! +//! You can execute it with (passing a TOML config file path): +//! +//! ```text +//! cargo run --bin e2e_tests_runner -- --config-toml-path "./share/default/config/tracker.e2e.container.sqlite3.toml" +//! ``` +//! +//! Or: +//! //! ```text -//! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml +//! TORRUST_TRACKER_CONFIG_TOML_PATH="./share/default/config/tracker.e2e.container.sqlite3.toml" cargo run --bin e2e_tests_runner" //! ``` -use tracing::{info, Level}; +//! +//! You can execute it with (directly passing TOML config): +//! +//! ```text +//! TORRUST_TRACKER_CONFIG_TOML=$(cat "./share/default/config/tracker.e2e.container.sqlite3.toml") cargo run --bin e2e_tests_runner +//! ``` +use std::path::PathBuf; + +use anyhow::Context; +use clap::Parser; +use tracing::info; +use tracing::level_filters::LevelFilter; use super::tracker_container::TrackerContainer; use crate::console::ci::e2e::docker::RunOptions; @@ -18,25 +37,38 @@ use crate::console::ci::e2e::tracker_checker::{self}; Should we remove the image too? */ -const NUMBER_OF_ARGUMENTS: usize = 2; const CONTAINER_IMAGE: &str = "torrust-tracker:local"; const CONTAINER_NAME_PREFIX: &str = "tracker_"; -pub struct Arguments { - pub tracker_config_path: String, +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Path to the JSON configuration file. + #[clap(short, long, env = "TORRUST_TRACKER_CONFIG_TOML_PATH")] + config_toml_path: Option, + + /// Direct configuration content in JSON. + #[clap(env = "TORRUST_TRACKER_CONFIG_TOML", hide_env_values = true)] + config_toml: Option, } /// Script to run E2E tests. /// +/// # Errors +/// +/// Will return an error if it can't load the tracker configuration from arguments. +/// /// # Panics /// /// Will panic if it can't not perform any of the operations. -pub fn run() { - let () = tracing_subscriber::fmt().compact().with_max_level(Level::TRACE).init(); +pub fn run() -> anyhow::Result<()> { + tracing_stdout_init(LevelFilter::INFO); - let args = parse_arguments(); + let args = Args::parse(); - let tracker_config = load_tracker_configuration(&args.tracker_config_path); + let tracker_config = load_tracker_configuration(&args)?; + + info!("tracker config:\n{tracker_config}"); let mut tracker_container = TrackerContainer::new(CONTAINER_IMAGE, CONTAINER_NAME_PREFIX); @@ -79,31 +111,36 @@ pub fn run() { tracker_container.remove(); info!("Tracker container final state:\n{:#?}", tracker_container); -} -fn parse_arguments() -> Arguments { - let args: Vec = std::env::args().collect(); - - if args.len() < NUMBER_OF_ARGUMENTS { - eprintln!("Usage: cargo run --bin e2e_tests_runner "); - eprintln!("For example: cargo run --bin e2e_tests_runner ./share/default/config/tracker.e2e.container.sqlite3.toml"); - std::process::exit(1); - } + Ok(()) +} - let config_path = &args[1]; +fn tracing_stdout_init(filter: LevelFilter) { + tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); + info!("Logging initialized."); +} - Arguments { - tracker_config_path: config_path.to_string(), +fn load_tracker_configuration(args: &Args) -> anyhow::Result { + match (args.config_toml_path.clone(), args.config_toml.clone()) { + (Some(config_path), _) => { + info!( + "Reading tracker configuration from file: {} ...", + config_path.to_string_lossy() + ); + load_config_from_file(&config_path) + } + (_, Some(config_content)) => { + info!("Reading tracker configuration from env var ..."); + Ok(config_content) + } + _ => Err(anyhow::anyhow!("No configuration provided")), } } -fn load_tracker_configuration(tracker_config_path: &str) -> String { - info!("Reading tracker configuration from file: {} ...", tracker_config_path); - read_file(tracker_config_path) -} +fn load_config_from_file(path: &PathBuf) -> anyhow::Result { + let config = std::fs::read_to_string(path).with_context(|| format!("CSan't read config file {path:?}"))?; -fn read_file(path: &str) -> String { - std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Can't read file {path}")) + Ok(config) } fn assert_there_is_at_least_one_service_per_type(running_services: &RunningServices) { From b29efb1113eabcf9fdac2ca8adb54770ffbb229f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Jun 2024 14:40:22 +0100 Subject: [PATCH 204/217] docs: [#770] update benchmarking docs We are now using criterion to benchmark the torrent repository implementations. --- docs/benchmarking.md | 96 ++++++++++++------ ...ry-implementations-benchmarking-report.png | Bin 0 -> 630867 bytes 2 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 docs/media/torrent-repository-implementations-benchmarking-report.png diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 2a3f1f8b0..1758e0de4 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -96,7 +96,7 @@ Announce responses per info hash: - p100: 361 ``` -> IMPORTANT: The performance of th Torrust UDP Tracker is drastically decreased with these log levels: `info`, `debug`, `trace`. +> IMPORTANT: The performance of the Torrust UDP Tracker is drastically decreased with these log levels: `info`, `debug`, `trace`. ```output Requests out: 40719.21/second @@ -226,46 +226,76 @@ Using a PC with: ## Repository benchmarking +### Requirements + +You need to install the `gnuplot` package. + +```console +sudo apt install gnuplot +``` + +### Run + You can run it with: ```console -cargo run --release -p torrust-torrent-repository-benchmarks -- --threads 4 --sleep 0 --compare true +cargo bench -p torrust-tracker-torrent-repository ``` -It tests the different implementation for the internal torrent storage. +It tests the different implementations for the internal torrent storage. The output should be something like this: ```output -tokio::sync::RwLock> -add_one_torrent: Avg/AdjAvg: (60ns, 59ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (10.909457ms, 0ns) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (13.88879ms, 0ns) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (7.772484ms, 7.782535ms) - -std::sync::RwLock> -add_one_torrent: Avg/AdjAvg: (43ns, 39ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (4.020937ms, 4.020937ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (5.896177ms, 5.768448ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (3.883823ms, 3.883823ms) - -std::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (51ns, 49ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (3.252314ms, 3.149109ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (8.411094ms, 8.411094ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.106086ms, 4.106086ms) - -tokio::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (91ns, 90ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (3.542378ms, 3.435695ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (15.651172ms, 15.651172ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.368189ms, 4.257572ms) - -tokio::sync::RwLock>>> -add_one_torrent: Avg/AdjAvg: (111ns, 109ns) -update_one_torrent_in_parallel: Avg/AdjAvg: (6.590677ms, 6.808535ms) -add_multiple_torrents_in_parallel: Avg/AdjAvg: (16.572217ms, 16.30488ms) -update_multiple_torrents_in_parallel: Avg/AdjAvg: (4.073221ms, 4.000122ms) + Running benches/repository_benchmark.rs (target/release/deps/repository_benchmark-2f7830898bbdfba4) +add_one_torrent/RwLockStd + time: [60.936 ns 61.383 ns 61.764 ns] +Found 24 outliers among 100 measurements (24.00%) + 15 (15.00%) high mild + 9 (9.00%) high severe +add_one_torrent/RwLockStdMutexStd + time: [60.829 ns 60.937 ns 61.053 ns] +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high severe +add_one_torrent/RwLockStdMutexTokio + time: [96.034 ns 96.243 ns 96.545 ns] +Found 6 outliers among 100 measurements (6.00%) + 4 (4.00%) high mild + 2 (2.00%) high severe +add_one_torrent/RwLockTokio + time: [108.25 ns 108.66 ns 109.06 ns] +Found 2 outliers among 100 measurements (2.00%) + 2 (2.00%) low mild +add_one_torrent/RwLockTokioMutexStd + time: [109.03 ns 109.11 ns 109.19 ns] +Found 4 outliers among 100 measurements (4.00%) + 1 (1.00%) low mild + 1 (1.00%) high mild + 2 (2.00%) high severe +Benchmarking add_one_torrent/RwLockTokioMutexTokio: Collecting 100 samples in estimated 1.0003 s (7.1M iterationsadd_one_torrent/RwLockTokioMutexTokio + time: [139.64 ns 140.11 ns 140.62 ns] +``` + +After running it you should have a new directory containing the criterion reports: + +```console +target/criterion/ +├── add_multiple_torrents_in_parallel +├── add_one_torrent +├── report +├── update_multiple_torrents_in_parallel +└── update_one_torrent_in_parallel ``` +You can see one report for each of the operations we are considering for benchmarking: + +- Add multiple torrents in parallel. +- Add one torrent. +- Update multiple torrents in parallel. +- Update one torrent in parallel. + +Each report look like the following: + +![Torrent repository implementations benchmarking report](./media/torrent-repository-implementations-benchmarking-report.png) + ## Other considerations -We are testing new repository implementations that allow concurrent writes. See . +If you are interested in knowing more about the tracker performance or contribute to improve its performance you ca join the [performance optimizations discussion](https://github.com/torrust/torrust-tracker/discussions/774). diff --git a/docs/media/torrent-repository-implementations-benchmarking-report.png b/docs/media/torrent-repository-implementations-benchmarking-report.png new file mode 100644 index 0000000000000000000000000000000000000000..ee87c6d42292b707338a1917782b39da1357dba9 GIT binary patch literal 630867 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

`RCsMA%N zu(-JUYJg)YszbE~DV077Funx}TkOR=#e+vRIQ4O7uu%yZ)~m6Y3Uj(CjfKjjDhjdL z&-v6}c~+K->K1g0n5+YI#Wi%Fo&hCQU6T*vL0LM%??4{Pm6!wsgf1Ax#>s}XG~gr( zC*QKA_Ix@{Cvj+ zWxF?>o#ktl!;o@aXksYR3Uj}alD&n~JpM@``2doQkt^=Ts~j(sMqeR`O zJuQIT!j7_Oco5~b>Pccg z*+|0M*OHzRecz5{xuvJ%36|Yyk36xhp#C2PsN3)Yp%TiY~Zd@SXdSm(xcx}+Mp<%HO(D?{;ssO zw_73rgOkmNSaza0x`+J z=r+fqMVu9!oQ;VMXZMG-c=DbVBW?y zM;lc$-{FMHu1-9V2+d(B|9~TESuh?$YJ${^E?p&MXl-6uIGsy}4lNTWl#eb4ub{_B zTS1-T5PEGCDoHO6c23zaf695AOPPnyBSC8sY zP%7bW7~zJXq^z2%>Pxye-Q8Llq%=NKA9ABBm*(x0wcq(OMhyatE5Q^*!tA1I5~0+O z9dMo(BQ3<}X-Ev7wXK{`Fa;|sne|V7cLL`D2nm>6j3B|82Sd%4lP>nk8ag@&)AfXxC^NX$(YzrRG`pxR_0{NW z=!Pw$2<+nPaX2D$1r%8Zc^@nGcTyvMVH#Axl zdW6|AlIm`f`Mzdeq_LTn)vZdONN;K$b@l)s@f$l>IT|<0o13uP5r#4z{}IpZ&3KyM z!WxEAL05u}+(6=Iw})sL@d*1>k^GFFpI6mfSj8tRXwvTQo4fQDheFiM=3ieR|8(l* z6ZyRU?K;e_c6vLZDTuu+pJ7Ti?&VAxL?)*v1JbgK-@w4&wH??F2?~-#l)O?%mA%{T z9=uRF8c2^}Pifif*Ff2Mk&j95)9sa}o4|jgJX8--!}0F+HCfGrL`&wnSqxC75n{vAf{qW(_{7ldGL$lnit`qzIw z06@R(o7aCQ0cYg@)9Bw@@&DyL?K1@Z>C-EQRQkr;-BY|=-gbQ39N9qiaY}plMyj3- zSFa}i;yy$bpJ?H?0z3dj>i>P9q)PoAt)-UF_iPb7PkT%lT5hG~=Gz@8jH0JAtFK2d zh7JKg1j!V!1{If_9%su;n3?*xz}CaR-d^#?NczzZ#tu=IZ>!rvxpp`jx=4cU`P@gB zrCro48lHvup7(Ft&Xay>pe$d<)C+C>p8C_V`@fEbKafBANV2)V=4+|#pd!2SpWJVj zYw9rEKd{Y~DbI~n{Ma^M1=*jU*+#UjkABYt_of2(*RpJ&XWce=UO{y|Hr-H}bR=#P zbU5^UcR{8-KS9A^1YG0SV55!n`D?n3o!T6XcNkFzM1=9bw-o}3w+1z^wup`*a!SEY zSg)f1nAeNJ3~)AqL<0Cf^@=6RGV=1V`_yYjTUP8s_@xG_G1_D3Fz7mDla(24oSYz? zE?7aCpdcNCGNghclcF>o+%O^A3Tv0gE8p>Ohnj4jzy*CNiU56)ZOq_zESuMa^XU(L z_dW>p?F_4pK4%$97LD83>7!Yvg@ab#x(jga-~kQ>?2wj+gJN(olq7oS@BlPs?ty{s z8Y*yx06hMB;^;fFg;w`RoYpgcu=1KRi4e>e_PcR8nb@gw!oK0V$YhM;D%$)qu-uC1 z*CyH_#I7IGJPn%amOJH23K%7cW0-}h?L|KmQ_-W*gcPcdU1A1)a1(oOhOwOuIhOs9 z`R+H##=@dszsS(I6)k~NmBE&Uj+QUR>bv6ycBK9&1^cO5Zx&3G${@qTs^b!@blTX2 zawKaDUeFkl2#9ttcv#SbQ$1hq!KITX`K(X)`n=CZty87)x#kidA_uFELQ`Nk##=2{ zTL%XRj|z7s>LbF#lZOptFspOYnU;BZc}J>B27U}iyNuYmvJ)cv3ph0Kh#zoFf-edM z85yKrHpxA@^TKZ_F~s;7^DAeN(#=k=lyb4maWtLBvh|*-W9iG?D0r8&of$|xXovOf z+ly7!B8J%W{gSGh0k~2TJJI1x>DRka3rafi)Jml~d!ce;Nq@W7yi)gt8B>Dr)ys^F zxN^7c1=Dh#Y8ZpC{B)OtWYe+%JFNZ7ofJz}HnxP=*#C>Kw}7g$`?|%krKBYlM7pK> zC>_$>BHi6_R2l>Xq)WQHq(P*GLpK~6>CRLC2j2JlefR#qdoRN=4myTo?B_Xq%{A9t zbMIx^^bL@d{hArVBO@b|*=Q7kgM%0BFM%uaxeR1dXz;@l4ZXJqm@9;bP8ZHfaQu8) za$i~}YpI-#Kr8r%#)lh$;ZLvN5LT9}Cl&*p>q@{PSNWv@=>HQ=coFRd+&D$W5<`Lc z75J?kEJux>!rt0sb;jQA@1)m0eMmXmynU%;`!NA*Zl;jWJS>=(mzT`;s;pGAgzC@c zbL9*UvPkb%(Q%F3Xom2FoQnG?nIyr$G~WX7OC=6ok!u1K{+P5O0%^J&%CpE!|9jsU z|6Yf<{=Epa{R;;M1dnqcO_?a!V#yJDf+aFmGkh!;dB>83%3(cbJi%ss{%c%^prby& zT|r{)xC}doOJFD;B&CMa!12P$!2#lmZZ$bF;^JKFONIlfXl4k38``LD}-@Y4LQk%xhvLfFdL;HNY3s}QdH z2Ec4#j?4U;X7__<3=;LH$Q{*iw}T}GQzrvS2^#LwR+#9`d7OhSRN%KjN;#sTZo0zN zv}uXOl!I_e`XV5Lhw!NiA(w%e?y+#DhK)g{6;{`ue!Z)T#3AzJuXgTAG(_=cNsl)2 z$%o5ChmZYCR;C489Xw!Dmr=B4=f{_e(8k)ZE%WlY`?D3FmRP?jj`zX~uDZ}!U_df0 z$lItl5_Xz@Ls?f>j_}@{<4yoh=tQIp3>h^R9^@HhB+=M-x_jC6KRTDrR9HsudEE0_ z$(O&ftrx^eN}FB$v9;rktmKLSTdPJF#oMb8?#{luT>Hs(M8#ycunJg#_KPc|<1g~B z2(R8gJ%hFv;&BqDT$aVQH#T$36}yFU4pkf$|Kwr}Nd`iuLV>Xu83{tk@()c6YsdUu z96>dI-f<>>rzvuz^C&DPdlnMeL+}TZ*=j-yZUDjO><@iJkmo+V*|DVrKLjeQZ25=uuHik*YH1N356YEg zAS5FvFR7@2xW2NY9(2~y($dqLKgJ# zoe6Dsd=R`@TD-z{&&wp*Lmu@IPv<;k;Nzmjy#AoI-FaSqO#e(A1H_oB=fs6JRq z4m?tlXyJN)Xz)ugL&n6BZM+6lMX$wMrMtQb5#f$-AMaqFzEqIHm-tFg0*g*`7kWR3 zxg$Tk{HRMG2$QqUl-rGyJh`-#v5h}goRyX3NY_CSGrny)dw(aaPMal&t27|gGsD5k zDtU?XrK0fF=8{LV-{->I4FMe$S{+#tZamqR#@=QJ7K%{1uVgDbyjw&O%yJ4`d;w=6 z1P|6?pbjs6D=bPKSl&x)NX|Xl*eprlPaSxFo)^;A0`cSKCXETbU?*viK7Q_BGkB?(=)lQ@r}91)hwk)y ze_^ekW2_8qb_`#3RqjcA#7;it$Ub^&{XFdE<>3>*H3{`x0;Y&_aNY05mf8nbCkf_KzmDOyLYPeQf_(Yt8}teBtgvYLp~e|ER^`? zza5_IKy2^&^w3eqcD#w#M2zjMPDTtuDl!FXsLbAZ#qg0t(_cdGkJ$Xg+RBjA|- z3f@K0WqlW)XjaN*qc)G&==vqTW58It99iPj2@{M>H!A1vh2@F?aQ@Mf`sOU5^Sl)~ zv2~$ZQ(rZ4l}tM_55HDs5X(}2 zHxv^kL7mOute<5o^E|qjG?+2X>{)DoE{$vuSEIPbq$9sAHyKX7ep4ZQhz%OG((kFd zsUyW)IcU8vD~aA4Cy3o%HBGq)Hv051s*Lf?nP-9F{Jvk!U~^lqk_25$1?kg=&@02PQLc~EU`xxV@7|~% z36h%mB!|G*4^*3x6DOtb4YW$X-g0 zBR?7lm<29dtGCh*ssc+LvDEBGClkpiJ^c=EmfjdK40FEMs46cT@RpSC9;h)F@&-HQ znK_x=v#a<9#znp=t6T=6@9pEp#e**xsF%^b_6@{}S2z#vXEQukyo#2f7R{`1{MDY# z5D*YB-UY%lSW@-(7w-q^RK*W!0T*_`h+R?twOFY{4NBQQAE^MWt`?}!k^nvpb+bxo z9eKIEOP!`}6xT523C>+X^EQj&KXHy}4fV+x)mENn4$tmZefwe*t7~RoS|<`xN~EhV zO({R%4Wiby$ft$%~-Ufkl`w6knxCM~EU+wB672gD@x5UVBo%{-ZzoAcwTq zy}ownB>n|M)Hc7(n_le&$ zfk^ee*G(F1Y<~UxfC3!}q@el5w*7#+6a+k9rj19sNQEa7Zw=mXrUX9#aJ|)^+S*Bn zcb}XRyC?K-m*1L$>$QJEKlF37=2Evi%M`LY7G|vC!1wezrftw=D17S=4@U|yL zbL1MbDzc+PMu_-%hii#=-f_2kXt{Col%z?a@RL?QAYg$^Mf9`ya?6;a4q%}RAUWuSYa>U`LPT~ zJUmQBWs=kc{-h_zs-2Ku4cN(xcRTqHkJWsQ(tfRhA8LBql?1rsg&ewcb2b1KZaj}N zBhETRabzhM{@9d}rp8kt6o?uG5-a}y9|vBu^;cQu_Sur9f89S<=npR@{%LLzqu8}i zirosY+KLoeVnq;0m`-BGci%wjHP}j6%h`D(2~I8z|1R)QP<;%wq)z*JI9a}NDzC3; zKt`cl=vB zTibdm!{wYF+;j~L39pN{VvUmkBE>WFQXUj+0%|FpFWgOVF5nFBz#4UmO{Wf zDw&IdXAuoPufu>j!y{mRyu|nq>eShzANHU|e_42++Jgf_2O?@Q2P4Y$RhQJw%QNdM zDZ9QEzjRmpY^K_9zu4}EnOIA>#?6wGx=7-Ay5qZ6gXQE8?_-JF<~!DV+K!WG2vks> zxe!X^=p?IfL^sNX=njt71-n+Gx;K4{z!y0@e6`{jlAA@(AIpo~5cHemuN4#*B&~99c<(;j)5?bzwWi!%haC znv!bQ$QeqEp!=WE@Ztq1BJ+BNoEX^{C4N__XPYtc;K{~5e0Q~3Kqmh+d!8}_D=1uh zJO5ckPA$uK<@$>!OFLxpv|wrssXUJ+zc%I478$-%c*rzJYrly7{-0NY;3Te@Egh6kXUqHtAFgnM#EJqUA|fn& zJ%_?BdU|?Fi?^=!vAQT`MY*|MC)T3p3{_O#2nv#6JytpuZX7dIS0{WH5)vF-eOq`A zJEa*`Kw!Im&GQ~q*93TY;<@-mMJjM6yyUuH*E~Hg16L(N`r{#tj{-Uawywv#c<5N$ z?LKf871q(-(B_dDeB_Lg$u+#}**Fcg1b%>5eSXgJhAfyqXT>u%UzWzqh{2x&t^`7T zO(k|2i#h^rk+}8X;`W)D*EF`Z`JZkMk$Rn9`XFkAPxOJZ5KX{rvB}K{v7^;sH$OEt z7Q3yxteu-R?e7n*fc`yU{!(V&tx@LZ;qSFoHopY#UBiZfuQw94%5={=9^mNvUvKn+ zeC+J@(=;*B9PbAZjJ||^FD+!>qwqV(c_l0Qlf{S$aY%o%=k`Kl+D%=7ato|(kTH^K z&=yGJF)<@(9j>I^F@+3cN9=UHl@OfatoVI2Q8u`j&&>)8F=}&pCV~R^)wk32kwE>1 z3n&V<>G+mYn?)b0qzOxt*DEnU)eq_VY>R`YW5$4sI1l!NI8e&c5GBy2cQA!BDecy6 zkOXg$)Fgl$+7RTK*4*LrQEu3I`N0H>R7vG^1Kw6H*AYU}tFdBO$&$uy@?LYXpfvQS zsBiaxTF9K?yMiTX6iz@qP`J8y)jNW(a=*WgXT#=C zoie!_Uctp5yZVUne!Z=U8(&*)#FQzuo@Zv2(>}BJ-MWd)=Yh6ZNoKJe`{M(2ZH2{8 zN#d^Zidy3YG=Sf0PQk|9DPr3nW$EH#X|`RP-V<|P8ZHQUa{%=>b#lsvQnz4R`Jc1d zseyW?LG<41OwrVL@B$^CjBTyYweN0))K3dqs75gC9efmIzc6(X zWJzS+{UY*lHfKT2_epM6dzNWViJiWDQ0qK#sy~BPo$rvq@Y#3JB^@=4@BRmpp6o8F z_kgd^<;&@pROI=WR89k_`u`WGUWA}>vkzXPD@xIpbpUj5Xil6mnD6WgM3yuo^t_yH zik*S)@FjL@WA7m6d*LDqZAfNls6}$#X?XjLO0uhi)Jce1@DcbiO{*Rl)q#NQVfBYg zlrnuU`+0m(VK8ZFZcafD19@L;fv$&|m-ut=8D;BTugjpw&6J?$a`w6&<6A}ydZX&j z%i5YAvO3Q~P$D^5FO8ByQ_{^N24lAYrhy;ga|rxQdVGL=h*46dp{f z{dFQZKg4YPJf56Sa*wo?tk3Q#%7wlD61Z&NpVJe8b9m#M`ufe)&1)!d!8%B1;dpr` zylX2}7Qya|(nIlh&Vo>bk<7bTE5pPGjR@$0egiyimyN5pYbEI-t=;eF2{9#q(ztew z&^4?pY)zO(%R<8v@T7+iT7fmODREvy14T1a9Ndx zb}bli0!h*r7KbYwF1u2}Ap(tCjlF=1>vfW9qoo~Mn6;wUAliO5+zb8bbABGx_3ZuE zD94CtWV=widt+Sa-_jmEqo0R69()IJ>^8|5kNcGuM;2N*2(%pKnnbd0MOid7`VSl= zoPeIQv7zeN`zk!B4x1cuDx4|lL|0VUSxDnpD(h2NY*Y?SltW5L8-)j+H^iNn$>jT?+%*9`bEZ?XH(itgi=70>r=RGiy#L2-jAr zwJkYmPq!Oym*F7O&YlRKO%@VvJ&e=*5SmYI%(;uX+dy=?q_^ce#|my<`QCnNbT5pw zJ_;91_0(YDd5Ir);h5t0BOlVTJ9#NFKuxzp1?@BCmiqxAQckr6kO9{_PJ37T+I(QBFy}1)Hmye*Y+D5?8k+bZGwJ-c$0A9wuet z&Hn7<$uE??$wO={zJ+(Fsyh&8EtS?2`tK% z=5|~hk!lcpv>a^Vf3`@uU~FmR4wV~pg6(N&07C^8FjH(jTj4k z&BbYUtHSE-9CV%K8KP79hf#jUy?rL+7k(priV~87S=HL$+3b@D@;!x(O;6QV)G?re zUy;=;9&T=Rb#-}L*;FVX5_yI-4Sxk8mPlfFC$n#)TTJa;DeRJctV{LTA|PF_;jlInw#40D5b{`GA86g6t&=y* z8z`?p*N$iQFMgq;KEHiS*bRKf$o$?qH50#L=d$6UNc|Qcz)B4+94xPzI7^pL4Yg#z zUE~q=nx^!?ev}koLtiqPt4;8A1A?h7{WRE`9qIz3b0nd!|}B&PCbUR zQ>3l=JUkEk+pTOe>(A$cU;7}tS^)xZc-dBL;CBgL@b7LBkM0e2wGK^; zZO5P%=~%S|Pgc8jpNZ;>pCyC!=jSSaCitxIT`q!ty0+;MT=>I>rH*4Oko&Yc&C?;E zH2&jvx8ICaP`#`tFUzAu#cckPXX`DUr@{~m&f&AjAfWyZFAf)O$e(m`$}y>+HyyQk z>0fXex!9(T(@sy=yf*tnH{(2L6%gaFXo=!wXg|u(YG|0KIw!CvlCAsDu|EYG)hJRb z?oz}->sblCA+*iqn+vz!!9kso_O^`3?>Xz|4Z(x15)w{bElR#p%JB4bq!nb+kO6N< z&*if9S6n;TPyRBWZA^%X9X4l|+3u~QiDfNVY_G&H{*P7QPixlO+Fm|d=Hg%zV)dl5 z8-$5$Q>z;4)DTMU(;}w0^1%RpGWDvmZ?40XOePf~Lz_QT;g-u^)AQ-qZ(` zK$P8pmj~S6lP2FFYAj`RPB6 zTFT!o20}tGsh%!;<|MoFrp-+<`Fmx)yz*Jf4Dy>Y1=9b(H-_14uBY%Y$NLo)mv8Us zXk}C4`d?&xC>)=}X* zi^_`y56q~;FH@f+YU(}dd0nO6fcm`dFm}RMR@k}$C!@A5sh;{y0zEkCFP_u2(y6Po zy^t)v>vBk~Z5y!@(glM1NWlEYT!aJc^k``OHFQEoSKX~j_USaigmgu6Db-3#CuMqU zMPueol`P}4vpULph{i?x#6b=IOCjNx(Ns@^Y9@{Dnj1ohGGt5El>_eu%75^KzN*CM z)~3lxb>kaP{@-uT_N2v&)-yTr68syAz;5!5rNza9r^0~JoLL_YclI-)c}0K+;S}yo zAmJ%_o^bnM0zBPY6NH&!>32|VTms@Z11g+1iy7a_rV_T9fC`85A)lz@Ry0W49SS*Y zDV=4)vG;`w@j?Bx&L0skwvnWxLY)5ZdIfYl1Z4ptyO|93i+du5%sD)Fq9Bl`(fJrV z@HMnTu3AolEXqLJ{z9qSBsSLjlL;xrbWaF8gBNdF)}f*67LoS6tb;cpx#F-;a0o+< z@V31oadk_!Z#^Ia4wk3+qWc1uM(x>BG}ME|egT*uPJ0u3(8!Q70>Ud!$&4I+3*3*i7gzUi;>-2 zIO=4WzF#fr;!7NCOwzn4+OQS)<)5Cu0A#PYrs%bwh<{N(9m5+3$OmP;y2|WNsv|6Q z{}UV&hHg7HRoa{8+P$@SaI806H#c4hlVNGau`26qmX5y2J;dVG3!d(-xKZS>okJPIZz;ED#@4k!DA(YQ+(o2xHhOZXx+-aKvS=o>kf>Z_ zzNm1WBVxK%BR}B=fC-`C`|JlKFEAu>VoO(MnM#o3LuNpfCQPRnS(RaR5zgt6Y(D=- zA`%r}4Br@@_D3z4O+Q{A%q#zuk3 z3>lzX{Sa^a{ASyr)KXe#&ZXRd_9SSvxY!U}?=k~wECMAd%dnsD2F&cHhSL%St7Ajz z1(*MO-+Y>hdxCY$_(qL*P(^^`R3m2%yXkr7Yo)nvbA;`m35#W zh1ThkKYiOei_~Ntn3y>y;z^9Q&(6Lo^vf2#FECuP{6@p*mZ1RafzfA!z&XwH#B|g- zeGukRuzA>SMWO~K#qK4H%irL7LR3INmZJa{x#H^PBbLkta8wNazhx-I{_xJB2rMsN zk(#>TbP$ZX$sra&hNOEK)vg8#wE&$@cJ0cyC6kIcs8|}=SP2Q;GIu2W`pLXNl)N9W z8HxFgtaF}7-J##bKZ@H!$SGjlu-x)O7WQs=`*0?kl1Sx@n#zi}Ecw$&szjwNIF+sy zG@$%Xm%;7xe5CQA>N&wi8U^jw3*k(OTy7oJ*Y)M20BdHP{H>NJfqx?XfZ(#_r_Z#b zvH%D3j}{ppSaA(oVFLu|rc{u=#xEQDRceh?5d6HJa`tJ#10!W1ocr92!5r`Dy0Q%Y zr#_6RauBMADv}^8S^=j=MXP&4g(+~)OjDC*d2RI9uM}=2?Y1Q@<#P`{e;V8nbXNG8 zefRS^8ghQ&DQwT18ojW?KEIPz;+|mk=v`q^k-+TfRl#bs-3%Wuky4$KM4r}#q{H?1 zwGR&`(HBC1z8p1D;XVz`_A~|l?T)=pPETj<5nOtT zWhHU9no+a3MFsBcaS2WO_^JWDQ0b*@1E^5BqS51Tuc%YPW!scn=JG*7t4g2m+D~?N zcH3S8eINkNW$y&5AK0E~XwK=vJY(m_!!IaXnvH#+g&^Foeds({#@uO3wpHi(@A--W-M#TYff5+fj_PMc0|W(z^z@b>eJ8f+ zeI1sROCcP*rJYXahgx`pWzsM|e&KxAXp3tSd8N0XJckV&*xdY1@p1txVX4&FJWV2lPTPD%|7m4V0w(b%nlUD$6 zacO2hlAcI|R^>w?q@nKTvq)hmk8q=l<%t&g0%i2Wus(Z2y^^x6Kg|AC(|@C~`jcpC zu(lui>y!-{3*LNuXmKBM3izk2eD1ylV1sjDTBCMxps102<^2-~)N6x?`MIEZu#bRn z?e1bb{8$MBY91D0fcjnTkZu085afC&-8)y|R;sWiznLvIPXq7ETRxeu((E485{6`+ zq&R}XPOU>fwvPd?LRlSog_Xzl!>CaB%J1^5erf2g9+Zo&zu2%#gJ`$Hvgju=&AP;} zu=20rKE)y+8a%ov61dHY}z6Z8_DiHRp6nVW*Sf8e6CA5hcV=ehZX z0dt95iN9H#iC3B3`b>=VZ$e)D zP~--2C~`;_pb6YY-2gtiWTv3N(_tY=PD%gY)l}y1J}SqaX~s#vbT+Ay-^<0iro;)| z$FrI~T1S;a8~hKo)nn`KYgtViv=#b#8OTa29EW9QN);CecNyn6Z6-WKt5JW;`~}zo z9Tdub74R()_V$e8@W!co>mb#HsynC!^`amMvX)n=o^f@9m~Vh_=q zS?u(abF(YQnC=C8kALkp(Y-)$Vp>#+-#K3XwcFgeUe3?Y3yu~5y|S(9==I@+y}J@_ z%)d-UALzjI^=b^ikW_j6*x&Q>;vlC3luvk`A@k-?~Ki zgz}ax+Xr|tQoY?F`J4t66h{A%Jav^}OXQI~Uz!Y62w9(OzJ4K1^J*~Mdh7s4G)vpL zcVxX{>QE25i_=Ym-K$Bo3tH5#Zz(tRGSF1ov>@rR2g*~7KI1!@SB8+=xuBw!y^6ssmIXu)4Pt6Y0KH!N~Rneoq7ZR6&A&=La+=&k#Rgq{zP`Q;!H=4n%yi(xjltI{C5EphI&63Js-pGb#{~MKN(>&X zjZPSeAFo582P%_@l33ldl-=R-ri0LQINAxxFOaEr1HEa<(9X670Bvzoqt2aXW}THZ zL6>_AvJy1qKd)URGCDt04StP5p?wpGE#0F?!Rn4hR&x7WT3N@33M9e9l8Pj%!PE)K z`1d#;{*zF{*B4LV4w?Qo1@tWR1R-`e*??AoYO-S#)iDvshZ3m#S9K>SC;ZZa zB;PSXK<2i=p^&+dRk%|^>4So`F0H)%ZkA-JPZtarc^-JsYETEiQWaO(W^a~iM{M_e zRrj}6*T4?xlFE5A?kD62q?vza1O&Y;ABenW1w!>3>jaYcpzU<)a1S$1QOlLTCRn@y z?7q9V_XjXuZMZkTj*+=FNdTbEtt@+>YHezKKALj!5{639 zCO>n%aA6U*-l6O0M1td;!&wFm3=9A@&Y8r17-$Q3^nbgwpZTZ*+p9pOvp!*>``HF4<%>_$au^1dmp|y~>E&a7)(^UaM*0B8k|NgWR18b@V_#KW z-4tN24OrJbpJ=<3%BlKMu0<#r(S0)db{S+ix44ha$Rvp`OD)JTkr~>TyV2g{EIP<> z{%mcHe9^(M z2s^0yAo6ckaBll%q)CqK9*eV)aj1nUOdUoq2t{g@3UHijO_t~ zSJCc8Ag2MK)8?knT~6S<+;?c%38$T$BJB(>8yFD;pPQLkw&_TRCAXkM%I z74A6i_b3+$Lgn;s=-siO0LDG0;!+CT+Dx;;nZoNi`kln-Sx$Avw-;h}9mKd!b)JTJ z*_9s>^(=CzkiI1kO5KKjuW>ak0ZcQO%Q#$&APkFbAL{Ubt!8kByT;{-^AYNA77siu zltw3heZ8|CgF^4)1dUnVLbj2LQEWxO(ZBvPuG_2vXe`3F8q4~M10&ie_;10K`H!HR z?XGU{)GtGdnnnqyR7)-T@xj!R;)A&m(B>eCe-WzwS3xP1s;5vUoPH7cqIDB9~9y?#*xsmVv5yvfvsZlN^TiA)!tOQ4g!ad`N?#nEN{ zTpYS^GNxr-mpVPj-riz8SqwX9J{*&07eU4-C@8q77;Of1t$dkdVj+vP=1%#Qe3~gk z1DHl%W;&;{6G8=q8?$;*6A7JRj$bU20rojiLjeef7oB0PZH%lCR%aq0{Dy?}kf;wO z#bjX;7+b7g|KnTWz&u&nqVyd;(XwIF9}{k0)%HMJUv|{mnwrk=;&q;cq*7R+zQ5Y{ zJZDuJN@Npm^lM31v{*8Q5iq6Ae9Io+R`gYV*MG0(*PDZ=5tI&Y(*}hPGR+IhF<+aLmEV8&q`*0 zOk+^Quyp@gdE2r3VrTyB!f49j#RMD5+l@{%Jlb}V)6~+we6U0QK}VVlO?+!x5dhuz zN=R6y&=a~}5-=1GW5eGAoti#|0|>xdhRs+@Q-#n9&{sgMu}fv`;N|~$+6QtKnEP1J z$omXw)Fz6nC1b`Y+cUM>cCIwd?nVXJ>D~rA4r@UDJC4u9qu~e1ZsbOl>faj&2KIR) z-id#I`oy=*|1cD6Nn>enw%y*gRbQgVntT;MIX$m*u`%4T1AEBuRP8vo7=LigCVdHH z`(j}iJ6fSTC)W4YG(it3OZ5w3tCfJ-qV7Kb8{GyFMP|aa8dy1u zfvF#{h8KYxnWT-Am5s%)a1JRhUU8hUMC zCKYHNwFI=i<&PP$#CqO5XxtAcGbsaK&Ny%yh$jGqYVYsgTMzr%{|36>6B8|(zQrB& z+P%hUn|%>Q8)i%$mRrg&?gI;BxV1F0JMsqbBR(rLaP#tN8X0Alf8=_9XaB9QuYXT` zzcGBs_1+`e+KqtdqK#i0Pqdmn%CuGZs+>#;gx5Oq$^Ea4KrQZ_pOsY58S@==AK0G& zCeY`2EKw;p!J9!t*ylX6-{8i)Kn-;3x`^&2i0-{*c@OV@p71|x{AkOBJZ-p$QUrX2 zrVMob;Z2}x%1{)GI9Udf*HrEXyR9ofPUiYQroFk6VG>CdRlU`FX=G>cf`3kam1%^y zH?949^b7ZeyRXKXW3g}egRK=G_sCMR7KQ=Y>z?0(qM*CVhpXXWK$0rHgo&>G~^Ye$@UmC382 z(Q0`307uv{%>?L}d$hwA`1tq$FR0}9^ic{LIOUF_b>w@z zj;mVC=}e+Q9Rubw=Q%QetYenfj-|!;y`1t)tRDK_nt(;LM%~c$dl=$*M4scO>#ZUj zLCikvO7yJ@7dak!cjjz+cfB7yOlE`v@yu?a`ScuG+I)EFLuTK$Wi&PQ=YZE<_~qgD zw!rYc?(Xgyb^*Iv-KdOp94JZ9)*AuMq~+H1-~UVKg^KKRuLLs^>TucWQh*)82haVK z9A08csI9@n%ouoUSm+yL~EO^W4<@FL+l-#y?KLq7{Do``njny?VF(G{X zo4`ez2cY>XeQ!{GKRkp96`yWkrl!w1eo!^(`~ybg|KLd7G)+#H8lGB5ma0M!w?6uT zg{~ja|Bc!jlv3)gGF1i>iMQ3`Qw9{YSe?pkU1>}uyNvBPKM4dMWh(x!=g1a=FC6CP z3hY|zS3)6jW6L+a?H-WvtO$G5sK*p<<{wMCU}I;Sa$?{GWjLbp`3$|bBNbpROED^g zJUAfl9Jldt6j#0He!M*MkZSqzRIENgwdLw9|c5U zuL3TXAF*?qR;)ZWVNd%hm*2m=Qlihg9xF+Gi5jdhf~VO9ouXd~ z#pTP6=dz-+=Gn<*)h0OWJuKE1KKi7gqhny;VW6d>r3G%*xLkPyo&0!*7s;odSe_XmH^p-QZCi|_|2jMI=Z z<0xk-hf;-FCc)chZDEJ+N`xGp8%qW$A5$qh%mFq!_%;(*H8x3E1HMjfF|A=*bi=3egE??Z2q$K=0aY6$ZIFGbtko_i$gG(D74VMm^ zwn4!}?A+WXIx!d{KrHYpaEDfUP+3X2YzA^?h>qv&x@*Yi@a_1RMLVO-ZxjjgM~mhI+E#YYN@svLiNVyy7d14$H_UOl~k6?ctn zgk6miCg*;8M5_2sjUbp#H!&K3tfbZ~HpPG8Sl<1d*E?+5Mhw16&!>_+#X#6w7b zpHcC{a^Y)fgaRh$XOU6BO;%dlQJxZTp(!^No}8W7kF5O8xKywF53H27BG$(=Ry_Pb zU8Jh28u%>B$cj59sW>QCoe6g8ovM$#wkam3Fj(C7QwxFaV8}53+P9#2i2Lwpq5rp$ z7%rh~pw2VTAB0JqBesf$) zw>tgVBocfGY}fm*PRpBhu3^CVz!!~WRNb}~!ehnhdMxKgh!TCTE=;eg>U!PM@Q8>Y zU{%TQ^DVW}pblT9UcWmt!dcjgXH{TBZGT1#qn&n&V&Vw*o;ovjpQ1ff#;2xzQ{M4S zCMK|Pvdh`mUhda-eZ=0I0I)Bbb)BGlKG5x#R0#?S5|jQcffaW*q6k5?!-yloV}mSV zHY|QW@|JnS-k_@R)jCH0!pVfV+DA()#Wce38KyoGp}gg$a~GwM?v3a)gT_n|o+(OG zZ8YVvi8H*SM*)*H;a~63RbP5abKo9wYriwYs(z6m>sr2JNQ) zh=ug&m{vY5yXvK2dY-SqrD9rraP;On+AWUF9@5y>CpC7-u`_a;$N6gYzYHZFy z4{ls9O+LET&hb%sRo)|wqDXUOedrNFHw^cc>$4bol<$aFc2-th_llzr{2kp>s>Fdc zjh*%NcUH;C+qw&+IKVwyP4|=DO2qS+>)vSWB^UVXDY!KYia-kSi-|LE>QiuDSXDL$ zP)+BW5q}*}=ox+*=nWx`dibj}Hh+EW6r`FMDYK}uHRJpH0EcTqpbq}|n>_7!aIa=0 zdSG^%XB>EH{K{Z*-(-lTQpb`U{Dy6{c$7rbqzy?eNi z3fN2c?HY-J=SPHAQR-eE7w=EKu6{iYqpeT`GEX{%Rv(pAAkCO#LFL%(D`z)Kc8A(zZTap3I1 zt!HD?UNUpNWb>pxvgzPpwkDr2uRkb?#9-_VppdIGyAlFpGT4F~^xHomQ_Is-@?8~H zXK@x)lRrcp-gnDH*mV}f2qEDi$vL&}_iwa}ZhRH4)2iPGygim+u9>A!(?jU6JkJ>= z5kkChZ%)rHq->vru51olOldZ_KYn1$n?V;+P?R4n1FP?+O^l_T?Kk17OYyV{=(;qJ z-x6vjv(2DFqhU0C6#SbozJ;0aIAr#nwbv}r4c#2b-!QD0UUB?AtO;froSK?i)dUw# z|4xRVp6?p@?4DL`_dta?cW<16Pc^ZQc@AODlf@8G@NV7D&Ou6&&p~Ja^ks>5PVEci zW*}z48H`ccJN|!SqM>ueO+H-&HPVb|9o*}r2F5jX_15zS&;JiV2=+Ze-6S;S*wHt* zW55~_^ny%ei}!*beTszEEeaX;;g)?IRDsdC=u!O{0A}*&{^Aj6YVI}9E&OY)Mb%MB zI_JsAcwIH^!sy@L4x9;dj|E4qm1;86q3)i;MU>j&;uL3?t*MW{>KDfWeRNc+pw&h- z2IUi6=9<>q-HM@c8W{-L-0Ql=oa#xjKlLRbO$gITi72u%f6DsM@fxl!HTxRuS;#{0 z1O?I5*Jp8kiWo5EJU>0~_{B*U`Ec|JwBw-{gyQp)3^aa?k!ZBw4Moa0Tws}q?LY0A zMUy)-$y4{x2qlT0KgW3S2>*=}9V&6T3~oES$vd8|G{m`LdyAHZ(N*D{H&eu$h=Y<* zFL6Q(Y|gQ`&DPT-+(#(w#NKZ0i~VQ9=YQ33%wqRpqW;Kj;!S#B)Hs}{SibFNTN|?D z^8*^$i$5v+QEU9q&}3r2_nTf{UoQfOO*pU~pgA|{Qwxlrze;dVe!qDM%c0L;UQ|?3 zXCMsE@GN%~dWX9h;*Ai^dMb_f9ebLGzv&AW@roxSt+7tj+P$Y}8GqUKJTM@Ydp(D9 z_U!M8*1f$lEOe$0FQxn2QStV`IY%^Hq3QX>(3_D@?~xC%VSEmyW^^I~gMl>&V60SD zTQ{HYO~XcHW5bfKMo;6o5AF8-{nOrgM?Gj%uU`k`Z;x-82-Qclrr)d1 z9^F3+qInoolwTnmZFA0f@7u}SiTP&!j3+ObO9g;ZN#-f_JGRK+t17?cIuCdE`#Wk6 zIIor1{A``dmD0nKQSmEhLF|6`z=}VI+u2+8`G1(#Q!ac5UV;5|pn5tc# zbhW3>vlZ+q0pfVSX|`3&qPnZwcHt?b*6Ul(gQh?2#^ibR@53A$6>H> zP!Hym8ELd0;#FYw*jS$Y)^NI>j*gD9GL14Y@G zyA$UFT;TXg`MQRHzSZa4e6WO;*lRLhAr&~NrCF};HErN=uJ%Lh)VuXfoz&dnBjniB{UZU&?K}d;(Gl`BmJ?ggFYdt_UHFzMDDwifS>N>F zxz;D+CpN1QT--{ET<6=8aI_d1pe^^kR5grQD?t53{3Qt5MwvR7x2dgur$27_%Gt30)$}lm~R*75!}J_4olK*a(A zK@6IY)Jn*A>ENNusS^c8W@gWWdW*qyoM-~m65|OQMI-H!(5xX(JTVz{lFY&0o)QgL z^Zq@79EhHyG_@A`%qbg$r4^Mt1`EP`i}l}x){x~BF?-JFOaz$~eOem=`vt|c0fcg0 zl@t1npGWJm@B3aGPL-~?;tHmr_v!E(w%~Iw}gOncQZ&x zDcvdEAl;0Dh;)Orbm!19b2i@Z9Z#J19R3ZCf`$gO&?KgDRLwQ6)Rty#cj zy}y|R_U{7kzQ?_>gK7-MNSKn?TDrcoa^iZ9H_|$Kn=w3$?hTb;yyf-0hB&y^2)$o2 zUcds$Kb@14UIW5@r(f{z$xm>lXlQ^pAG3^o(N|~HC|?9DpdN3Wjs09@;{e2EW6AW> z*uY5glV^T9$Ac!~XXz@_jE`@80hNm2plR{Oz8v|(3`rGC2rZcoPRsaLX98PYY-XyTvjLX6=<18yPt zJ!I*oCFb z6XP(!F9<$w!MGfbRZdXsT*pFeV@-u^mAxyXgs;Z^&Zk1* zsL%Ly4IqiYWA${p*O%wO#36Gl15}V2fwrC=<70Y^^8T!b{iYXjqO*%9a_qz7y@7~r zL*msgxkIJK!9-B2jU(u(@ZCdrX>xoEoxE2;gGQjq?DM73IB!Ggg5Q18zViG}z77U^YqM_ep z5IR_X2cx=(Y4gkp&mt(2#3#nAU&bjYEW)o#2-Htta=!+GY`OFP+7iniKlrV^s%H4Y zTtBncM8xPMValno(>z($B_IO<4qC^N7mh2?UDmh9tTQZE2z2{SrR0{kt!m1q z3OXLS@_90nL-Hc(D|wfixQQI)nQtdnIIU;Xt@)#NM$~*O-TCq|!`rAU=w;6r6e@Y< zo&6^uwT&cESErwsvtm_%@__}M>hlN^zv-V>#&7S})?_$fz@qvGpU;F}>iv5M;l?KG zxI#Sc=tR*nJ2*&;>e<$8^Rx$WsvSTa z3Y@iR89K=Mj|M7P7<^gtj>6ursBmURmB@_FB1La#G3%obX!>eN%qlHJ%%kPKK4D|c!h<&r3|u02)aA(JDHEWTEITVLm8 z&yUD|3e~P^kZWRD13cO|dA$s$Uv!DBt@<{^*_btYjr%$hH-pmYG!Z~MUy-mgDN7I?Otq|e_1LT)fjorIdY%efa{r&g%lO0!N9wf zQFc$IS7qt=CIOo)zMSM3u*cguAg1Q-AUofE%Ug#NixX1c@9U)SPdE7pfI5zUqREbi@Kal=h02=B_$H{F~$^Or{=j*LSW4Mfu z*3yP5Ik?^StFKP&>12pbIR7t1Y>BUn$g?e;$h0|Mh(XVV?;D${ufT#wBWvH6!UKgY zoyBGRH`NJb)Xh|_0zv-{djx>6r@c`opTATNG)A<$AGmvYK<$UE|=9W z{TUM@QeZY@Rd!m-B6nq?Po=GRI8QXP>PPE^8fQ*6aWDp<{BBpX1qb}-Ve zXq`p`=?ht8GVEZ;_-4siuNC1%h89P}u)icRRi#9$H>@9AIaa3)&5sdqGn1mWF@XO} znC~f?RE*-9oIJm!YrJ=VMf`L7$ay~V#U8Vl<8sFWnYi0ith=jtbH17}YQV|iW##X{ zkYgJm=c`-IUNz6$m*`*pyw9X%{WFj|x;r2RllT6-y434xY*QV* zk4=o{enV;#tT|9*hrM8HzdQ+jLcqhz?)L7efb6l6Xztd1zhx85gjj!2(Ym5bJ#`buq7(ck@bZxRWa=C@5fBf%(o6PiGyMBbAa{1 z$UUsfs2Cz@XOR0fZ8JxU;mUD+{T=mi<#54oDqc?@z;&d|!rR~Orj5KWM0AvvyE=z= z#5spw0=VOEGuw8SA*}6}CtXp#?Sp-CgwM@&)W_vQq*^dpaNZTYUcdBOvB+Fl%yXuq z8O}QUrs)QRN9^R2BiX_nIrrO}o1ZDgyY5CQVoU1l@j)7zntmzq#EO2}+SK~W0iOdT8*EW3BVXknSPH8Q$GON1O9-mt!>id z-&z8;+<08XQA@K!{ecpUXM69$a)5d%b|m)=DhMcFSQ+QKAGXEL*#8DQo8MTf>PZdI zX?(<}&r(WGO3_xk96PogPJ4&*rBeBaaqV6seMNaE>kAaTLYu@R@LqFsn$}8g5Ke>c z@luOrZympG(l)!;na4<3a7FxC5%yxISP>n^%~U$xjfYLzJuPtOVIA`v^9hd{n6qk@ zWX;obROFedBI3+Coj3%ToYTKQ>Ug6v^ZTki zx81m53fW1F$U(GKIieRMu~##*Lee14*A*nmd0hqGC!u);AZtfQirgm+O0z$I2~rVo zogLNg0*bD-ot>4{wnvDEK52TRr}*G$*G%YnR0SF1f~oSvC^d#KVJix9^=WzhL*@Vx?IWtzj- z{Z`fs_)$8vyI!2U$g1*f%7e>lbv{4DKj7yVH&jahyA-k8*-;^G4c6mI4N>sM{z{Gl ztX*e0{c`XR)DO;gF;!NmWX?&-&a13iq_MfNK_=q;Rh47sQ)gdxr{C&K!6o?o;j|{`_YwSUsI=z{B5=MT-Y@> ze!l7Z4@~g&w9~<;+4+RlvV0MAFUl`^ldVnxpVFGOX|=M@ljOmaz}#_#aIN1_v3G(x}|29*~^60@$A)jf$tx zOlu|C;t54f{=RL5&#|UAo%PSr+k!aLa{`^0c5Yaj*wbFU0gseXDAsS?`iw5E2kt#+ zTRBY3Z=MFx^d#kq=fl@xDEjP;563>Vay_FCef?=HCe7gasPQ=&mu8$P81yr+q3(ks zC9fgabtnSphDrQ~dVpeH0I#vqr1aHT7ao7CjZ%v{|2LSMD>|8jOE zEi3HT@ro1W6)TBK+$t(F5DKwl&Uj-{%F*S!V$Fm18k4YHqhk7%c297AX?M5OW0=H; zlK^L27j1x}5r}Y|o}V<;7Sat~%!qLd1D^u==IlaMv_?mfwqi6OmK}nvby};@js#?E zByq*0Zlmsv+{-$OiA#s4VS*rqV!5&6XDYKSVFu@vj@SOD9nvzYa2L;bNtutLRoWFz zytaLC5!KQsq3VixIQF)MRIk;a9+?Qog6%wBv0{rL(Pk}x%by@4e1m#Aoj}WpK5jz9 z6pHNDj3ItnDM!H<%lVL!0;fmE{VHh$2ML#wlH)O>GTT|0Q1xza3{xj8%1xhFDJDhz zur$AeDo|pjQQq|p>6$$?Je9NRuE~FH3+t;!(M(!$Zf;R80}0^3$Ekj|97B2S_{glC zJ#I;`^b)j6HK0FNc-Ck3WnZvYdffT}wX`->nTWf{aNf=)z0xTdO2*HZE3m(_%RY~UQ+L``X2De;WoiQtsaaqqQ^ zkoKX-lAbfNJsM$T-^ecmIBpHRmP#mnOP8spsNg)*3XV5C8WeBbT*g)zYbk5#8x`~d^Fl(-g5Ya>jxb8CDT)O19m4wMUU1!< z`3f(Gu zimOy@jt4rEfZLhD-&dWde=28n)3=k+v`$n_+hD}t78r4`e^Uf7h%3#2_VDV?CgWv) zskhvLP2a1&{AX|8iXea^p2-M+;&71qpc)!9)In4cR6S2XOw@WuN-mD1@`mq6#6h7_ z)WK9{R%d`FZb~9c5G{NQbQ6l@s+M*99tBAeQv)iE-Qvksen9`OkB>P+%!~B&_#DC4 z-;l!riQaZHK|Xhq0!cvdG~TOeUNX5t*9eyRsl}_m&QBFSoMis=$j56Yb>EICIi`II z{wkw5{Mn1ko^@Hksk0|?{Mt-0oTY`Lg31CH&t1|v_&=JL&ZIG>4{z0)_dO+-f!F@k ze&=t6GYnx?vjJY+tHMiC3h(q3q(ZkRwa$1vvKYRly%og^CMnk>vvv0hqrr_Spi8jN zb{+YB9^FyD!ZW#WKjeR65Lq;v3lIk8ceGgac2%A3O?A}js*>0Yj0_G>U0vCNY+%po&vfj8O=ZFj z^s>nNwt7%?!B?l_cH9UcnyTa#GE=U;*byzzSIOoDd`s#zQ*X4pMbCC}I<90{^82I7 zFpj3>b8zg62&=L@ilMTf&Y}Br29+V4^T@rlVU~?PAOMNN!)rBV>^Q#?_iqk^yH=zs$%iIqEq*?rWB&lA<(-5sAGE8*C6%CqH$t3z?tq;(Y8RVzaWi%Vh^lu7e1M9asy>uR&Om4mh%%#s2lnk{Trc4AU z1wPQ1ra7_weGr-Q@r51Mv>Dyi&UV1}lmkaTBk*Ok8=7=v^_>@gO4& z`aR!>+J&Nu=I^vY$C{_$ji6%`*i`$=9B?TJ&-P}eIg)N#iP)L z1cV!P-+5NvTw#IYK+b@!72h@kxxE6|4T+WOiK_Ka0vJ@cKMclA68j0BVf+zBRQf#J zez}kW53suk(-WkZk5d#5Q4;7z2uh;+_>x8TIUdKkYgOmY24rxbSFTv_Xwlm zbyyy)FqqY85(uCsWg<Wfb$wv^F{b=tg%jWLMFs!jbvSWP%a)pZ%v{HOo&j){z;bA@j)1!t;QRGi(@tYr z@ohmGc2gnLpys9jS}VA_2tP~f+|M+)n=hXk0M^%Hjx);^*-f2J05_|L$14dC$p7o8 zl)|4#o7RB4@xR5&-|6=NfJ^cb*Wcw0Wc5EQ2g@7c*3G{|;%PTucy>E&5x9ziOX@!$ z)`(ve9$b)`mjKVOesEaCqs5i5-@Ki!jvVcL__0B4doveK_w<=1Y<$?BuMZfZ1Yj7} zJeP05;lucqHqdHTCj)Q`At-^MZgDD9U*;nza!BB5u50S?`z>rxHiG5D>Wr8w?C`M; zSzAAyJG<{8KnA>^{Oe6xrYZM$$6vzz7mifLOTQjhcuotg7)+6^)Q4~%h&6@FXIFLG zgafyVS}tu;d8qwlgPG>jm$z%cBndO-8=(I}D7%_60WOhNRqMmA)m&9(Z*zzV0x8Q^ zkEjHCyArX{vylgiyQzG@eAn8A-Sb#h{NJrRa1CUX3|1i@sCsS}n=ZFI3KHqSHC*ygf;jAYkgLBFsn3FE)Q%|8P7>?f0w`H;QM!#Q2fBpWaVhV=vYs4 z&M>fhdPxe0sz8pENpNCsPWu=$6u=Fb5+c6(U!l7)2{Ic^ZpSkw4+OR>???jW1aj#+ zQ((6H^7H%5ZK66@Ds#JJJzt2C%`8WlN=(lY~Z~m1V+m2Po_+&SSWljcjy~Q68{wpwSTi zU;cqDKpDU;$n--vl0o?wrYNdzRt9?d^>AA-uZ?wXA24Gbo#tcL2Y`Pow$=N0z2TAZ zQGw<>=W|mEw7qOV?TN|g^n-w7=zOk2MI106E~SA>yhUDVf_)~m6f&pl^)Vv1B2Ty@ zVZA0^0)V)TbeBzq?_soy<9x?}61dO&mX$m(IH;om37ppGe!aS}BZfqJd0tS;|AX#_ zd>`gC*31v4_Zy$2q*JPo;U+&+${>zF#GWKDnQlytMk6L>{8tTxE_BaS>z}#)`CxI= zHqWbQyhtv%A~^NK#~k`H0(zStH%UzNmmnPfAX?{Eb3elmzd4v1a>Ibhpst)|n`r;O zD6mrc6_Ix{S}LCHe2ZmZSb@!ZTv%0JD(2EU{itbPNGqK>61v}|F*G{ ztg6I)0se+Yjbx?_LwA4`CY(?);mqI5Cl+nbr~>mVxQN3a?#2L`YX=8-yktwNw8`UG}y#z`?>TyAuPX#DqS(L1!W^ z!b@NZ;aU(yc&S)IW~0U5et-0+2uXWH6*%PJ09;5?C}aPOGM^DJpzXidtyb4);e23W z#bYr2gPm?laZL&4cS5#Mch(cNahH>kj&QW+;lzGZSRNh6KseEVU=bvDpHw8=SF;mG+M;6#0G~-|n`gnp{f`PTS6R2*)&n zHMiq%IC~3olM=IvUZlm&{s%vC+IQu}(|S+%`DmJO#)0<`^hNOtP*gg4&VBv!zJUX) z8RpSBcZiKj%Kj=QBQDwBW9Cn_8lADMabm&3iIJV66=ye4j`W>jTu(mIty?%Gkm&_L zx&Nh@3b-%mk*Jecr!~00I~U<=RVMt^@!_6}*Gk6jN|XP0y8?yOlronsi1pt(lg@1w z01PMzu%l&+Tkz*BjMPgK$srtf;NEq?Sq*xYZ)5NJ{I8!I2A)w`-tafgI#twqp^$*C zo64Z2z-AP-8!ipk&0xZ)UbD!ujTOs!U`i@w;yu7MX7C-)52Q>J>VU5|3??zF);OBC zN*H?U$>8%T=0Kg+t9=j{`s1d=ySt?_eB{__`?YQ9#nbvM$tB8y$-nj7-}11B|52m` zM0%?V{z@+uUs&s$K4^Gf*S}xsZQJ{C)=sCa_LJrpL-wRZ_xLq{s_R**v%?5{mjT%N z=VKLvBa=tfhMhBZIIJd?+2LZ|tL7`8Ibg+Fy0BGjpQ7PDM8QrihW75-c_L3Fu1Vt_ovW%ek5JWDEc^8B zd@p$m2))Noc94rS=JxjqX)}ta_ehuzC4rKOmxXT3mk-JZ{k4O;GuxY`2;W-G%>|8a zN9`^^UpSvPkXxpX{pA=CHGeFr87;lLLK?Mi>EXJd9PyKZ930_w;$Rb3?zhL9+*uz0blB52OJ;lb2l<%)~A3Nqp|I0dg1J!tiKT7acz~Kxc6?d#|68U`De`(N3B|; z7`_TcK7p&YIvz=2PHtbvUXfSjdX6zqkHnKKFYG@U!VT*z{lArTd@XhOD9>tcPU<;A z$Uy-2hJsDe;k?>?Bn?c2*>!&+;p;1;oJ%@RsOWdqu)6c8Lvmw6rC0jqx2(_V?iPM? zlrNAOQ3nSJFv)|i+qUn$yx30FrZ-^wzV%s8{^*g)JA-gjQWg7wDmH0YNNC$KFtk(~ z4%2O00!Th>5#6@Vii(PY#~)gSM|l;PoE~(W1n@!=?(2ur6!4Q{j-#h!d@n5F4&@X< zwe|t#GGS)7P@e#`--M6FFst>28OZe+e^fi=4XOVtu*bj>q~l}V?9qz;>S*UWf4x%h zqQCH9Dx_nc`)uyyyQ;V-+BY2@Bz=>cjOqE{gcnigse7@UbZY(5)8X@iFT0;e{$lJW z=`>UNHBM~(PGjT2ph<(e|9_QMuF^Wruu(@OzSKvldCX^=2 zP{BAeH9tW!d1fT)XT5?VGVQ^a*FI4SfnhaU6H<@-I+uQ>yM%iUVQln%Y&JnY!asZ{ zS=x*E{6MPPja-*B?4((qhSq>p&hsobn!y}Tu+2;MyJHNM73_IdQXrxXl6@aTwgp=$ zANFXynM=QfSI#>}j7*MI?nm(MF!Tg3`pAbZj}>r%Ul&kOSMri*fb~dESUOStcsMhM zUja)>dd*%v|4h7hd=fsF$~T?zHmhr_U-VuM-9;Io^B6D%SI?bl8m~msss6wsrGd;^ zoXJUOeh;1VU{{)1uqfe8FE%js0T7=6IsgDM{|6=jsA^YQf%mg;zwL6CH*#W0?clFy%-s9^R%h;Hm<=w(35A;C$eFj z+tSnGwG0=L11piD5Ly>en6LN@;jVfEO&lAao=9i0J`X(q{@*hUNc%g6N0|IEL)}tcS*&Vt z`>v_!${Xe8$*XO?^~kLlA!tk*VGy%ig&1V`^l^`RbT#^_2Kcob`duJmi+N{C?)(8J zh2@8*1W>u-s5AeI5x9YMU3pwq-(FTDG8o!1XZl&fL_j^R60B$`i=E^dki^fy|}$t#d$gipmbjkn>-C%C-9 zRPI9AW9n46_&#UYD&aNOmjzczy<^Q3j~YvcmR*J#*(by=+=CSQYVJlJo#X@`l{9rQ zEy!uK|HY1H*MqtMxNVNVk3ZevJB1CH{|PAQxBm7!{XpGpZhd-|5jh3IxjdrW zsD?9nmop&#xW9d>v7{pAh@;aEu10>#JyyIxaJF+LZ|mc+8%sO#eRuuyhc>Q@figvxAcxp_3uXfG&tYrJ{gB zLKGy92}6M8XSO zQOJD$7NB$f-=d}2BI8e?n?TXs>hn2zNzL+dV`DLpK}0kB!T}L5jWdiL8c_owXfPOA zS(z`p@{8x}&UYQ%FT?m7-Z^v;dHqrCeHMn-hXv}41T<(go-g6Ru9lCczU)UNzyHyC zLw_lsXN>1_rkXK8!IxYsX$8!OIJ=gro4WTeSlm8JDw|eseu@Fy&Ck)${x1>FyHP)P z?#$uZuK+FClk)s7b-F4uw!srcW$OkBK_daJ-oGv(CNbVoJdvv8CFZo4fQ|JPI`>ml zkL&I?2BO)LviKY!^mUuze_L;U&+U!wUkH)K054)K=H}-O7Wc6K^2(qS-V~=h z+w~d5XP$m`nYS@uc8IEG*Dbl#cO!0D?C$%&B&%KF?$3pA5!mXA$s(bEJ>L*)K}!^5 zK6m>w>S^!!Hq<$HX~(iw55@m%WetnW@#wy1ug8k*59%N9lW={o0Fb{NlZ(@JyP$Y1 zUi4^M@22O@=c z#V4;XCamC1^hqhX{7KeEy3+@UagDlx=c%eR=@l>&b$h}Fna9jOB!44sJCOFq`;EEx zfOM>wZzlz)n*mFUVVUbV&!*+dF-cKg9#<%orIdxB3=r$pr*BWczcgWVt-F}ogH6u2 zcXO3(q^@(1-o13um?*{F=W#94U`qZuwnMBuY3qUcFhl(>0U8kI|Iq-dU}6ND!c261 z;1D-N1HEgxC~KuuHayc`N|Xr`xH);_cH5mQh7LRpO7d(sz1;MWo$)Uu&@u7oi(5T? zMfU%rne@$fQn&NJ)&vI199O$lR@vS(^#^R7TycRxU*x4^v4*T(Kgz&}(1{U6peaY? z83yP-bYLLW*)O+CD1iv|6rx0AIVL~<`HAcI^9+4Pf}0DO5Oco84HUrD7BQat=1EJW zO6QTNP0|dix&vtyg}rOp=7dD(lK&4BfI`-{&F@$~!gzJCJCvJ*!`g=!^eN%}ZZgox zi9vFij4xaSYx`TYJa19OdF}7t@z-A8G>56#;y-!D`z&WsX?j;y?6+^jcw%91#-kns zFrmrA`1cE`TNzF4CTY7AD-58C##3|d#Nv$edVdzu4i2X45SHtD~02I1$+FUz~Yvg9- zpdwkuLq|JLryG0#;TV(iK(OSn+|6FlcJ{|$^{ZW&h`INh5`IDTbP`p>Z$<^ES8hWO z9FHhAu5copHXf~q)5XP%ZR}e5ROA?XlcZ1tV6?S(erMPdmFZ9uZso<6qBZY!J1xE9 zu@`k`iq$N`v%P9XlL!ZC!xiMrb!?uvj~ILQmeCusdzn${gm9DWl3oih^&xw{XvYe1 zC(bj6MB#5=lS0+Tv^qX5US|W0RQ)uefDa0`1P%!3Ld`?N2C;*^-zwknw_@QhF{uxL ztf@J^P|iIP+h$f3;(t4jA77+c0AP$uh4_?RzAB8gX6Y!|B8FL+k}dHkJ>CAro

696hVJjY`te$}>KCuCvufNK>`2F2uoKYdvCbvwX8ftu+&qWr97h6r2#0CJ zCi6#N7cwhaL~t_>fsJ+6$G4j?Hk#}Str*o^#LFc;tHsZUT%3H>9c3d^mz~b^;io2D1Bq%O^C;w-ek+a~{0Vb372xBwP zQCjFwmjflF*WU`wH5PhlmMCwXW+ z-^i{OYrH-Q8AF8#rONFxorotaq%ug)VJ~Bt&Z{ujNjeL(=!pLaSir7s z23(UTCY`m@Sm_VnjU^{@bYCdfeas}Y2R}*7>`qs%pk;PF(Yths!6**e`)c{mC?0JIRaiL zIv*gEz%r1R2gF1$FUb~nUP2&@s$`IWn9NHY=Se)%&WKHHN&hl=##jNcdSzNWWu>Kx zbkQD4qo9blohuGa<^`ZX0MQlK85u3nM3w8={1?-jx%A!9SMDh^qv)e9Xsu9Y5>X%M zDwQNr>+zykOS+%d_vc0M@f`n3bw>p1EK9hjp{V!MqLI*9d7C zw9yvQ=26|_zyym@pv41vjhf~&iM`Ob{Ch2}#=mkUcQesJ`*S?geiXaX(PcXn$Y{^- z4Orj8>KQvFSjlf~*(gB{IJugCZxbk!H?En#%Gf`E5{lPN(>3~R#G#gJ$8n5}8f))6s-C5@HRIz5~4SP&O0~dp$C7%!zcbZk+ zaB+WJ>tSuA%+@vX%2RS;Y_YQCz$B-X3n?qf64eUERRzby7@mYGh?5_*D;2|DH6OQ)i?k>jvMDbr+;^tHy2EMrg z(Ec<~4)RuGUl;%O6a~IlZKW$fI5^$gYt-n3?>}aAef)W@dIj7!gE6-4&7zxRh-gQA z>ov?Dcp-oX9Rjb<`thfVU7GabUg^#Hm6vA>8!a}vr1)sTQ!~}KUEa!Jh9`ufkc!{> zv`8hVOJZEWii|syyf!QnEJoX%))HzfdH92a`+f+z^i{k%ndBQn-toU~j#u|Au1fCA z3$jaw7&=PA01kJg2|)1lLHsk6r@E%ru0>gxB6j}X@9qtX54$8 zmN$bZb$8J%9Sm&QTIS&oPrw%@u8LWO-3E-=$k2pg0V%P1FMdMARrzf3r?OS&Zx}!}% zSoNF^h;%)ws;=h!R$#AVu$Cg=zIA43VBf&h&x{K=RdAPn{ErsET7K)TXV=HEUx{XQ zC+FzlYefK56YDH>U`7q-xZBWs&KHUpkol)+Kl4!@Kb>FK{Nw|>r7hm-w^Sf!OER1eypRT;SeN~%SeU)Zv;?)STWX1rRD^UW z#r0cK1(MtzsV><&K|aI+G<12GqPpb|BGCcC00(%d)W-sTz*0EPIE?!uyZNHOcAv{; zcLy@FXGD8Z@9NxnyZqbkkD%63V6wRx=bH4HO2PTzZ%oH;sUuFRLq_wuc~nSrV*h79 zX#)7kS*`r-K9GL?e#_3mDQ9K1EVSBWjzEBRLmaZ21FeQlSM|yuY4m7M*7U$n$axSn zEOr1meL9>g9rQqDL&ZL!zBu`>FBGVlfX$`=)PXN%HweB<%)BLL$zv|trmnQ8_OWaq zGM6ZYU3q`;9#$;jzwWZy0E}llY&+M;nYR7GUoDPplk;6mp)+6aaMXT zmO+n7<`XKe$x{VgXP;6petuEzH+-D=-e1E}pME<{T7%b7K$ zGe?o=PGW^-CZW&yh3;UJ<)J{t_Cf~a+^pU^l4rZ`4$92vh{L*b?AKod`jnd&5j~Ta zj+$&i*yk5%H>TlZm%QmsAt60HdLa-6UBaf(uo4~?k}54n0j-NNiB_e`eU_Aj{?aeI}R<~(R*{Y zwio~I7(Xki7h^O>q9S-Lfg)uYD;C(l?FClyo8sJ8kNnv1pDONqS_p8Y2_WSFebD8u zK;q5OV79EqK7CihdNXQ})g{2VwI3kXtdLbQlL8gb>6l&Lts}Ei5RJ~O<`Tbab0gCE zQ`3W6T7J$tE9rC`dh3e`*qkk7T@(77|Dj6!A?2hQ!YuQih0kBu-R8KaPf7j8R|}7R zcl5^VwY*n5%0eECJ7oXz$CkW8(D2F@k2bdGMyJ^A>kV_K&(RApdz<6OhVsQdMymS`y12Ee*T8~I&PIhzL}M0zn-!?{H9fXqTr`{I?A zLM!OQF`UVM>;A1nnZJ5JMk8g9y_kApW(5An*y>%X{REVVkz>*wZcDNA6?_b*E%kw> z<#z)hSl<OXq>^8j|vJ$-4JA)RyGsodV2JvP5^%afBM@gz%k{}e*762T??(Y zU6lMd{-g2xcx$s~|0**;i8@1E+a?LTVkfQK9qunazoi#B>~f3xt@lnzkUCe~qr101 zxh(Ajou^q*SryT^4WF3mN|lStiA^K`Zi^U~$#6F(Z?83NoJY5hTi&zmfJLLmSo-*` zxLAAIf7NXXUb$tw$r-Dvt>x7X#BoV3>Ben9zd%*m{si(CQ*)?WeS1bsW|7}Nk=@g& z1wf5}Sg>;Q`GvIX68b8J7ja##3DV^`E4f~mV8m?jzUhH~zBXNM38Q_u!?s4m44TyP2*~3@#lh6_cC$`+9Ci4it?8{wgF$6zeClQ6%+`8(M__s4biBKQaeD6c7wN6xom#`gE49Wi1IgQs;y?| z!MuAYm+jMS+U8;8wSBAE*s1j_oD2WsD+=$jdPU51z4_G0Y?aAQ(#~U|OCreC=pl}e zZyL>v2>Ddtmbd_dC}{L-V}YFbSa#-Y3dQ{TuYt(OKze+sl#X!2VXbI0$458huE ziNr2o5=%t|YGqSgdS5YM#32v}b?mK_GsIb8(k;}XuJ)gUI( z7~j*gxc0-%DQ>$7M zL=u5Jx8&ya`Zg@$xX<+@NKmk~rAW~H>lJ3xNDf2D*=sJ}58DPXvD(pxjghyp(g@JH zO6LY@?IlzS8}-R$cAASRuB!pV6-BK>j79%+Sz0|>gj~lyhNya;mgqJf$iCbl+tU%g zdZiT@7|7Lfx!N}m^xgs2MPC^rzi-lD$s483*k-braM>=lcrG0z{Wzoe5R=e7ko|*B zJo?@IkB_WRJUxZ5ovpiAluX(5WFDHY666%izguC+(N6^&!>B^V@<~JkT47fL`PHJt z{l)!W^y+?}9?}*OtyCF$(v_|I2CFP~zm5d=gjVj@cL{1k(MPjhVZ&~tF+bsjT7#*=AySfj z99{5MsE%9y#j-?KO3eWfqM_HUe}B#8OeS9y=<>ZQAm~ZF+PiqsHV&f_ou0DYsL)JJ zkQDdx9(vVs&ZVzP&8AE&G{q4k4zZewh`Z$viYhh!@S93V>Uj8FB8KCHj6Gz-Btg}Z z#j{K93UOj&>SJ3pC&{<7@RgMQ+FJr&6Bh^+Mf)!2jpT7*i>!Zb+=SybGP0x1pYy=zC;$hc7G- zBMYMxr>#yI>3V^q4Zs+~n4?57zZ4_LncG zy;iXdu)cuFSm!Za37@Y`0?U{L@xk&Nfu@Xps}r}}QPwBuxD3<9Gz4-Z9{o5llqY5> zsOZTpECZgEpuzPOiQ)@vVogU!2%yv1*|Excza_S!U!r#6(G)^}*1z0)qgUT!#?->4 z4HR2=HZ*bsYzaquG*PH?e!B5z9GJ_cWMoM2q}7&?@)B}ILI7j6F=V|E_D9xC#fij^ zM5(2|kj@%l`7p~>=1$6nD<2W2!FTA8-q#$)*PNz(rK_4+hJT%Y^NtT#=&y@)ep=ijLOZ!`K=zx=}ydLDF|UHL5&?Tzy_FDnjns2`4ESdwo5W zjp2YcwtH8I37zQy6yF|VvRB8Z_Dk9Al&i5qA*7Lpyv_q4VsBWn6DPabPc#(aVl`PA zCIx3=Ae8m%{8`pzr@6*5tK;cnxR5sVrFSUKl*Q9DWGlNeAONy#VJrBmCtC}U1k&U=DM@>ku*cC4_8vMq8I=7cxkx1yN#EE?dqVB`j36F z7VBM|x7Ii6c&>7@Ve%V^{pFBX@H8$^#4J2bhv{=L%99&B+RSs9{O_dd&3@tQLb-g z9%}j~E)YjA+U5HpKp*I3BGaAr8*gM&2k~g10Q`GE`$DHjstk##fG)W*85Gq?UCy8q zWNvOX*C?;Hb3wwRwU@~E)syhkvE}L(K`WqeXi;IRAZSfBm*)P!-9@1cFT)R1ihurLCXxet0^uK?!Y%G9v?|!n9f4WcnRp0LYNtE$|MIp$Q068SeRdd5mM9p znCNnz(BRT$6_iRBi>M)dJd$g2}T?ra5nXDaP}qhxWlM_tc? zQLHa*elgrBJ?_sL%tWuJRSdToLSl_N-SWC3p^;esf)G{LnX`@`6OtRGV$N(U82~Y9`R0t2MAR$)QO|QG$h7-pGw(S?L{DjHDUgyxC#*J?mSeG1 zw=kqn-lY^TU7E)53^}KKaoLZ5MukeC{b>#N;w|G@JGeKkyjlI=d<_i?7%VlFOC8Sp z$O@UD(GdVB7=qaBzff}9x*5li2*V<}JMxP|Hk-itv9Zc@Z;Pgy8N)L?PR8>8uBQa6 zs1Qf>I)g9uMeGMcD$D{91NL{prK8L1@G3^PffR_#8O3)Dtv&z>IXgSYXT+=g0CYFD zzu~=W02IGeza^ZCxX{=aQ@frtE@q8bX~1!#O&wO#(h5DqpT-4e-r$V#@+KSjj&}{a z4e$9U%4#yB4qa>^veTwD-vSs&jxx33S0vl_M^8W2kVnOl7gfjmzHsM?oMDcl^r305 z9&L0GvRV0_$QAjR+!%sIOo)~R*qb&DhXne*K|Mbfw*AEth%d3Xn(IO8tSOE=`wjYA z^FMjC;;~u^Y#LAn5=u7-NNfqfOA@txW?!!A&r#U)n6G%ok=ZMaa_aZhhaUp$k`TkM zd&9`ul2F7+E1MSuak+)rPmD@`Wm&$3`kI4a?V=mSpL`?G7~n#=8CSIRWMQobE1nf% z4+kZ!Y?HT=&!pLK6fF}?d>{;I+p7j9Mv$Z%EzI-h{=i4*EH9ONuhbG1+4gZQ6~VRZ z+d;#?oiHL-m<-b8sHZMi)9_B~^t5lHDIjJ6$ToqN!o3~H2rjQ}dejAZ7&|6CXD4lGw9nQFaQU)K{eXSce~lbGi*G-iG#W z{OI(bfjbB~g_1iz!6ntU=f!9mq5*B_T0XvLuIt)xJixR(o@FO6_;B+5(Ee5OV@b46 z3%0MKLj0?y%Zit2|p3bufz00tbHF>3F4|dTFD@&a0ZN*!-qgi2cIV&q`lT|@0kO>zexhf`x(bCfL@$p$GpU51sD6oa^+@wjF zXBK#y?c<_caD!1BJ0pOMStp>z-lUUmyQfYDq-X!-C3%Fdt}YH^rlCHvCyOLiWacp+-TXC1o5Ur)qC|pds(_EAX3>KcgjpM=v$xn8}pTc1}Gn zO_c%bQdICRjoHm+CPEY?B_BF|nC&$kf9I9NoAJM4!U_rsqST|fD=>qKx&6qZ-o1U@ z-@AUDw;=ktuc$dcq!-)lQ)mjw#jr-j-Pe6pFg-a*Vq>c<-l^^P!}d>-UI$30wjZHB zru9ubKF*i_h3OY4B73H4vl2=@w0LG=_vsabt&?9i@Px$pnQ$&8tyj@8py>0Gg26yUYD_-Jh{C%JNQR=kNiaXo5aNYCR>m7Ezc=qf^y${J5Lvy0BEP2RC?;k{dYeB8d+{lM?o58866r z-nRCo?mc;JX;jCn_9&;IaQ;tfc4QEB@I zQ(kMc!bn%Ul!|q10Xr|Mb9mdHYGGKJ8mMOJ+ewO^&C$+z#(aYkWB+CzB_-f?Xqc(2 zKauagwv;Hp(&Fd&T2RFnsSYe|mQp$Ob*~RaCEab$6$Dd_NnL+1!Y4 zy)!`8-5O&PL#1be9Zd9jlKEo*7sGex*QpOLYI+6T?v_DWD9!CC=95Y1csZ&r3V70a zGb~0eAt!01!Y9_`)5tuS=CS;0q z*!lW7ZH6p3ue!5DL%cD`Bgfq4!Ns@8eoSFuVUCWDBbne<*CXvh%)4PGFK;WTw=v$+KRB&-g-VioJSN6G4l8=SvEPH9SS>E4 zv`Z&gUashKd1!NWN!lpjyb5I<#9fvD(DreM{~$>vbA5++Q|x!t7m-c3+w+L0Ho7(Y zb=p2+9}bj`ma}<`((WP+GqO&MQ9;7Ooxj5JrKU2A9<2EC+;e3ADk*t-{<=Q-t!^M} z)c@BB{!Gr;m~x8zMpZ^89B38zUahbu%K{ZuRgyL1`n7^xtLT-_1#0HBCmX`cC={|% zLwBFWb25ZzF^!q*f2_FrWtmrg4+)cdO^#cyrj$U0Ik!XZl9sr3y;ut+=s{+8M=6NH z3FW2zrb4$r-S{G(==8p*mT8dk%b6Wt=Ck6Y_`wK3An)NUrA+GA@oeq<6jDuo2TM~% zY+E~Cxj&p+4jj*G-_mD{U^gezY!J|)Z>p~n6!tz?fS-buG&E|B#D9JXi^8Pri7$mYbwP@u>m@2-wZG9jTY*fo5{bV8H4P2& zNI6)9R!AtL-B6LJv#tu!Yv$x(v#>?f8NV=#@8p%uBFJi{o5n74f9oyu@%h;FGJbGw zUY;@u(ofa~8bqzcmmi@SCBGd_`W|lY=$x=gnI?Gc$F~<_H>53htCmgJl6`O1LZsy1 zp{yS_o@At>0PljI67CW&NxdpX)SulE%g895bbs;inb4=Z@@c{D0;G@IOH&gcc){6H z;A>(>403x8STFc~Ax%GRFjaE~K9r~4L$B}BBcP8{5mE&=YA4a-`q3~jfc+?J3{N8- zCHAP0IB30sOB9bX77VfI&Zsu2lp9xyyEG{ZMb3q0Lg2SMnGm7=mp};d;e3$UobeM& z?+AxW2O2jBB*^pkmZ*}!Za+#;DZVwv7s(9-avI=2vO76uE}PQeT0X5>Y!zVB!zLOG zFoJ^=L$@C)bM6eyK2FWHBTwW_s#_l}NGro&>muT@$YR)3adud7)~=h)Ip0s>WN-6& zNxcqR;RmhndB2|1w6r{*f&+-ijivri!;9|BYnqn-^a4mULi!r7`oQGSixE5!%R-+=4>#RdGZ25 zt-`edHZpvIU7D_S_g@X!lrbg)E(ibs$s{l&Z~Xf-QrEJlj*a+U*i_3`(TfU{PBy4F zAdGd(wV$g&K`59T!Jk`-*?&y!d)Bh4e`eOzd_(XC-otAiz?Hc`%$u(_SD>5z*esacQ0o9A=PHLp_) zbm0qE(G;Pg&cmKF-|Rld5dP(8i{zoZyu+!I>2#bseDcH7#^c$ATe@&6#Ef#`GobSU zz2ExX7qhAQ-8G;)JAr8d41M46<*!!4@mob4!lb;0h6X=rcxpZa_KmC4F=vtZ&Jh&_UB-+zDv4ouv^Z^$I&_egu@tXtL9req$lU1=|`amcr%2Fl4Lzpsms*2 zzy{>A2_Fb*737jWLERI&n7#BtZN;bZ_hl(xwyJH+%UnFPNwunJ1z8hN(9eBQ7dJtx z;B9j}Iqe~{)FpWr{5VXTg`)YI#C_V`*M$#HKz^fZUD5>tQSTzL$DP8?`oTW~fHYL0 z=xG5Em2D${N}>Ss2iTi}7tLd?eG&`uTZ`4pZ1EYlAuE6S&_Yj3zZss?-n={7xww74 zR%Y6=4GR8TP*9*M`NZOj{(HoDa!3(d0s2$meND<_ap<8pDhvRzcWs>gMsLsW=;(NS z*qCfRY9d+pS3qVWwJZV{`DVAD!etj4}4qq4k`O|K= z)gc~l73*p^|1QC~FpBu!!Rd_4?tI!r1Wc-EU7zZXH)zthh z9@gjLrul!2W42_KproAS{=>nbB%L=?y1z~;0c&g0H6anvGpnN3gallnOfnu$gzS63 zppbH=4z!|;rIoeC5{Li|Rs_dchqKfdlfGR-uYpFd)M2w~ZQ!TC>GH@r4_^t4l}Xx2 z*zoy)b6xU12`{F|w>sv=o3|gw!@K!jZ9H{ClYUUKJx4r0B}4J^i2gzg%$1qWyLvge zUQ7B#xKCXVmS{yerS$a!0D&IQ22M`SD=1W%B_q! z_H2*4U8!X_2eVHf`x++GT(sJ0QEerIMSz3>7dN*G@Pj<*Bfz6km;>UFxZccM4bn!F z2*ivEA#^V3EUD9Es8h^!k%h{G+9Kp=YC~XTejY>0$%%i2idL(rJ)4tuS-ztO^6kr9 z_p>jLDU%R_R%<*O4nO7}OghbE-fLc5^>O(cwfhU|sKb3#U=w}gHZT}alt4qE;W!9` zR86}sOjo`@DeX^>E=+nk=A^2DK4pSijWf%?ZGPYd$ROEQ9pe@Zy7*Cg0}dN^vMSk5 z+GiBUj-m`4Qx4jfOFsT4Ejkrhq>cm_sEu_5d*HAz?bOyiK3b|t%S-X4=Y&|ayBf3AF#>?c-3aa@QuDaeml zyM*h*LAkT8gm|$6R`v8QW~?H%T69nsRlVe^O(jakkN$zb`*TTH3~9&}2aF!{q-bg-+R6L$)AC?G;X(3bJd6Bg<71Q@$pRygofT}vSll20$>blBf z%Yu)k%~w<3Y4~zWiL1woayaRoWu+{xa846wD9~*l^3jfQa`)i%QbvNa)FHGBADxSj zf3}efvNkziSd@MwNEJPZdtOJ~Dsv|E2DG}fn+)UUcx$lw$NB6AMjDN4_anTQBW6Cw zicOCxYbVHG$G_dYaOg1ganszskcqY9Fc+-Ufz_~CFWD1$A$niIKno)->Z`YF9L;4<439nT&PtcRo@+mZ-nouu zUCB^Rya^h-AE?s)Tc(_xNr~jCN&@gJ)zUqx7W_+(NcDhH)o0C zQTSdi&$Pnu{`Z0X5f{Hm!$dy!mXA^{rNhT;0k4(z#mG1`tcd-iBhJ*}xJ-7tgaK|Y zu2Rhk|9x8T{M_82(H#W{wZKxcEMO3Qi-onev@3N_B3kMgxvVmQL0-ki_XPlo4rfWR zzCz*95umUaP0``JDnRd&Oq0)-&kqj|&*8%q)~J#TH{O!2<~rB_4#oJe+KW$t_Wtke z)2M(qC=`>lnMu^4PRK4FKLP&4e#a2NuPmez-)d>Qbbg~7Ki^x$PHM-D*EubS-5DeC z;s+y?c(t^u-Io43Vd1<2Vkb$Pfz4jmbn(*HquM3V_3m(#tl?B{uf&*qnR~YtAApP9 z0iO-I++%=YV=F78Kp=@Kquv&A)=Rc9KM$N*i$^(eu+-Gn;%5EH?tK@QMMl==ErLpQ zflK*~+V{h1RltO;s_M>#=T#|3w~~;$iRIy5y@B+D6(FeO=oO7xKPxL?k6CGF-*<7a zew+1I|K#>!hgr7{lYn54W(A>JJ2Nv=G_{|Ulw_H)GB#!pjQO~$X(VcAFshTtfQ=_^ zNxMwVdFmC7i%9j*k{3;;=hLUid6)RzP0t0qSsH?7idb2|y*<$}|4G0eSEtl!K>N2kc;`@5(J;A{bV#_4fg zQ7Ydc$6Z3H(Dk4qdIA%ctscAf&`M)Ki(@CW3nA#WB;d_PWc->ZjS^Y!GY@~#@Oh>` z#Iw1jzMhwwI;aER+tSc*wWzGc6Bz08xG#QQq`Sb!z$+gp77OiGc_J;kihR6+tHXtXng6THo90? z1JJk<_>FhVweVR}$Y)I}E4s&yH1+WYNk3e}EimXZSjq`}x*&Okdu8p>9W|5k-qn^(ez~mI=M4xb%Y9=vZF`+ezvm)GD zZ9H40&3E4uFa@#_*rgeSYj;@wN70oDUzupdEOd8_@NzKLB z+$<3n)spf3Au5q9ff!A?m1OGh9J-nJVu7XW-Ah^l1B;T~x|Pt`%nLOW>Gn9^`>LmY zTHAYNj6MOt+RK;-1Vg47*8nD=hY{_n%6;X_E+e!P@t2@cD)2jM*2hHt4L9;Y%+BrvIO(RmRR! z;ItCfig?U%KqJV^a|Jxa@aST@Lfv&uT1!jl=!NP|ey)GJQ{>}R_#;00L-nWkR%1Q? zGD~dTrzY~8QqGzM2rQ%? zGj0vA7JxKZkUNvmuF)Z=r$1e?`#44LJzn^g?ql}5T|rJdz&qoU#K4=?sZK0NpCNl|BpW7&~n}nMJK0^pE5rPKxN6PTZur0?w(&*^8b1+jyJ7PO}ej;0kXSUVhtFk55nWgMHHx$*CM~Vm%*Z2X6&RYngKMlKB#PKOR0HD170kQtk=xZZ0D#qm+=yW%%nV=X-wDdr%(U|@6xB#zLHQvht39(&!|Lh-74eAi~pjvb00|t=j>Jm?V1vLO`kItoqn&OfP z#?q{0zM&tSjW4+23_?Z6B4s>JSyNC~v+C3I7??QA*J8lMq3f`OF9q(K zb9hvOV9pQ09I8k{7p>63!__EmNB4<{A-z}K^2y&Owm=DS9v&yj?90QA9*xG0!%^Q$ zFZbj;3{>|%>3*jjU3Oz}qx^9XG^E1FNmTc0Qq$ocO?Z`C2V&2oFfGT`6LFfeV#pLj z3!=~gG#m%5)xI0xv{}*HJ0+HGPR(QrJvtaeMq(ZRWeFD94N|aHFkr2R)IhT!e0*^0 z-ud5{Ts=mmVOlKaYtKNm0O*F`lQPDDDPzFEf>S1H&o45Usu1E8hi8Y)%=9KQACgcE zVHA>Z>$fLcVcETQF%xm)?&}ribIqvmxwCI^&E* z(;<3GOH0*|Rh9n!{tM&Tl}YC7?@?@#E$i9sPPBVW6ab^r%7R1wcNA-xKCG=7H&I>h zX!kuyXn5!@(#_XbHd9mbX~PZEa|aJ=BsK8$6K6@+m4i5$WI15A;Jc^4gK_^gsVbSs zZO^xrYQK!DdlxH{2r4_ww=I0Hca!lybJ|i5rMsUD^B-=3Nq+s?QZ{?nqzx-<*re`f ztn@ge&qphB*O$sA%4W3YdP`b5-zd+ul}|%MV{U3XtX~CE1ew8~3EL%U!v8piVfo6+ zD$RjekVu)!p_ZtRfICoYwI8KQqf-1?%{uqrkDC03V%$+Wsk9 zskeX9%eu*Sh4yY!gsIDfwFmy<1@CU!K~WOwW8~ZHv3GiGitT4@o(vH`c$tMLT?9#A zRY+e2^@x1hY;;sRG3nR9Mj^jccHO@Cpku()$9K>;(Ag0ERwuJ8p+!Zl$$B0Mr;WDT!Y3ehidb-*oC_N0`E!M6@4_{SWQmo} z2X3vp_&twoW2xpY@5MHMh`B1KhVASkn5dX4pk25x2va2DEIybQU!_ID4+Od(0s5^F zKFO-;zHi~&=373BY1MMrg73ZFn<$uvf=jRUw$|n!v_=cN?bHigL*EPaXZtdi&Bkb5 zhQy@9zi<*VVhhOM+CE<8(N?A9JpUr6-Ii>D?$bOi`cK|E&=7&bJOrw(;ZXuFv~irNr$oYQFoRt8r%O36ZnSVf+|LGEjtmDZmP!f$r z28=RebFV|C@lOx)q1$&{Y^*GvFcloK+HfD|_?~qQR1TTSunp&Z=_Pv|OpS77x+sy| z#)nk>f|ypl*wjzoQ1rUP8!KPL*GGrdn;lhvcZ&Lc_|xRJgkt;k-)xQd zj320F??|gA$L=~19T*BcSvq0ZZtUsl@tfZ-`-ku+e{W~8o-fpb#?U}7C?|=bAG~Np@560F=Jtqu)pJm7tK#d$mm7R9#n&e zEd0(d837u5n?R)pDc`0@W{o7VdIC~87w|w7s~e~cQXH>gnHMpZKd3l@G10F_#?O5ACz#_fPB!|AiZoeS}@I z`Rrp!%maj`&uqzSYio}bWJCr)xO*a)Q4bZ~(sNs1Bb2qa>ZW)#rQ!KrxtC2spr_A= zV90(6#A=d4HGL}uALd#6Q;%qVrN;r{}{m`@Vk81A`Aqu?w*iDh$8gZ!u27?Fi){u=VVEH@|j6 zv?$qgNfh$y=!h6HOi>(yb761QBNtTgkBj@No9c>3#~%nJ4$v;qANS=8BeCl_UXh0= zJ-D`GMFxGRJI{!bPxVPuNmG;TWjkKLt`k>BD(E)LM9(H^;y6_mX_rjFvHN+ zU}tNM2^XT26zZc7rngA{CmEXKb6)1-Wj#RZ|8-{f)~oCp=x+a5c$x*ewY z;&FdVCmv;A=rdKy^1vv=Jg*aF;@Bl#Hyi?-EOGs$SXitxGg~yp|7R5fkp0+i^qF5x z1!|?D-->BHHBuda!vCUD430CLJ&E6@&JEn2LH5IVe%SR2)Q0x?Tr}}@t%cin2}%OF z&!g5VJFV`rO=Z#*syc#KbD$wcx9dX73P8nySe-apH<=n@4x$v5zV8$yMt*bq$5}4`h=Mi$;Um-t3mtgZv0wrtzRke7{wF8#Ox9?P; z+W%SxbPPX!q{{Utpelzg!yep#bH}WRlvLC|o(37x-!-lRQOFsovBpFIkN2ap=jiz7 zB;;Yq5}_7XqMC!K@Nh;Spe(}8rRl+L|7fvZwNg^#W*D0jLwxc`C z%u$+eph9cE|7z27yv`Nu9m3~=F-4|mrh>0tC3|JHcFP*6G95qlV}b$lC#gM17|bPi zmH}IrE*amp{F53fF*u=H=xK1i*nW6@))ZH7gqEda`;4PPGz8rT;68wsL3Vq0cCYR6 z3nbDfXsKJE+OXi|>Dd6d4@6M$n9yDkGP(;Vx}tSndIBsV`^*ta7BJ5@^FiOO%rhq+ zVP~G&+C0C-%V_Gc2^Cudv|pKxCJz_HcURjSNRvq+g$0^*4A6MPYrmmZ56kPO4h zB|g%S&NYokis<_K+Re>P2MDr+nwKsBGhG46eAw(F&V9RK$*P)yr3e5s#!`Q-f)@on znubmKy&8UKu#2IPhg7WF7r4r~Vh=N;*WHS`0<-qpP_@q1qtc|6rnx@cm8V?02Pgei zBhlF4N0mXR{RJrH=r-f1NH-Vzi(Sc0jesIpQo=|@Wukk&va%A5`Re6M4?wsTJOX;4 z+*~AbG0d*IR8WpZ+`$I z#=VTteex?Kf4taq7$>U@I}(*e506hMw|Ej&T~l+na}7Eg6AB0B-y?n1>IM=nDw$iO zrNsRndj7Z~itZz!8_- z)Fl-mDCl?A>gQrRLrZOSncmGT=NK?uB_$?kabfUn%NhwCZ3h>Bs8fim zeQ)4=uIkDlf;-%9U~cUePdYEvZN~o^S%u1DYWW!)J&+}^*NiOHnV=)|4GrM9h9^r+Q$he1_0HrLOk<@iAYkJQ&C8FBO=gQ_{MCnu)PtNm&UB^*<%C&GBXvA2{- zw-}&d%^@y|51uzK`S(VgQKXKAv?f%7Qk>}uk9t0+67udz!AaC^Q=J%?3sK#|#4cMW zpTLKf;xbwmmGUfYh(2tOqN2}d(0ZFpod%SL^3{dgOs7$%Y>x=xIr?bkHiCWEG9ez( zJG&W->w-F6ToYJfon)*aL)W1=h}@^}Zjo3@YgnDG2?wDAMM{#L=sz9q>bXCy-Y^ns z7^Senox0D^7S@DgWykC1$FpqqW+A)#t^AJuBpWKnyvSZ6XTmc3?M5ZKc-RYmSPl4sp}(T#|#wiYc-iY_m!h znK#fwM5MuRDRk?~4vFjUGOBoLUIhLhYQ5d9ZR>i0|3q*qZeXBULXsLmRcnjz4p?;Z zZ%X)AhZ|d<*9nwz{dHlYa7?T1B-;{`@2#FO`JFnHf|CqSm2vn(V8%=c39z1(GZIC2f@$Rzjbiv%LlSSIp?37J(u!vDHr_y86l1pU8hWjz zE%5XC8i#(w5!<79Ok*DlDVmyJTqIWPhxqsiQXvd`ESAOjtHhaUIkOKt1V21|O2$9J z?B@@8?0bmYa?42RU($l=R zA$gvZ2{;y9FE0~k_FHZ1)-0Rsv`he~CuPDyD0N@va>?a#$rQEP!;E&32AzTEHvgR# zf+zpZ0jlpmQZA$C zdAt8nD66&c%T3OC#6tAQRE_4a&oeRmv88Za?_W1sS5qTTcE$h!M%0+jrt$mCSnIpA zRF{)jel)g!rW9G9Wp6+`d+Bd$*+$aF!obNH|4*0_yv0NLO-tvUU;6fBHddwbz0R$r z($hi>lD>uJ;i1XjY(GDdC*g1QfItwD{~6LuROq173Xn$cV|vpuFnrmM%Uky~@`Yt* z$>yApeqg9i!fOI>3A~P$!!;J66R*)?$#KOG95&pZcXpS~i3sWJ{4f|v9t}KY^a7&UkbH7Q4$M^|LTx^oXnSrBs4vW4w zGezf(=2{3bDCeL}%&; zH^)>}W48xLvN4d><0o>D()SI@c9{*!Z-XoP$HcDEzBig5r5U0{?qX z6gzzY; zW|J$LX3-tW{Hh2n*~`ovOy7uNiKz5tz0!l2CCebRoST?S$33LAXwUca{=h>fU0jo zi@c;dgt*Sk0K^0r#E>vkmm%8Dy)o_;Wgwj|;GsOR)OANbFgTd1$3&>gA91pvzBCeq&YmpGfJ<**a|dV+ z>P4JCYwjlZoXe-kl30P&+y1J!3&+0FDk%;4exhO~cEEw>5i3TB_ABnSB+lNe>N??v zye}zhzV}D93Ib$Si_`Mq`(WwFp7dq%CX#$mwUEfk%?}BVXQ@)G`r)VIiBR)NP zqScH^r%o~Vqe7Uc@R7)CXqB|c=3M{X#FGrkl?mJkyNxy`v%lm| zTV`ay^%OcfHso^R1>=xkPOOX&HTL`Vem8sqUqDV=d5h%@1WD6G5%&*dDSW~C7~Yr7 zON-n$E78=;Bh?gv-`fpW?Rmb&!GNiP04@PcK7etL{1%yA0>N}QAG5T8)BU>6f_l_9 z60`ZYSPkVIk?KpyLUfyp16Zbjo4urGi>qkXUAc(BQ8%1QDn&N0J_2<}rwSh$l1~I0 zdlGZ&sWdftJ1}#UZ4=b35Q-_1kTb_0gNga_qIU@le^B<)vMI{7bMN*=YnM~N zvHeD-GPJ_FS!l@`Qm$2G)PW^3!*?H{?zPxazH47PZguT#sEV?qabP6M(^)&WvS}lb zYg3DXE|n-Q7UXD4(0Q*m^Y2MmBfiTjm=xOGd{JSB@P7jO8g|^@y45#W-zzpy7*{{> zJ8UUD9t99*Yu!gHe$0el+YUC1GlCGpTsTLwc^rof1)djB=m&QI^R#oaq|Ks zIm~BS9B0}68%LztaMajZs9ia6JvcAADiaan+m961!h4-_t zHLXk?WV~AyLAgTbxqFd5`iqIkzCjbMXLLr4ITWpaiRUDG1ERiQMnGK6o(+X^ga`|&d$z9yGIfPFp5f2Ap3@g+uvNpTds^SHzdS1M*l8Tox!Me zs1NU+r~`RAzVv<>4DencP&B(-IzStn68cnYAZT^!Sp=2(Fg6y?&+b;Nv|mrRR8-$f za-UX0U&9`eN7n$*b9bBanhi-s-hE@|`JmMMX3{rIC$156X zXQeZLJb;FQG2T_Kv~`;JkHv`9C!%_=X!A;u+GR&U9=GWaB&1!ly8uMhAnda~pjP3@eFp+XY~0%M4IZlm)xI0^E5OD=9Ztirjo_b(c9|)W zpHajNmwJxuG4KQ(jEvuZ9iTY`(Go=E&H#GuJ}(|Ws1e{J!(WPn1ePs$9Tr#Jq_2lL z@i|tbxUIhAj~~c?`zLpj=_dvZ<5%${hX)+PfD-s$JN$QF7G!zMExV;zH|J!Z$i7yR zxs5$G5ToZ}yvysx6jFS?rGro)Bx$ zputCG%d}ncA4Dkqh~DulwoHq|hG|~iA#h+oF3l@Qi)&RD#ZUKe6>Jo zIDSv%bMoaRce#rd^O)wyVxtk@w7SAZj`y4(FO>k0o{S5He`&MS2_r zpaUy;2iO=xB}zNLV&XE+#T#6j)!;OEVH$3K9KiscS|@Q_Kie~*;rWyk6Op$&;tyjo z$WRI-)hL8gpLy=fpl=>68?9PNvprNl#Z>#}^ZGmjto94%b^KJ#!-aH6{#jPz?RmAo z_-FV1B^W};h4;kH^Cv$U=UKz*^#LHYLvW}k_pILxA@$i%IjE?wf;+) zkngG&1b!~X00NCo#0L=QpbZQLvC-Kw6-vvv6kQZ9_v0F)kQX>tC^hSQ9C9SrzuA5k z)&6M7{A`iq|6Qv z757~ryvk;3kQ(nYK|c_moH#)fvxS{^+D(Iv5*S1Q;N?e}e-7}JpOUy)jl zJW>wLa>oB~Bo+>dLd_nMuI6~6DAQ_cc@5teJ~~cH0Qc*&C~`NM#f~umi=AUHlL|N+YzQUy^=1Ej)epJ3^NG+fOHUura{)~#h94Ovz5Zg1%7=mZ_u^#g7o?+@aSeqV zR^H~H`=20fO~F5Rw2RO|;3o zbta;(&JsOIL|86?To7QE%mo^=Px2@I2UA$- zkd%Afr=2$OR417z1-B;vo-zam#B2h$0;Fl7U<}m&=LbMG)6MJ(KD*cDvIZQlc-LL- z8SixNUU>C|(yKKnvV?TMHsVbywYME3t>^K4BEbF>G^B7T3&*moJ#+8sLU3&G%zUPy z5dd7OAU}f2BpWg1OM7O4G&QW)MV~*v@?UZ8Nxux@#yfD5p<_pz;e307b2$4&iG=xw z!iH8?sUC1Y&pc=UOTcafjIcS*tkyp24#&e1N#(YKmU2iWLRIqRfpPY?3Igo5J0&+- zMZ;=&#0f(`|dh&s5q=XaTwqi;=pm{Wa<;o;J zh7!ic#{MrOmTqam=s9=P1cN^5$wgjA|P^A16bI-WamrzY1C(%Mw*Z zW(#>*KEIOLQum7!irM}VGAy0Phl_YR3IB?qrK20@?+0f1z1XF-2z3ZM6;JtQGqovM#Ds5A`c^_CpKreBO+_5kpDmaafO$B<&v@ z^jiIj4EVGRVC#65gk+{sq=F4jJMuuFLnW891TZgJmKnh@dL(mYjF8rD>RL8Z?OL>& zJ+BN}TF(I2Lr4i3>sW`P0HqXd*sI%*Gx3;}tNr)l7X>`NdU_d9hDvJ-3i~t}1X1}P6)aO#E@YxyGuD5wQ_;QrpxYU!G`Q+v~vQE(u%N7|ZCBPH` zy7?gchccbU%TaPX#rQl@V5{;xEto+Hr{K2krHwuPe*E+D$zA94 z^SQ8~%#6kn6BbgP)>)rz;{)6k*e%dJAa1&IN$=kUvyw?^R%v9g^x^^8*A~&qO|l*A zPi}we++$U~QE8Sx%F9Dfbno^nV#}3r!epSoUunM`@ZlAwlBY7GT^Y>X9p~4&s-fup z!OSbDd@z$Yr7z*{^0bZspN5e!av#&|5i6Qk-?7ON?b$Z~BQ8WIsaX#a#OeT*HN0mg z%DM#lkt-aFV1T#EdTIUJ>gJ}&^|9M^ulN!f9_MYD1>2myfcAnJfj)2HU5ja75WGBI z{uc6RUt`V8W0Uwmu3R=DJdv2=Uj+X zY?nh?m7=xT+&-!$9?gW!Ei75pul z+Irciebf7Qe1so}hcS9kh2cuo(t$O$OY`1gvoyY< zF9WoSvgPu6gZ0N0l6|9?I}lAcF!P#EMU3ejLO+_*h5*B>J)5J)Ee~3!=NdWKOzv7` z>g5bvIo+4(uHGc>Y*X{Vdu3cOg2Ob^diey>kN9tJ-<2|42_XtYDbU zAy3T=aCi=1jT^zwr~Cm$*{Or#dhn2vQ$w~B1F6RhjzZ3{DH1TUKyU4{!lv({65=L< z5`qK(Rl{Y!O)}t5fhITpHvu`itm(}^0U7C-p^bzdV8*6*TbsX}R|VRaD{|oubWe13 zCL{ce`%DhV?uGEq3F|O=D{g|aYOlNyHj_&RdS7M;8<&`Y$6KEl=lcuL5ZRbG0Lnpz zo-Dv-$p1rujG0=5;DtnMa5(>uaI#ea#MOt|#T2BKzdwLi14 z)LHD!;$cVv4GQkiMEvcy|MUZ>BCk^dG?!I1V8MN?7Qj@v$vxnT&BtimTj#%R25tTj zdznZ2urHV0m1&CCwSn>uyOTV?-*=49=y`n@4f;=@I?zabYY`QN{P zafpe%bxcnkxmVH~Z;!Pa7z_aS?Eg&18or$@rgV{XQocbuW{#GvJ3iC(6~Qz{JDxVm zVGaO_2IG**b3g2alS{&Kcfb|3m-OdF!*w{emx~-e5sNT&`P1aqkZ#TgXxB#)$ z194bAq8~U`Pc+<1*bucP;D;o<*8+^Zc@e;R<#B-osB$QeCUNA>^1pvoAEO>G`Vqy2 zjLI2ubVBLYzCz^Yv6EkZ5xk+YN$7vNOGXbY{~{M@N_^g2T5M+?9-BoIcvPdFaDsXM~*&rn#zeJTHt%le( znhz15OtIF^LSebFd~SfZSA^-W#UXrS=jwiP2eBMoTiy1V^q;lXYyV;!^%1W6hJ)Qf zc`xsFw5r+4vf20uYa^Kb1aK|c9lVnXc>RF8|q>(fr2H|ds?BwmBe-mWdflYb=e0e|{$cj3Q=hw#yj z*Gz$=&64F-hlai*J#}f3wHj*--*|n4!HL5SB?>?VyhX-Vd^Kz18t9y>U^j4|EiJk3 zFXjPl38*K^zgPtAIXC1;M$7w-jHL6MQv%x&w5rb1A7xW_xL4AZ@ugms;Z?U(xV(;@ zwzF^$I4hVJ@pZd9XD529x}JO1dhhUl%FA57+5LdC|8@&+(CVuBq4Uj6qS%XkpefCC zhJM$bJ#YgTBt09_O6UUCS1* zl)?L|8N{+?o+YPd2twDr{m?n|bSjgk&cO8wofvK&H$D$+u0GSMd9@Yp6IuNxi}pZv zl)THu9(SjcRUpgZX!w1wJ);RSmluy}4zuJiVoTO!P+$FW1q9syaUU>1(tj?t@%??O*VWF; zGPuB6Wvj+=X4Ct}6oA_QQ_p&Ld;QGaUo%jLFM>+sE3e^GRA7XgUqZH6Y$OHk30_~q znJCXk798KUIG#lT6#y}CM# z$3V1qn$~Yx>4{8CQ+-tNT!TS#_2ITmjl)|%A855g);|JnX}WHoWq#NaL)shR`(@x} z_f23eg5h`7O-7Pt<75)9ca>8XRi94lXj#}F^)=9qthV+%gD+uRd*3IVLSKvHeG=!| zE=DvtU0GWA&S60?aM(|n#)y#CZuU-(yETldsg$)_;RQP74pIO%J+6su-ag@o02<%@ zs;f8cA7v;-5m5eu#)r$TC~P@v0ArJz9Fvq^q6=KL*Tme9+8Q1mZO-y^ z*8};Vdm+U+5O6Q-msneYSV+FZM&F;ONaV0D#E4rakhS3{L%C{$EZ}x>u~gzjxBdp>cbS^@CAY5Dpemmu8*T>gra2rtPBLF zK;RL*slaS~M>72iHh}-+BZ?19R3^lx?cya< z;3aNjCvRXy&kXeX5hgAjTsb|t`t^+bF-CUSpXWQo{x3%6y?+5p0$q+)O5NM44_ME~ zXx!JTA4{T)@)$9B5K9U+q_7i)z9tV8`=4F@E)LIxksrYWSeTGWRc-IRP}Ca&6s2lR zzyCq2lr6jnDzSd9GlK%Zx$XW#=yUz)>qB?b>Lt}$xTRa8feD1!$;g7tD0}Qdy%`@aNOAGIO*hJc(E~a0(4tgv`6S8)*D-uPA&fO z1#;p%sX~-M9=K_pUADM;wk%|Cb7()@8cBbnqb?3v8e3kL3wA8W_rFhs$U&EYi+MrA z{{h88^Kk+iN(dfocRGl;Br2w51=67e-y5qhU((`B;7t1^2nX5wW?Sbw{S^1{nrc>r zlG4ifWHjdfdy=J|;}u`$QrO(R;Eq5GytH>_B?vQ?1lDsudG{g>Fc-R=XFez{)|*`f zsauCcZX#9UNd`7O6j6;P-%o1L%gR`n>122C0K9E}MCCu*EAoi6`~TBg5CGtLVWP+V zCmpIxy7G0!bH@cNEEe8m<*P$C#rY|g^7T)))k)wUdAE@M)^;mIr+H$_bd8tx@mukx zy9>1*-_fD|0t4 zW=&yZz_+Eu!ap=i%me&hLkXlebN}fD_@DKx7eK@to8j?N$@w;_xs!=s+H(ITK8ms~ zX-%Er&#QQ571y{}P$}S{c0j*K&0>PJOoLE8!%J3@-)WY!L~K|#N#gsKecAE&K zv23?b>?a`=8RHI_T>=)w(|3>OET|3kk&9UA7yaM$r&pac<$UKz|zpM^gz5PJA5XV)5l7tz1|Z6$ws3l2lT_XJ==ZmzUe>m%o3<%u>YXnc-() zDfhhE2mU1^gY>!CTPg$y#P=02_4-MD-$6hAG&*BTt4gN+Ex&mdaX42AxPs%T5PN%j z8PKRG^mO?UOzm3R^lLdfy?GM!-mSqzy8|bo(gs+2E?mF&)E*r772Bv|#p9AGT|R%x zs(JMPWA81a;^?|{Q6gA?U?D&Vkl+&BJxCyUaQ6T~g1a?Ia1HJd+}%C6L(s%j=S%Vd(O|Z#(>dU)wOEPxt{qTeKX-Kp znbe;R$?`lMhR`Cm^sitU<|~NSva>}+@Q>T(cz^7RtE;h|i)gAnAS^6wAtok9TwFZg z8fc7C?<~Bu(A?ZyuGhT%kk06-p#i=)0N>LBxr{tXa%jU00Yg^)1jmk;DB_u)Uh_Kz z?Y}oe9frByP7)^6^pa8V?v`tPs&dl??q-{Ys%mUZj3dwqyw(b=Op1|+Gxw>+#H7tA zzi3_F{rp^#Mq@Oa<>vVwjq5So0DJfZi-gG-^xK!4;YPj6ByW5?d=)vu%_exkc zF1Mp9}LvKkn$72Z-?7hU@Cy3}cr>oA2Pl9M~ zU$i2v)HsZ{mz-YIgaH};%}bCC1DUrm15?w}AApoglj^y14n6^%Ci3Odw>j8xzOAJP_TVbwD+P)dr_dizPpMgUF2{r_w zonCg|HaNMsfO74Qjf;hfX@r<4PXfDwgMxkmlwhTH>Bz{4Smw|D_g>w5PBUO|Ur&z` zDcUc_j{xWc#^do!XfpgXvUpkG%$+w-cSWDrTaJ8a!w0tbM6dOUI5SvaMOWxfaj72{ zTpQ7r0C#CSPr9YOVZvS>$=GTDjxh}EuMbB;`bNRTG;(&933b|F2N8;eMP-zFKiMhQ zfoKV8bH1hU3&*+9=P^^~`|(Xq=-%-qG!2M*f1YaeAK9RZH!ZO5V>|c~NYfYp88Gy) z^Zd#Hc(96x#>j3%LLTrwvucb+b;-b1V0++pIcQQ4B*T6?HM@KU%`jls9S5t3S9|Zh5gFsK7x^eZ|g6*=R!kQ z?x95qL4kLqe?`9Rny`N@fR&Ol7Ck0_kOgep`jGc-yf0d=DW6>cTq`-Nx-x$ zbPqZ>zP6%1@9mR4uO@gqO9RxEHOjvF$n;Mj-RfOw`?A2YS4`Rq{Ax z#F&7l3Bt>QhgFj}H8`g4W-%WFMT4+`?gThdtf%BizXEr3Z()q(^{)E6o7&8RvD~>k z+wG7$;~kNv|S|ql!5}5A?VJ|$LD^SioP;2fk#NlW*7X!+lG3<{DCUYkG+4+bAaE#svMDif{47jJmj^zAs^X!jVeUQ{}><7!xL|P z!&ogOc;E9gXs2yxp~c*G^7khBB%+;6Z>d8af#Y~V0Ehop* z^NIDN#A&eHTJ|2;BZOe?s=U7q(TtG*NyO<$Q1(3TK>vON_Ce#rXcEUr_>G4KIk_OlVxhWmcR954^gGZ>T5!9U z*EYBme&yA#1TW=&4RE`_L#@|t|CDToJU3mui3BSm5LO95KwLl z7*#7-Jk;FWE)zM@#u|@sG1o*oom^bffi4U;LvpFSY=S+&)cVrtkILwrovZCbE7}*zaJ1v*cuuO>Bql*1k~qv zb0?Kp3iw)(~H0c6oEN^SWJ#BJqDL^U;?a>dnS~Nsk@Sz+i3jYFMw#nCPms#^jah&fW>Y; z?*|ss-mJh!zH$Y3eiKBU_HMJ{kF4@?Rb>7x#fl@Shl1idr=q_9SG~I@!liknNrobtrTnBN8^87)u!@2R?BYu{Ft?aDgnw+4R)!K= zkRJ)C0SDc5CG@me9+APQnmBC(js3(pFdvF^8*_bng9rK9C&P_II9Z~>yGb@N{ zQoA@|XrNW`P)1VKd86JmgH*sS1lmZ<5)4~jI&ATTUVl4Hrgd(=&b}2E@wmjmMlAV{ zqHnA#(?qUZ*EAvYljL5WW=X~5?RUA;mPZzX z4x9ai2412Pk-|5-zfbDxC*gbGuR8GNJ!Opq88+z&_sn&SHV~U69^=#RE9uaJ*%dj_ ztVdjR=#RkCE7zeb5pF_Xqy*;2g6Wx&%-Pb1dmNJ2U-LeFmQ+%|Aj*M1A4c)LL44wAknf1PPEnJQc!0vwNKoAI~kb zkM0ZvBkm2cw1>Gpdb+byeE=tpM;RB|5dGryaxSs3ACpPOxZ`d)d0 zYb-2C#$^8^DlSm04j7?;8tv}weV&Sqy=wOM4)9hkkRu^=Qh8SAJH{#mrDP_$?MUQJ zuZX@_n2cJP_J$qaH`7Xk_;(;ccKZS`n!@K^d-H{Pzo9_%JOyK6y_0_bh~Ius@5}&| zyMV{&0vI7cPnQ%VL#LZDQ;|e|=~n{3RN6)EL5^kjwu`RQ<-(^)us(9ayWOM0kR}kbE>qvIS>OBRxsI-yN-{KZ)j)` zfHa67igk8FuQCK&ZT6=MjS2B+fa(~p`+)MF!Fp&C4F9Z0T$f!SHCJSIN658N76Aa^ z`u7Pm8U0?DrbFW|K~_3a2)&gIvbF-3R8Af#N&xf?n@0Kv zyd|J=o7SaEIfz*Fzin9$Cale9hna9NaegX$=FT>-Fm4pvZw`w&I##C#+BGz;2&{ra zgO%uM0y3vWKeKCHXQ)a@OG_Ik;HdxS3JttCPHOmXJCf1n-!+;wP5wMpAF(%HcPSe^ zs|lkryPT3nT=?&#{vl|14KcZ4-sJi9ggan+S)fiCC_x&=U1>~+^R;FAgXGsDAZ}O1 z(gDod@cR?SQT6Ar(8lkaecM>Bg-A-6QooSiylAsIL70J~V$4~tnO*9x1)h6P)*V4< z$MknGb;nCr7GNUG%rx{yuW{w0dqr5|?&Tp*jAH@d{s&_XBtc5YP87m-Vd+_0y)I9R z(6@mkd#vsE%)5zZG2a#B_0C-xINOiL9w(6Cq3HoEAboO5N^n78p+({l0_3Ua!92D} zm}BT_cTADsc}t!+*k+=Na9A6j5WMJviL@+fOZ5X9yxw+&8Gu6^K$R(;6)zP4JL~}D z;2y`AASLkf+08-uScZrJ@G@YNkE97`Jbd7wyqzX%W=6ddBbbcnQsn#7z5r$LYuPRV z8UoxAu-Wy}AXiTuBjYKGyu>=87oI#JGc>CA{gEN@Yw^ZZ#gzcC<6 zj9Cu|awb@toSWmztE+Erz6Ca%7tPOt4ABsjw5tw~Z8h18qBazmnBI1XvANI|Q;V@^ z2NXZZG_gX1$YU4C68EK>#NG9|X%)(cU5<`IA|Mc=5OjUEB#)mci}0k&WdmVhjzg|a zE@u~)h{9tQw9dq2r1I>CAr8zzKEU$Y zrmv(z__xKI_)&KN0G|>=BIRpQd=p@b>axorM*+KQ6J2XxT2bg$YpeL(QXPR$)brXK zoCHTYO}S~7GCz4en$y$2*5!K8E{UAp5>=u53DpUdLKh6 zBM>cW6Dg6YpJ=P6QP7;Uv0v`v_G>MtJf)zz`gUQc`bG&@r#U_xFYrNaZ(4U&@bRc- zGDe$@=HIFUQfp$;_vdcLgz0r}PfvIuxPFJh=pVxjv{}KfPJt$lb@c#IO$^_K_ ztZ`BNYOMUm)}j`B2sVcyvr?`jq~Y2{+qe-SuniyvhYwFA_`>c`i5Tw`hA6VVwT+}r zla#;Il(Ow?D=pFb7p>h-3;+|^RL-JEM=JgzXZ3gCZLl#NQ4UeGd=O|o146b2y7law7&XST!u?cr(t*dc;}Tu)qm1m4sMdic6XzE7Y)fA zz{jg8jHxzTBu)+o?lW2@8>a%zCY7c4%L5nQ#c#_fHu+sW`%d$sLnosKBIaI016MrP zB)T@ravG8lVhf<_Wb)cQIx7mAbFFiyptl0z%Q&AJ7&F+lA#1IR?LX1Je<>-Mjj1=EBxoZ4@l}$)YyRreA+}_)q@) zC?SzS$aJmt!^h;q+H3UzhX{f(Vu4bB#ey7R*Xf0q%|EqTtdB(>Rwxk}b{~3)jI(=5 znna;0p6f8v&zpe08Cu$>8)Yp-y1Le3UdCeIsUm_SJ^2{D*WSquk`(sN=fVMD=gcef zT=^^Pgb6o47W|Gyvdmtpdr82oLL91vI6sscC@Gy5x52-0ml;LM9??=LGnd=_>}e`w zy+XJlOBQ;}e$xKR_TjMh+n>^B+2o^ls6h^omUn0^vYg)IQ_51UFTg}K7Kvqg3jG{o zn0LAw8i>B`$0Bqu??0(NGvc1A?PY;k--KsjLEAvFZ3K%PjT2GP|WC|kSIQr1AuK#m9m4C- z$;tE8c0dWlu}G-sWSEqU0B-BWj7|#o*H;h7`W{%YrF2?faP3 zjbKe;&yl|K5ZRw~q}Pte%AT%$$or62ZW@Z$(8jtMfN{DdL9N5q<)xSh#2*M&1IFJI zLm?1hX8@|GSpBJaRHFm7g_fj^C_2yQr~z z?R{Lebf%}ZCdV1&1A5a0j$4m5Ku&?XT#qy!qGvst0jYV%l>nXrLm50FFg(SX{ky`c>9s`;T6(Fd4DPH$JtYu#| z@bNHQGz9Cr{*O4N$IB=W->5`d6RDcB zt^?X{2Ha@baFDiMuZJ$weI+S07R$`b+wLJWc+ng};s&(ethb&6Om?sefNs`9SzKIP zLiGACTFxhA+i#bk-DT?@!uRKsz;SN8gex%`$GF z?U?)o2uyx8ak1jl{zXS;AxITZxi5kgzuAw3Xe+a+Ps6~Q+6BGdxW*X{U1>Ks#LIf* zLizC3_rUTN%4T};$ge+r!~DeN!NcfF2bdM&;xE6mp+Vh&=SV^7z(qclctccuy#4nB z1%AgY&(}0_dwxmBN#_m}CdtaC>Q`dy|9B2U@_LHYi_+$^;fSI7%pzJYbCeCV`m&pu zuKTG?AhoN0ksL!D1N+N&jD^7c$YNb%Ovvl~$RR6vyC_`GwCdavmr@ujom({(7*3sW zW=#0PSpNA?Oj6;1a-nKGKMS}@h+m}hKY5~WjPtGk50J)1Ms877+uHdkMXOwiN;+3M z1k?N5(R(8j42U`%? zstw3g`_M0}veNf3zni+&Gdpv^7I&z`K%9IXy9B3!xRhFN z_~mryX*MUO=Mi*XNGsQwM9zVN;%mT4u2=5jxb>~!trI9*e!5%_S1f{iG^EV4n-Dh9?asm>~ z{a^*>vk*p0M5HgmlgvHFVk?67H5^9k8e4`Bo1@%Hp`X`A`^0@@DVSZ6E zK~&=e$khKqscY1s>W(e}4VCn#!EX!-hIEw;9Au)at5{TRhx_0MPFjOlV+8Cpf$&GF>vH3Q&=8i#m_GGaHt|&j%JsxyT*3i^n zFo$@<9^n|n?)siZq*#^V@n|ni7GD+tfpVBGpP{3FlXk+-tg%#PZ6TvKuj%N$BH1(u ziyptMv_j{*ddri0@THm0T+dX`vDw%~0Y1Ne2OD>%3k~{G#%8Oo1@lNbA&m(kXgH$2 zq?FlMNHR{~s?vo0G%vp9bLF7V$G(1_Ul1EC=sGMLbuPv)#LK#LeS|mLpPo6-k5<>lVNc|my%7ErC!s+UftIQmr zpXSS&O7GSfz{w+_VZ+M|lTMY47`T#OmyE8M*-fbkS@A@F7Myac5{Phe-+c3IYn%N> zGQ=9Zd3H-7c!#a%=ngNEnX6WVarBf?v41$J_i?(pI~y|}ZMiesabJdBVO#HoWfXj( z>!v$O9_93g2S8G=tSV$Ta$g@w!2K+#r3Iw4UU4{&R!U z{PycxNCkmQwU@BLs@D*UzRz23fYH@13%0Vh7IHtI6@imA-373LemB%vv>9Fw ziw*W&T5vCL#Y++`12R5`(|(cUc0nR|K3N`jyFuq5p~#t+*NRQc2pGBd8LqJ6+V+yx zF9XYjQm0rU(hQyA1g+`RbGMfJ(IzkC$bZZ1rJv0pk>z#jkAF~ujhY3&DP zbbH&bg^HR(TkA}LnfukK@mYwjV@NeGNo&Wr9IvvCwzWf1NZNt3Tdpk@a2usHwJ2(D zQC*nPgxm7R)=&UbqU~nCxOf4TE1jRcNq@$W)$R0fPetl^kX}C*@()pp8_y$V`*b1h zvK?{|6wsaC)?qb8iB~tIj>AMZSp$*;k7Obr5J`iF!MTaourD~)W zagBz6yPWbed6T~)+j@lQFghv0zh9apO2kVmS=_9QS(|NJ7dbT}D}UI+I9jr)CF_4D zH1AmPL$%u4>7&J6Et5zytIW>fYu~s^JkeG6thQYo952BE&7=ZT|I~_dD%Z-Md%MCj zZxyGu(k^4C-0n)zOd0cQ8px?T-=q2z%4xhUtd-tx?`w3sfog#7CH5Ey`1m5ec-TG} zvnP`*O!XO&l6Yj3LcbH|aU|eeXKr&BLz#6l9j8#P^Jj<|y8z>-7J-SD@){$L#8w^S zWL!-s`Jd`3g-6Cg!Gqz{6r&?ix*7H`8QT(vrtkZF%&PWz(upcEMs#*At@mo2dJ<*+F7!MA3Fzz!SAm8h=P-L3%6! zfOXCj%O-Q(Qw0ug&6;;e1=7OQ$w!_@W4|Caa44Dq&t?k&JtLcEXY2|p$ER-%y3}K$ zl?Z(nXDbxQOBc!pC+KxZ?DclS6WQ@!GV_DF4G0A02 z!G%`I!kXRH1-gR(DYJV1WQexfM_5`swb_+eipi5`#8D?4|55X7YP*8DmSucfRPl>f z*2OY{TDdUnV7-?Bh9P>ZL|8JlpwFI^M?m<6cvMNXLDuMImZ+7~Tz?_i80?hI6z({4 zsdsLZs>-HPob0gF_km8m%ow#L@q20%Ylu@E!-tk=v9C7YOru85zbiE^m&CZgw!$C{ z>wr5=J^!Y`%)JLr4$-GmDq;JQhZZhS8lRSqpRbY|I}~Ma$$D z)>xE42pqS&)HfJU*K>(C>vU$g3p9AKqI{xxS|BVu( zLGNO;R_R*HprqraddMzTT55DQ#2=Sxgtt|6csCL_-@{B-GmkxoL0BC3P8^dlBYO;# z|90L-9t6<82|8^pc{>Uj)x6NR)G7THleO+vR)XvC{1j!4t-`nKZvzhL42p4^*MnZ* zW1>iH(>jy91EE597_?+rLqcz(WbXzf>mh$d?Y_#UI`Us4BoG_JEmzk@dP$ zMje0UP%fQIbQMQ-2hfis`SKoYU`C^2?eW zREq5Xr0#nk`wMC1LJCUr4~;r#8vb4L;bO3uiNxZZGP{7nZX<|fd$X5mgd}~8Om5Y= z+=k9`=ug|=KSVRll{@}3S8abV^s5@nU=e8KYZXV0a}$Xc$fK-~GAsOsBjlt3wh7vy za>QGl>cg2EJn7< z8&uxeO0Ew)3w=P-hN^%Q_9%9I^iJMAadqPTco==cL+1HjgfKaKbY#iQ} zH}RUwxXGNWx5cVdgtFNbI6$hB@_0u=*?I zrB65f`eCh@$l*vfUly5f4U6}Ld)Es=ca!K*nxRF=BHWe1H*e>=e(RN(ZstWUc-q#1 zU*xgv^Smn4E^FQ9qK;AXYN@`e>uYqc&aS*K+7dIIZ9W;4;%dn>+tDErC~u!dBi^>L zF7G!XKDybs$Ko2=8ZMc}TBCDlXs%qt;)*dY+O@j!wvOhoJji5Kb;C+i%S&pS&TxAL zwoHn>OFnM-1y(XBNY5~^Kfm7g$E@!58Hw~wG`;0!QQrekyQligNVd*9m-Yze&S6&D zRp%ovX@t6VWfc8$d)JP7FJ)r9Lf{V4X~d~HloS+DoBYu6z3Q9v6yU$6YpJ6h)Zq8l?B0N`vFfEW z)nK(;o_~>Dtl;62n?Fs;mfrHUS>}F|PzI%GLvs{S^iL;4X*>(w{p>ZUXB=2a<=B!5pnu@@+r}kh2IRN?2 zUbj`YO904sirFt;$J?zrN~>jVQaLVTV&x4rx^&lN;e&(|kpcvXkjRb=&6d;FR{V=N z_Nbi*tWhPwF_ol5cRW+cQggDJ2ILk&wdW-E_&~;Z`RhqDKx)NzXg3oMo zrOh6BvT3gIaVs5Q70c?z@l1-uGmR)=x`iz*Y+`z)vL6SA zQ^~5TP+mc74ymt^+ve072aRn)hq6^Mm=Nc+t@h%+7{KNm ztI=GQv6<&`y!Q6%S`JU(nhU9mI^l{rO_mL-Sc6C!I~^3us697nQU&zEl$lLb>8S?8 zvmd}a+?4X7fvgcdLthmZnK8KzB&Z9_FW1IGtdXo-uH<1j+l8zsnsCPFZhh zZ^PR=aYYQV5j3uDIT+BhWzo%18&N$-r>`p?Z{ELc(uW(x*gAy>NHBD?_9P~0WNL1ec!&$|GZ)x#sdE_=>%`W36lZH5Fl*tL$pS9M zFbn;i^6*!=ax^iymf_T%k{$k_N4>0m2qhM!Lbg=24hY658W<{z#(V6C1vC3g&ym$B$JeGytxvNb)| z)L}lvk<=8J)RabWKQ~_69AVC(Iw@|jZs%&k;$xeplQlDp?rro0)0Ky2h+vc0?dNaL zunp>Gu-L!%kM6a7ysfUvGabQ(YB&0mSO45D(R9VoefLNVZCppE@iS2+rXfh6UY%LH8P!R=x)@ zbkNX3CfLel%)nlZK*RpZixQZ`$v!lckS?gdmF+6Ooj-?jKeiH+6ka-4?0yYTIEA7i3$TNE#QC9qoKEw{_Ir0gWV{2`*n5Z`wyzKo`?}dH!eD zv#hQgY0uWGR}e1wI(dT)=BbD&P`j=E1xSaiK3)hvAe2VmmS6T zve3n-j`>2*$ds#sF$MHe>g#$Z!TMKCA+s$ZqszJb%LCxCsA^2M66MwcyXqmrHoOIr zqw+m9hXCo=;+HSyv1_m5Y``30E?-R$-^l{GY}=wuZ|ZV6yttm*@}T_aFr2P_#{r(_ zosto=N^GvoEpuwrPwQmI3rH*DZn3eo#xMv6&xeZ!2FKB1xL$o7nDJVA$JfPboy=|P zEl;!(Dh}(WYA;IBXls!yad;Kb!2iUA&K4V`d@-CNe#wl7N?&aQfE8&ShR1Fv;txQ8 zg$UC{Jt=_KIbfg|Q!2b`DZIn5c|pHHvoy66r)zL$_l$=t=q}(X5|M0Cx@mAwT8iyX zH#4%KbtpQ@k1ipF2W^hTT4zVygehxWxMQNSOUp?xm=T-gcPvO~05r0hO*<&i_r^b~ zt(i$^F|0ZKjB&-SiJDHj=enStBwHuICsI7MyJ}wVcdlYLEiX(Lr&YK-84U*MXM2cvRQX)zgwX3+)0G^iY|Udf)}lbWXJN_0c%Iha^n zJc5H-rd`af0vOWhsK=Vb_B2hL4NpaUkHsF6asVF<8~9bYwFNaOht4t8sY5a%sGzp( z^W66bi-Ahkdsb6z)62;!p0 zHC7ErQ;KaVYYo>tHh)hCQfBb(obhD+HK$a=`2g?bqjN`xxNIG-^!Ik`qG+U5RFB?y zD90qfi=)YBb}MXw{(NfCe$t4{;II0MQeI)iYhwM&D`W9%z1+(@xk(I53W^TXBwDpi zwM*x2&##L8pRoxWQOTEzyt5vOe&K%4#)a zM+t1cn{iP8k-5o=iz`jEejrn?QCmyi#&Q-=u$K6yKzR ze7?k-=w6`DJ21rgN#G>LMf``x9$mAOGRaZBdYE*kgO);vLYw)PoNsk$B^zUEpU!+^ zsmEBn*BN(uke_9!y=j_*sp$`(`dcw?pY4!$o*zjIso2tURFSb?if<_>B)`sS7R*}I zLDqwVR+KbrbWE2Uw4E=)s_DfQC4Q4`(S6{vYxqu~CoTV)@Qtym`$6$5MW9n|MxUBm zO9S+#N9DW`8)G!>NtqHBQs%!*Q&vn&Mw6hRCk1si;lG%Te>-kLh38bI_R+Qi24f7Rk6L zk89Mq^vY7LG+U?l7Qejq%3ShohWz}Mn7xOw-aT9&%Vhqwxa2}V-!x9VEvv-tIhH~5 zid1mbH~dHac)8uT*9U3P-$grx{c2#==xSP|-&p^WtwqkOHWXo{(v$_ld1{7I^|iSJ zhpQ!3>_oXp^oAkurrD^1d|KWoxk)W-wC`_pZMRSt1&fu&_H92|ZqxN2K}f>K%!ctWACJTYaOtbhEj#p&B}B(x1{Q1mqGvdI4O8Plb88vNN#(XH^OkJRdzWF|d$@BL zyj=yhHNHXU_SM$(?^}jHU#Yx|8wG5$`rZ1D(qEpHr>P!Lt zm|-U290s@gBAfD|TCh>N;}0*UYd4wCs0yWh{hFs|ZSWKgtRY&8WVPLb+9c)0OSQ&V zx$o383>RFd`)_>zFym0mlz)+%N={dGf9|R3?#@GUZ(0&|Cv5bdye-m}R=xMRmJ#S4 zb5aK#$#2M6B=$ieAhEts?`1!2nXr`I_9dUf%>MMTfObOv`M4(|W6F!X{q>f>pdh-{ zMx$L*(|0Ec*}_=PyoS4|qXRnkKFgMP4MK5sT>fKBFJC}LZn_`Iek7t7KZ0K}lFe9G zTd%Sc7Lv@Q3?-;;<}U79pnS?>EKJ&Xs(d9J`mkn~3R-MNah_)8;~}Auoc~KB8ra%t zj80bw_Q|RiCAA{x@raXBNh!u|kjX2n=)EIXG}d_E=_2ra2l-yjtbB6))&8w`;lOAr zxp-%dq`x_TVtiqGjBaN~3rrMO8eX<7DoejfzbV9Jm*;H@dlstlb?z=|BpUKgSIbAG z`nX!_X4tnntI0O#+lrRfP;s^M;DPYqTCvGTlK?t?-pI0?{=0l1b|&De`r#n z#kG7=>!4(%4_$(DN%K0a#=*bxi>LSfP8{{nv%}AK5uz878hwzO@dec46j<9$LZ20YirXpb|NZP5pJAIK3h zF-a^zU|DJy5b*LZfi<*Va~00l>Lr)~Z|3{oH@0pyo;|ZDEnX*vg%hIqM*aAQ=VP5Y z9*yHS@h;2uxERr2fYv=-G)2)%THIdDkvd03al-hdcJ~Q&sUzt*8B_0=1nU|n64R4!AMvbz_{&N90Wsq5bBb8%o6U>V zmSsFk-?%XGY}iGa)#dK^`co8FIO(NJ;$@lR?XPfZEj+Wfaz11d-VgdkoN`K_A7wy8H7?R!F2YOVA$H2zd0?*2nJ* z<|Nr-6BRP^rmKA9vTY0s(ms%lXX*z)iS%DLd98QU<@*PiQPX9X$`m84yGfH}Wa=Wm+>FE=p1&_ypto3omb&b`ccia!vIfZ5%P@x`0(!HWY^z!!}pz3Xv6{=Rik>nu4WNlSC%L{+H#wH54Tof6ls;p`!Sj`V4*3ED^gpXc@4=boZQZ8Ceg-XEGO$;t;!=Q&{5M&Kd;7#M@xK^yppHmkYfSj8WpUQ|ioxLv*Ed@JB!TJsnR`tgMW=5f3!z-GW$>p(% z8YDCG!xm*#@(L6b#zDMmZ$q7f)rC|>e+s&)h?_WDEPKT{G%4T8wb{RfmVA9fVd!HGYjpA)U9!#l-m5Od;LpxjT{f zS~WuefMos*z-3y2O}(){b82%nUNuozYCn`zLh=O?R~}JBx&|(F?1qPWH2fRlESpWAG4Fh)S!FheN#PUV za!wXwlQ$Q7gZBNsgwxd0M~2!a+Z#dkd{Bd{KaVz|f_1MbY?iLmv-GM+o>^&3H)b`P~C3%VNeFsAJ z(KI!E3M1FtA7h-%>UdGB8~!t=x=A$#qs^&*_SZajmQ6n2lb-nGn;O}iyjAOHR&(~$ zD{FmzmI`n7ddybU4VNK`Kf)RSuBFtF>IW(+4(ys6&xL^QCWA)WMCi@6w)YQ0Yr?0- zIbA9HiZJUBdZypGat>jDn7>gtY>=V z(SL_d7EtWAVC4Y6M|_&Auw@dSO85eWNlef{-ApMMd-wvg_;t&#Sfh_$+Fd>qmIa-K zIgIa5d?=PhF1G?ICQU@&K0YYp9Pmsyk2>G)!udO+QBT)^{N0AZBghN!xk<|l-^g;0 zLea(^8x`K_Nvr+ez#msFZtx>!Rzr+9IeW>E2KvxvM3@z2`rtCZ^8 zxmWQO<(>ALztW#zO{&Tiec|n%d>0R>D5t42s`h~keB31mktRXMvS=4KKTfrkZH#4_ z^f+KGIOKj8T%`ZDt=}g5<;3kVRRT`bpH&C+DaX7g(D}U=Qz&1b;Zka_=zkSb&UIfI zxnSPA;KTsv=Z5K!zuS)F6Ul)L6=UZ4k&5od zeLmq;$F{HRKr|i*vYpI#wX;q#wj z*76f&c!-in8r0Tb&9@|tN1XF(P8q- z8#P%u6!#y~zc{?@RH`3cCfQU$8Ah#{*@jgZRGS{7nlO}CjJ7kR$WGmScxIE(#br0~ zZO_4{m`$a0s9asUK4E+8WHvsDQ9I6G=Lq?f{{Tz>~5Mo*gIhz z5X6um6TWN=iSdGAk-H_;Fa8?LXLmJS9uWkB5dr7ll<1fU5+=DumIfUeGaX`agsU+P zx~=0{8IXUG%?86CAHIuD(7I(PK2SF&T6NVbZRU!v{Rjk80uI&_KxZ0W$NK9qzAHi* zGL|orG4bjvURb5m5tOMG!Ohk)sqVMVQ&WJyBwNx=LWGFd+BmKaldu3Wlz{m&Xf*qu zPFdhb{2l+l{j>iSko|M0{{v3n|9jAfsr=t}qzBI;ht>R^yQ+J==ytd6P7w}?8a$M! z#AwnYMCdkNh2O*DZDg*~Z&t@33PhHfDrNU$K3?3STn8fQEduUT>b!N%Q*isS!GKXz z!coN19hyfMrjM!O(H!Coaq9CHhgzNyvGpVg%wI*DO26!I+!{klDv`vewS^rHZO`BM z0}q4~HQVI!Gq&AM5lYF5Ao4~TQyV3KygF9}899~QQqQ~5;Fa|2o;nthySkV!cV{eS zrRhEw_>0|eLD23hVsY!f6Fes>Au+iX@~As2Qn zN&QF@Iyu9Rn5lKDal*%?UW`+}*bl2=B!p!Zaqpak3L(;Y>Df3`?wiTwTc&rK(RZ*( z?+m8RpJyELt}KCHkffw?3JaN@Dw4~9EN@DE?4-^wn$oLjeNH;Y^_W^OXuE0UnjKGv zL{tz?6Q$)3`DRFxpQbMEWNAbY-Aj`cZq>br%?$J1_40N(^RyvPx!t?7LL2@=4sb`0s?{# zC0)`n3L_oT(%ms2B{>Sx-3>}N(lJ9xw{#5M-3>EykNW=H=X-y+f53g7dwzm>X3jZ# z@3q%@t=C$6Em%U5le*!!SbpN7>}lijAi7C4eDB2fkiq}QwZ~?YdjD%d=dHMjmx(tj zori-;LHeTLm`PpU?2849HCSfBX_KP~uT&Y4XW(m8oFA-Px%!qu z&&w;X!sqp3ng!eZ*rL$dHBiFkyf$YKj=wPf?JeQM_^x&=@jy|IrF%asMXOlyikFIr zw?8_CH?Zqy*<8z}A`6@bnX46E*fvNab6K`cb5os^*zV}q{3w2G!Lo1b^;?Y;1B;{ZambIhDj7=@ zwubBAhC<1#dRMC)Qe}Pt&rjy|m*X}};NaSmchQku!cMbdrNQuGMEuKnx!-%j3``>g zt`9}rTuTmi5@TS6Dv-7%T5(R=iBZBkmj{XGXMDy1EpjSOm`3pTq%9hc6KD2@T}}~F~W{m z3TLpCf9f^I6AsU6sP4}JJJ0i>o=mb9Jy=iog}ceSWZ!@|p|juaBdXU5+Lzx`#v6X> zH^87!!3z8Hv}3ADLW1^=Z-d-#)+P#Qcb~qIs4xzeCV6u z3cn}GsyBtoE|Rsf7>h1tgN?x2v(H@KDW}7V?2s;7bhu^IrfD$E55IfI-w@8jT7&A6 zK{MCx@s=_qo^}mAqL96^ND&(4m@17)UBtJ9u3dZ=$BGH{IIx3~S~d^Sb8_UY_g!>; zu&Hg3v9ius63VlIYfB9CdI~01S{@;vIZ@&2U?w|@CNjAAjx*W)cXN} zD~@O2^hu`(N>hnWHkC>HdAE`K1&#P=?C16*pRs#nHIsQ=!d7<)ug@J`xSwN)UkpDX zz@~rTTT?^aed>nqtn$rn`>5;gHSP}j*J!|&H%>4*XJsSwpg1}(faZPG<=l@c0$aE{ z!jwv|%8Ol(1Ok!u+L{%)#uN{eulNUz=Hnj)1lW#JMqQR%=PMYwPPnYiV=nu783TPM zqub5WQ@4~g&=)@}k*qgsz4lCRJa=9+?3nD`qsJUCX{bi~25eGt%iPRKpZz#z^LWYa zKbUwqH~8(>JL@44!hQE#q)^4R<@J=V&*crdN~gT~O|h)vxG7DJdjxNVszFNKMWdV# zINEw>4W@y97l^3rmt~d*DheOVP!nBdF*13seareD8^G-UI8CT$35wbO7Iyvek7ihq zAitxIM&<5kkm{?u9jP%Va~?Zu?>yUHv#ELpC3YUBKKH8QHh>2d*uL4Bfh~R{Cx~pU zKIulOc5a1mQR&)wb!P0u&hD92zu_tH*{h>puL&NwG2N4{ZY;{UYKN)u5^$X@e$#Q7 z1SSk4%*IxD0kwY`P2>AMoGN;S{Pz5f%`j86TT31Wv9oD3g`50IQM7{k7+ z(07avl8HvxpU(%KF$n>UvAL$Qpvt?0`CYt<3q?~}`{hichFG zeZF=vWZqpqHJQ!+vM;u#@WXX+es09?N7!FQ>GD^OY8(m*ue5>wGQRzNmljWw1G@UK z!#+D04U<@X6Air@<$XUmbM7*2MN4roaMR`z3sb37=4GnvTt`#`mobMl*-RYT zu_bYyRUnz?{bb1i7`EB&p^7P+#9M5&HWyNTk6m$l3IajIp5MJk7}@^Oqwb0qhH^W@ z5bp>vLASPtOG;f{23IVVT~1l$d~V3 z=ki-L2A9L7GBpmCV3TkxT`r5=Q;5(R_vhfb0a?_C2CklzX z5!JAQNQ7ev_I+#TQlE_dzVq;KeA5|jSL!aY@BW<2VAB)k8PQ1x!gW}q`1-q5d7~9i zp>{pGkr5VMUmhC}fN0<=5_)7pB*i*GSyjr)vUS6`Jlsh* zU>z*A>qh0IsFA7gYbM18A3?b(l2D^VN)Sgz6jje`m`_5Wn5jK$b%}HoQ#5%MD>`co zI$A2#B1?446$>l#@gDET)!>A!`B^|9Zoc)IWc%w3HD|faPa1zGvxML%q{>26)Iepk zKl>l!X~LR5085>QZ6L%$@O)HNXZw(KACG`zii;>``o6oF0#3pvAPG*&!p;1T$fo@@ z^cecNuGt#Ks$Q|PUX}LVRjd&p+0c4+?SP|<_R<+Xt`l(W=u}rZUfT=YtQe`?ogwz$ zCvP{qmVGKh)k+N-d(>UVdTzx&6=}e934ug$b~8NMWU>smxIKQ_INj*7b*qte=d*uS z&}?A9%Bepi%cvSGFC}h{T!6V|_@p78Ba_gqn3 zMPo$o-{bt!=*ydWwSd1Ky>DiwLi%J%92ogthnFhCROp6`0Kyw|&XB#3M+ZX$W%Oo@1i ztb%t`r?oCfky)EmxPPZ7;NWKll}l<)`(J^g++u?Q680K(ArO(d>fo?qlSgMzVnKVw zzAFunS777oq=47(aIK4!=SZTviXOUK1qmv9-{+lk;Ndn*=ZCsi2X!J4RT7ys%>IM| zsv_gdHjLh<#%Xb_)qs#d!+gOFE;wiha{*=WIA!|<1iav(bs8>ykXWtY`+kBuo^?Nj zjlh*P(Zz~}hc4R2eOO;aM(l!J3x}0=sB!@!Zx-x6dQ->Wq;)V<&cH-hW-|uQLVz;0 zd`(qe2z;z84NQG`_Wb58x#(B=BkC={PQ?@v+gQ1W3Y8kXK6?)3^73j|MDWO7B3~j4r`V+N1+C4G065y1|ms{DwY+ zzQs)S`telLAgtiS?((2sjp3a8xr??Y6=cbC&(Wo_q23d1Q|HKubjIOv_`R>CY?b&m zplf0TAWSEoet!(^Oc{sWAm)XB-Bg&ilN*?Y`b%L{ve>QoF!%-}SmUN1&h4LOuGN)7f}s{Ph%9d}x-UdVYR95rr&fcd&!` zRKL7#Z-;eWNK6WGE$`xwEW}m&U|F<2bh|jKx@__Z4YL(9Cjp16jOn9#A369Cj|Da| zRBF@}Cqxq|u|4w|Seq|}WD?JcUx;_Yo3R)pP8e( zjK^oE1pL2NOWV?boL_?D#0mtE5;4NO8R@Sm;pKTjWFe5YG}0LMQ7&#DPj$duNj?F+ zyWLqLH=;RxS{XcNJVeB08HK8~P5Cw%(e?l|aTo}wB;)so3vQMw+?J=1wxht*(c^dL zZG2xNfI)X78d>G=e=56}VVs7<^mkThDL|mbqMs1?!A@wIXWnVYoPzl2q~H~@mw5~2 zFD+x%->chm{z^$zeV3JN&5UDvY32DPiHx^Xo&Oy`s32E@fa@c3JpRMC0LX5jH^(Vhq z{lt7j8|I8kc>y1e+4uw1Rk*C^3YSICl_Xu{x-{~|P;q(9Aft+a=hUOBS<;EVmwE~y zP;kApJVab)qIIVk+nR?!L**XHs)G`H20aiWXg1v*_A5hEQHxS5`b7tCWMj?H6uDnNV4dS_+kp<;fzaN3< z+)Xk*NUkg!!5gMN(vA>noCv%BIB#3QX$TBB2^yEa6WKB<@NED%T&^zHf2q3mO~aej zXh1^+3e$Ak@!RdVE2C`kUOaM~{_~b!9wm3K#BV>}cOsB0I;jo~r*nzjs_kUFEcG`# zY>GsR7s3Qg?&M)#g;|)LK#`>{tJbZYNcD58H~jDE(540f@*5)dUo zvYHsWyP{JV^F_vfJ8_-N2wu(fhTP+Se=Q8c>>GGpAf^Tt3<;krpx`H?-`yPW7!PmZ zdAXnV47*jnoipa3ygmv3IluZ1#Y%5|-CY~vo$6bFHsm0sjVH&Q`S_I*qU-JCAT$Lr zQ7vbT&fZZniuIjD|8lDC-|Zy^tbb6?R&Q2NLF9{9z!5{N=BirMAm%1K)9B^V>9eh; z?VPCv4GjfTUDyY|wV~0P`Pn69@1Mkx8efzgh=29#%7VMqt(^{*dj43Q5#;h1RG`k_ zgFvjmzU)?Kvrr?^X+(GMR+)|BIVU%^ODhEyb$}gpLekZvj|xOR*9=j0TRdE?c4h6d zramhBbr-C<9|FRm(HSZ~mF3!&~cj=8`~<&2+=V=EBA!ofBwAz?Oayh z4ly2A=Put%-OXhv6&cp2D}1l1gBfZnMQ4vQClS{J4Na8q%m$2hK6cVEAO#!vrK@YX z7gugho;zXchgHUV9^&KU>|?fGMDwOX^YW~sdt!5$G&K=-;^YtEW`3pd1*oN1t5TTj zvS75Cq4sE+gs6AosN>;q4x&RdHMVQ!!-Plw7W`uu9jM+F#p`c1P?^*ZVRV(Tk49>j zmF`*=shdNg*EA@!k2)rsJvmVG>F&G5mXKqT`Pu?yjqW1FiJvGK=!Ec|`NIVj2QVqVKb7wmA7`nCAPGOPus>F=w7YXpqi}J?YTs- zcrqZrCd(T(HIre^@6@;!Is!&qk5x@`#$5g{7eI`=<3J;_v^b4y_1&l=Dtp<FZ0`OpfNbp z!hdkZF6neRynGGg8#6>`+ApgV)4H8~dr2oaIjT+Yc>*T!Gce?`6K5PF<292Awp z=C-9jmQNg<7*Lc14az2AA@bsESAH`jrc{_`!z)ET#zGwSTjyA(BC#mRn`~{V7MsXz z??uDDNlHfMI!rms#PyP{L;hjCV#T?)XhucUJCvL&@~$m%PN5{tl4H9VqSnt#dz#zp z^h@KbD17hW-78Nz$`Rd$x`|XTOmvp6D-5JCcVI z6V}JLNo;W{jfbnnFg=~?JkiW^Y!3ku_JBpYkQBa=jN<&*M)&$7Ngu7s&prDvflQ;f zVtVs-4%=gNp6_**6RpVRe@4`Kp>`Yozz6ovc!gtI8>IKMrq4SPLz3Ilo?f#II{ANhd%bS15R@5e;D*rO4tDm8FbuF$;b;xSRhQ0nxMrrz z*K5Cm&HD2TK}6QMEz@KE^4in$5E)AMjfT3%K#l4THswIjYl+1RpDEGyylbQ?_98Q z2J6$?Y0p$2UR^w{KBDR?NFo>XqMQ{V(T}m4d?#B_uLvs|a0&b(x4V_m`|SB@Tys}| z6G2$mqdx<^;y#0;$d47C?%l`(3|ZMsCLL}K_?3LEPxnfSy0TPvH2)(LGLRvT&zkfFQlKoz%lv8odmgQH_9A%BLIAq{63{-@d=4=X>xc5Wg_m;dS_5xETwKV=MEg zm=AE-eR}=Bti=?Hpub64z^C;7OMhkvxSG3v{5xOp|9|!Wdov*Mt%T8CLV8CVzyn;Y8>8L7)?E-Ftc|Hyh@d$kM=5V^5fH@g zg@yJ?^Ld%f8_LJ8#!Okgx9Y8VMuHh|-pJ_HB|)4WjV~P|+k1*kGKyQ$8AY@Owub(U zjlaCec!*9u?u~mW6#>Dfh()Bs#h^ z+x2pFmgGccPD?4}+8Q{lCU1w8?ubK6knbe078uZv-O#3Dk=qN%e z^c2%7uc@4Qj7#J)j&36igs3R1mTWiDNIanr0S4ndb-y(^B(h5>yde(M$Oqm@3 z+^2)5$pjO-6yL;#s+5(0^sk>aCiiL}==l4F?EkzU)oZkWvQ+*DqWI1#tEB^1$d*Z30!RK=V3o@VQKdQMc05XJ-ZvPwPSYdrl-LSaSgE*9Ey-A z@1j(#i4yDq!N#&q{dyhiIm9Ify^pbOCFe-gVg3BEcznQF;qRpd2MUXQWQMHl?b$X7 zO%H`RS7y$>+Pzp74Eb<_&l^XTF0pIrs}n8%gXU=Y`m`P)6^9c2G;J`@v4c2i(+Z|B z)s8fEWF-W%ePtrbMgb6H>LkIdCP68GBtZRI?#odxPsQdpXs z+c`dD?Q>*Z&bA(at$JQq1JQQJ!$y6LciLBTBNkMwFBUpr#cI8#6@hukhn=ZSKx4p) zshP|8A#0v<0pg#VH5iB z&p;5sK;ZiJcO^8msHwMVcjr}fb;Xv(GUgsD#MC86PozDR2YVXJID-Azyf_RAR(qd% zvKh4Mv;S&s4Gs$0V?Q%c{m4>-Vuuzq8ByQDx`^cD;^MWLUIC^(p*|-k3}Oi1gBpMc zWHgiucW#W3_^AV{@?umX9-mLQ#=^o3D_>bnd@a(si@ta)ICBi%;lmD&=394g*s|r; z?&>alB=7b*XFY?Psz~noThnsIUTo7&#S5$S0p>c-A`lawo|wVX!l6S+PQ?+B(0oz$ z*;^l6kY7|ctE8oDwDj<9gG&Lh$G)U>C~)(8T?p4ZEvIF@*slJhr%OBh%&ehOJ*gJD z>;VEPt=sA(m3Uk=`zr)WCN{XMQ@Q|cKdZeFH(CnL_-Pyp%unol@u50}YB}I!<|XN1 zu-v?TRCGD4xOQ-YVg#pfwf0YG1(&ecpJxpA<8UpW{%JU^BBb2ADE=(gKih=4!x+|} zRcRiXDQ1UdyekmF-JJUF5PX~OaFagl_u30BCC*r8#b24vr`&BrC9Rd>V0syn;{*h% z&q%r)xv4OQi=AhKa_FCQ^ek@fd-eli>g7Bl-!&~Y?rnagB_7aooO&*yW}(dNREQsydVp>p!qNnz>x@=b<2 zCIsZ~mg4Yp?fNg9BdfOGj%VgqHl7Blm|Ra8R*k>0>LV@`1AV0mU)}io8+|^VdPfFM z&fy`&5S~Aabb6p<9=dL|s_jy9kKpUs?)+Hw%~=<%@3=TS(pz+g{M#h54*Dy04}S0d0X)t5wQC*=UDHl4w-=R=MXu+CR*!m|quUFH zk_(f$n*fKC`$s)tFl{WFb(kNRR zDL_#Lj>g&g(2`gK8{3iOVl|+VE=$CDXJ^I>626&NPciHTz~(v5xe^rko1clLPC~qJ zy5ln<@ZiRXlhiU*%8H#RvPd;|EE#s${05&F=rh{Qb+4syMr>Y2x`fG8?nujh-&msa zvCHkhlJG@2KQGU&CGdB|pY?7vpG7eAn)y%WT)AI<)JPr}y^N4HyFtjhSufuV#Ersz z^IdTLh3}Jclm0zk)10qe@7Vo6xpn(|C_GD*^y=di0`pyGEpTL=`<)Qz|GmCCD-fz{ zJq_DF{|s-6XLuoZ{}a@E72n~Hfe9CXwO!*y%tbxv-)TyqLI2rxhKOg;w+$|S_1@sk zVj2us98#pLYcd3vYrDSc)3|XB)#uSyw?dr~?#(*Aw3^_+(QueatYc%oSo6;*Fzw&@ zkPgq4-KcI(?4bjW_KWXRHhs8hJ$PLdCnx|Icsr${Nfv)xrD>>w`1|a{rq5DjOwQVp zz#iE`!*B05G5ypsGN`&S&Oh1k^$W$97dr%d{kZTS@|yx8srmsokmIHIHL&7t50>x> z5tn7h&i8k**ymKbrafwl^!4^4(g015$tmC5@V&a99Tp%0|NV0jZ9z@+GKBB%z}eGY z5B2kMudjJnH;;`W#%yKX26hvUELsv^s=)3ZAjv zwhnpv+3JLjji;c?9y|Gum(qc+EY(ps)x!3)lz>kliUaSz3Ekt{E-bGGd+ENi*{yYu z9xrge@qsS?q8nwZk{a;=T3elYNE=ikmQ0alxTo#BF{z=!ZSTT6WRK2|-mZeZ40-|8 z&|LfDhM8I>4&j1&VtYO~I7nre*w)rYMKGG+usKXi!W{ZnaKy?n+BRmZP-P$lfOf+r zNNxP&fT^3_G27qv$or|On>$L$WWlFAHZw&E9)p=UZajr4%+w3c0TW8#QYLRv?RY~W zI5qgA?9UVF1P|WvoDZeMe0e2A&%lt6x%a2i=s-Hq`3NXcD*-7~FRj)y^{7HplJ!rp zFf+$U(wIwJZ&dcK9<}=8ho^oP&uv)u67D*!+zd6QHJs7?e%ytaHE+}|y+VC0HFbMK z5LxrN1sF4ZuW_mjjxY-ck*pQ5_!C95Rj}p$9Xvt5F-s>$Ja%gj55U%ef`Wp?G1=G8 zcQJ;kM^(}4fgAC-Idt;)Ao%`ctS|PjxVRifa^y(ZbW?=gt22%4ZB*Tf3^|P|N_Vt` z@~%<5(7Bs+Fs2TuiQY_m=eFM`0fRo}*H2mbNXY&udpAk>`1zYtFdJi)ILPqHX%UJr z=bq4cb7dyUR=92+uZ93+I`Kvor<;;CX*z|=Mi{uU^|AUNr%&G_B6Waeu?_lSNfIWn zHi_?tJO2J#kq&k_WmNl8;Gr_Asn8sp8LD36sj*u6|6T-+aR?9BdS$3^Myj@Q?Yh@05pBLGEaE(05P z*~@9;SD=hibh{|zwCC&*3?s6Bc`x%{JJ;p$|Eju>2SpblM`w5v5*)1h#Et-tN=+M+CdVg^m(~is zZ`2p_rH{J|uLDcL!g3_JK77+Q-)5YWp+wO62Rd20Z*jf;pVj7j*5|z^5K%RvbvZB< z3>;QS&(0o>-A@s&w)a}kZ0CJSl@6b?eUsZJ?`GZs(J~64ieDb6)C+_IA)$)33f$9F zmJ0gIk^~~MXG=ci^V$DQJupQM(zdT@f}F3312Y;{{6MCh=>g?vJ$Ipw>Par#oQA~nR=L=@v%+q}x{tY)C?W1Gzpx2KT=;>Ufv1ko2-eLS z4+v<@<^l0rHKZICQNt8dhFElt(PQw0?_WFCod4exYC)brGw|URzvwdpMRo#SQWC_D z*A(pB-=)sT;c1;JSexv=`DJ;T*t2FtLPi$zLgB=)0$60$P6{v9f9nD)ZUPa(im*%h z{6wrASwms~V$#-u{`&o0S0aM>5uaAWf*oRKqlf`>FTkKnu#Kw!(Smxv+e=OnsdB^5wftjTNgLW7EhnVXr~!jXC7BgOV8R4t|N z*wGSs&%ios66J}x?CZ4yANl`Ts*XIT;~dBFo3qC@0kEUYeals{V!&C@TGed05$}8m zOK8P+Yp3{3@P*(D`p=0A>a70*yiNP{8;T@8;c<`BD;&=ocbUOZZS;E?J9h=+QY$Tg z%e(nOuKl;Fg&iX+H?A8!VEDB8x3}n=+-zMN%2V6F>x1_uz;DR=iIKOpU3i%^lBZAj zxP(%aqr<+H~oBZlA znwjL$&zl`!F1s@pUlF#w%$k)On+mL>4AZ;hiFh4@OW(r#u%}!Redg6qI0UFHt+_(P zH`q)5z0((E`&5nJyMUXrXsppY=7-_eb=E1dvEi*YKLMD7@w9!dIB$W$-xDv*0ljt1#p=^~gJzZHc+bkK2j;=7MLMUtE!(*zTXHXI&be&I zF41$8)ru%D4!x&cM#xeg=OE=&&{u=6wwO$r)EcA9={vtz+$*@=*E4Ad)|6<9-AjiO zxF8?rM9qfN<@QE7-!RZiqnl90y+?4;>Bc~Bc( zH`iJl4I$6PlkWrfbR|PR@1YM0c~!p(ok6IyIlt+Czo3$<_rAHUqkYBMQtY?c-MzRu zb-7x}m(%NgxTzOsz34wbZK2U}gQb+$Y1oPRWm%Ug2E4%+Rz^lfZ9C8#K%!Haw+~*< zE!+Q67?_X92N3OBN_Wn5_@RE}kaZQy%cFD6=R{<`hKtg`u>om~3ZoMRA8kVtUo~bv zk-~c@}eF)=Iz+PbC)N9 zRvZ*Byzc-IaM44fsg`|7f4TwOa{z z^X zyXYs14hNKvkle>UIZXMYBkrj-t7wMk^Uk`CEK3$Nk^0zWDt|H!CG`sH((dpgfxG`p$P&kNu4 z-CB4p&x3pQ@{ejbD2kpJI2OyCHm-g$q=#!6m?xD@e}7fa27xJQwr$<9!dp`-eg&v& z2Vp8ej{_F?db_63=lzJ!uiJ>QrxQ$-_d%AHBdE2mY+e|#XKB~`qJ|IA+e2`YcRK-v ziA`uxMRXHTRL`%ifs!(RaM#OGmCi=!lYlUT{!g}MeRew8goERBj7Kd9;L$hqdu9)n zT#xihve@>GiWuV3KOX|Sj@OpIY?&n4Wktm8U=g0=lproFUb#XRs~{JxjDN^&uA+Jo)vmFK~2FHpBdm`X4^qA1>2xzxf``lO$n zrb-D->r`k})505|dG0IaO`sV)KIX>cCXeQU{9{<01QES~J>h!YOiF#IMK#kU$)_Vr zpJ>XU&9W@c>$}ldL)SKgW_oSk;E-xEQ?Og@CCw)6gaV4f(i^|D^& zJA1{JDpU=>^}+sB$?e(%FOmp}Ea44$RzDrJatmhPVzu6C$K)NkJ8pLfO9_tKtTrN~ zvzP|lEK1+l&vyVRijL0bzviFOHvuHro6UioeZHBrN~S>Rqa$14_-$e8HQQVDAAoMH zJ{PkkH%_Y$^n~AT4^Mm};$GjK`^jAhp$||%UJt~5M)F&I)lV4IN$xpEzu|99i1V`B z$E%Mz9UfN=KMp>w39x^82P-ppTcr71er_z;E!wxM$B=VZflyM*i5#u#nEBp{eU@?m zIfNKMFlLO&FcAO(gzj&8=?uIqcNS6XIwu>)Yj!dyXc}q{Q~=x4u)KuINGlE3_a)Va z?D$x`XrM=02I0zN0o9H+FTKf9nO%_OzEZe?7+T?E)#a|=fYP;gbVbHxduq?wfWYes z8&4>(G~karO;I~{l`^lQwlHcKc{yQbId)*Q>LRz=(5-6!xVdg1vn ztLLomh7oB9`)q8QM^%#v{TfTBaBzbL?Zt)sWt6?`z9157#PMTpWIIHGJioY09Yt%rq!wjCeS;#;x+Nwc*# zl{TC3IasMYFn~Q_`IX zb*+@BF61+~l4cS^u7BMrNUl7^DfZ2iGaDuAOSSr`8TbxTQ@X?z5U{*gZJe`UZK7N; zUBU(SNtzZINF7LAxgLsk*}ps=^HZBufz8QukJ$L!t6!`3TLe(&p0+{N%+MfJx0jjW>JY1&Ur@8=1_;VXr#O+MZsX8&=#j2#pVtU<(L5oRmf* zeK9A2a~=vVp>tWoi2$D86%Y0&KOVm-HvBqRN=3F%&}e0AMXFnSgde5?(9fYU1lzj* z%LT9k=)>9eMLb>CnuHA(d|>yimCCmw6sRIQ%fzN1*3(L5j6o|b`6m>=`^JP3Y^)zOBLMxfHo1 zkUBKOPxzK5{c^=e@!rIWI<$HU9Nb%uHkLOxN*3`dz;ryw4gGb^vo~!p`8V46<<%lh zLsIl=&3V_@S=GLAHrqKhP=d?3nuEnbp{GmsL+|;r{f%jRfBUULMF_mL%f-N#xuqD*@A`#CdoRlS)VmKK! z;{}1sYxBGPXsjr}KQS#0d91@HoQF&;#s%G8lei$!wz5#$4m2lBhg-Gx4fWjre&^qN z5Sq+6VE4|k)odka%3kOMeZH%!bTS%O>7r(%UE$+gf}^&w3n<%<5s}X44-rwy zd4EVthL(TIh$`^0D^2~fDN)s2%$3)VtoK&5NgJ(Rfd8VeH!wHI6sBt)@w&3wq;}6X zyyKe)Cw*4wt>U>Wjhz~9mfX^=8`m9x*aQe_zo+4U_sKv0;aA_KY0_ynXRBLGR+rq>t0W!;8SS8!y4q?wm1kGU^dIDBz{;MGa+l4s!|9HA2 zABC>$@JsP&@-_8G3f+8ZQ5-#`I9RO9n@wqKdA38(K7RZ{Ea=3s(-|0=rxfL$+vf;_)Y)6H}9%n z@mn%X9ag&}iWIfF$v~88*u2aju6mcrd+N~xyjy8@pC$mGH&oCib;Sms7yr7Sc-Zut z*(_7RM!AjAPpHqfITu9e4YmczjqL0e&Cj^z{*k(%ZCt0~Pw*cXfqAM{dA0dC3-{hw zo#1V8cBR9C6HniMre$7*zMhyFXGa_W)p0-kr{lkfb(QlJ^&@%+B(`HXHR2^cN~ii5 z-p0D%_;n-PAT-16W@Hvj;{R-+ea?9$14ZElX)~znPv$XjKavcw!C^1|OV%`hNC+BC zC9najM^N@Z|7?Kug$u)i_ z%e8P}vKRHB`a%91tPWhfzabp4mLLV>cfo^o2Wg21w>|%m?f>FAoexcR*Eh>vf|`?C zEp2!$@l3UrsU1Y;G>DEhdzk&z{~t5$n@_WkkWWf~dK31V4wPU8d)*=}4Q>>sd_WSm zmUR4;8eqQ(HK;Dmu7+?Q0gz5WD=OzfF@=o7McrTH*)#~CMRpni!^=QIWr#4<;>DsV zz&QCNjk)=blM=LTU;ZA7y2>OWYrOhVR;69h7Y{$AK{@*smQM>7AYJ)mD zKK+gVpWys3ZCGoC7AB90M~_ACRv)%lfP=U5wzS^(RcZLH4p$G*W_o+7b$lDZId%F> zk4e*Z&;rW9{tdJE==UHSRz3+}MXDbKI`QKg+z4vo)0X}Y-v!rCXl z0HA&r3)YY-=7lMGjoc~&Jjj%2AineEoqixMB^$x6G_ulET>SHJ`D(Y6y+owSno17y$w70NCgkb? z)?;NmIj;?ODWl}H07p8p_)b#1a_7REVo1n$%o!=LtxXQ!gtWapA zyqdBulX7K^L!HOFgpV|D`@w1|Dk`-LG5Ub-c#?E_mYHoL=|F-4gflDF*)GmQ1tp z!56PZROu=lwa3h63T^}o&;vc-zWIvxMjxhTAF}f|a|wN+SKb8P)mDMny{yyLekBP> z<~K2k@&F>Gz;#k8XT}`)ntAs6I2v_|&U=#n%t4fQ1`Jet;MOp+TCseAoMj2GRt@ePeWNRsh8Sy7<$h(Bz+L~ z8l_-5=_LnOsB|m&x#1QJ_~{K9hmJR!e_n&+Uk+97e==y$dv)?`Y8zAXdqa+rnb&7Z z)Y!$LDK?-hxW68ngc&&xtk*Fz@p&-d4Ql56p z-gmej6MNKBoPx1JVgS(=J$&RFODY}Oxc<~DF!CK!rkFa!pN~=K3rE=I-*~RRha%-n zHLXO9?n_0L_#Lf|cz@>FQm8?)gW%EzR01G1J@9Fydz+ig;TQm-#=rG#r6b@dh2j~9id z_QC88Af>&Ms`bQGUyIbX_-?(0(4;Id94yKnp1Zwb@Ho0VI21&pkNBse0M#8D&M@D= z#I%Zf!?CWGDHV4;{`E*h%TQviD??{WUK_j@P=v&;`xA3^OSxJ!MY5g2V43e_AbObJ zC#AiQFw<|+`#)9at;NE0E{n3}9N|YbgdBH1Q$lmZJLR2Yu@6%8wR~EEAlNIW|3xKV zwoL12;j^OJ@rLFu_`r$fGf-rCN{_cg&*Vw8)jjH?Ac1U6DIwMlLJFF{YB*fEiT~3O zVER?&RKPdu(131z!v5qffOQrDn8lIz?NVOT1FEzv5YSBa3d0?WZrm@*Zf;^YHKV=s&Df=I%AI#VR;eU9*y;_x6u6U> zn@dFCKe8(Ob;$fpa)0$1z&jP`HcXF~j9G??QxjZXT}`uv14%}n-=>8V@V30)8Pne9 z*bzUK3Bdyh#(G2f0F;}imR6_ejX71FGpX}3;sa0jfM-gq6DW5TtFa@r_Y{+ zZWQ93reT7edM4y`X*2X#6OsTtUfK>IqvnLz#Kby_k(^)>wt+NZ1&U~{kI;K)w7n*1`2(f~a(n3k+C5Ow;YPJbLoqT1=$jD>0J z;WAj^Z%66z*{x1JwJ)Vd#jZ|4b915ck^(L-uZveApTmG~PV;F3&SNiei@IS?8Hw%E z!V;Qe-;ktscP=#-M8=-zyTHKcC&WV1w?cUTDt*ePr(|Lk(~U=FPc#Ba_hdPfSbCgs z2^cEx16hU9+hs9?#`jRL6k1(25D&l=@_KD8JeMy?wo=74&Kl6uIF9r z`@XgOgFjs7ocYDx*S_|(_cjr;`^+d{=JN)KjX5qI_*+u)yOf11M=-FUo3>uBgA#<) zyC2sBt%?ta%2FFQ+l^;-7SLC0DhB3HE0z&TYR-*i{iR;tWTt%*YaRupWkKG3$9@SatGQ9y*x^J^a9R2}Hbz9wuo*N7I&cv?>d z&lBdi#Jd~LL`Q1Om_v7@U&(5w#nyvOURWbV|EgWQSU9xG=-K7Srf=bePvnU_u4-q& z^p%v?mre7AyDjzEoyHZB7{07#b++da1J>|Yxgjt_q0WiYi`=T@i$4y-Wkw!Rqig;s zf?8HGQS(Z|l~-gBS(O#3724Kcbl#5pQ|>Q~yZrQ?IVg5O-FbT=yM8}WYB3*;-n*OEdI?Jh)q^|$~-r0d7n=Cx4#E%$+odS^03EXM4gPa9)11TxMo>8N}nZb z*0vw`4?GH_U*?SAiT#Gc9smJdRVEb$HplE7WQ_3N`D_tir!Os#%O9}(P`KWykW_1A zyQLP*uU)jy`-)4h*!iP8%8CQze!E+{vFGwkA@Sa{*kx{i#k=E;3)?sM+8w2$$|T+& zO(L?D9`2+Z;cW`2; zmCkb1?_AP_V9*1lW0r*(;rL0z@c6-zV>_MxbFhg zlS#)N<>IkMgwg z%GqW;OlF2iN@^?Pzd-ChyA>V--fg==zgPUnp@SVyLII!o{YSA{9(*CO+CN#92^4U4=+!>L@5`(_>A-m99@mfJ1+}t`z%v-$6JVY61Hj?9lN{|o9Rj8 zLVBI&AUGv{=OC{}RkE;p53Ipp5sE9EdcPkTw*1w{R}VY(Tw+An+e-*Lx2rC1 z%KaWJSi|O;5V?;YUADA6eOH9k@@<^7!Faa9mLe)@mgLbplQv?vr?f3E3qe*aPpig^ z`ugx|Z*Ts9-foBV3B#jX@6(&2qBZ#WEly3%={G?0*kQMNr` zH=ddvGN}vUNrEp@hP?FDWRywFS~7}`Rh@b!TFgi!RGglLKc};avR1Lv7EJU{qG^3g zsb{sk8~LGJlix5lm4}3+dl1e$0ug=LM>OiH$tHTqQbLZlu@t4MG`Ta+e(jo^&kzW6U)1U6q$t7 zf}V39bv^sdYj|qi(b1it!rZpVGwk~rqHW|SKmD2cN(loP*FE5PNQdBat?`FZA57Q+ za5vYxL1Fl%6X?+*v5B=HA+vfxaN`u->sUMY&$2~*>?(y3q0(&GxuqF8nwm+qt|#rx z)WhhN384v_LO}x!k$&|zQC?_oEYnPKU% z_=I>^{nDUZh9GYshK*UXx73H!0INK&d53?`Q~scF>kcQD7RBLqef5@l-L?UyBVNa- z9@sf$I_Ayu4^Wm2b=cxt5HBY%C`DvH%mi~KV_XYS1JfuG;e}l4xi)HfYfRMl$C|2W$8-SrNH@%G(1=kgL(j zVk<`eqhG^ezPZR~hfRcHH_|)mvlSkAjyS#NqteWi)KfOouA}d#=}*PC=a+mY=z3F7uEsReXYbA4ZC>N*?rzZJvI8wu$P)Gp z{S4UR^OXVyW+p~iQzVW1u@Exw{hNl9M^xpR$oZlo$d6W1Vw~d!VL?{Z?e_sW^DREm z6R*2QaF1dQPk`Ke;fmtY?M8pB5SzO_WZ%z~B}m)?lFifZH#6NX&31Z6YJ2VTqT!e* z^3^_6LzGHk32bz*`P}^sRphJJt}6OlX{z?#`wqnN;*Tas*8l?*O|%PkBiIm0&gIp( ztVL^RjHd_GgG;*Mcvr~yc2}$SCG+sK3BkKEg4+q?d6L-2!)XD%p0$I4mjg6S+IN8&==Zsk? z4(PYV<>eQhNdPTSPMMB(F`L0a&2+mJa^yEw?|!-HgBScbdKLZYNrTbv&wG?vZLjQT z?T*$p(fHA~Et2u%)`>fxL{(h9_OH<{f4%dppJE6`|4oKYwQv$mdjkj+Rz`%8{0<(Vup;-6MUlh9>`%a4{TeIPi%Xt^)Ae}dl@)e@z7ky<=G zYXGHl?M!c8J$4YF!F$L4U@#*G)(kXSL{-$16Es?s4?iPP^1Gjz+xE8wm&wB?uX?W% z^>LUfN%*IctbarY2IrVZmXch3gWu`hC}T=kgi39$0yo3pw+e$hQGqJ{ricc$ei9oQ zhk;*%copY*ozf4~fYFqho&ree?Uqnie}+B))7!< zr@@LVS|2Xk@pSP>9e1mVgf;NvKXI@;*fjUxvgeQ3(6i zy5DsbceE6oI2U^ODcb&LU$do)2Bc?9{R_{#Opt(6Nz7eL%x!Vh{nj=*1#kXJTQrs6 z9FG|q=Uhh*vjI|qz^Ojltsn&ndnwOMI1`n;x4-RNRxtoqnZKuXm_uOjzCJ7 z*J7fQGGi-M6SZAJ^8JbZ$0POPfvKlZ)290)hD#f({-AlCR;+@ng2V3fpF8z$Q$#dpa+URTm$uj4Kj0o+6+7_rJ>rCG3R&CFm z#zwqES^ToMgK`+z_@P!TZ#Ji*_@kLP-G+~KiNnH?sDF=5<(esLh=uX;kLLFoip5>D~%pA9`mZ{6BLHeV@j~ z?fh!RgDH|1smdg(Rly6}O$Qcbg-xA?pAU|JX{INU%{##irPXAEVf zu!$8b=5`^}W@^s1R@1x0rc742J6`QqxfR4hp3j8eg_f_vI zy?C27=@rt-HChvz&zNfqD@v}TatAoxHW>kLZ1^mBAw?jfQ&KUnajtIus1pZQ!h*0h zXKbIP#0k!R6udV5mfY{t`^;wdjZQ=jKqt19zm5Ja=ROKs2Y8Ir<96=-<~PrIVA7+buh5agdd7Ba&jSX^0+u%ZF3ZVXIOqKI52@O zmE`%e&`@HT-8z<-iyMIVB<>!vt`&T365{i&#{Tyrt&CVfu=LhQCfVio)*+OWw&||K zZ|K1+0(*Qio;vEQIS1Vt>r}$OUy!n3%Xn+lo+d<@4KMm)0lLwb6OS2r34$<;(2X}e z`wM9xqS3Pb>@u&ZbI)_*!&|~-F$Y7h&21=v!mexpvwP}u zp6Tzq6A_+YTe2X}Tvqx~BtGF+jyULpa+H(Q2%qPJxc~TRV8U>Ty!oig^De}Rl|+=< zz!M+8#fc9pThL*~L+`_A5&dsxl<{!M5lqM(TKkG@+svD7DzD;(dHoyolLnAdq+rvD z5fGr=_w(6@p5fn^m9$K!hBrLw#!Dp`#1C)eR6hC={8V9jJbWTXHHa!9ZOe{@NrlOW zOAc63b~@R9P+7n5&!P>XDD_d!4~{;@vIEA&?Y{K7HBHBJK9@Uj83N)k%%f7TyMv=g zk=4moU@+KXC}nkrK@!k~pS$XH?Wx)$nT7 z%o8KOWkR#9g+>-!8k~QKgUv9XqRDlIzEt6IpW3MS)^xS`pv*&d;YlZ($|gsSEps24 zF=M8B^Y&#n?19|&XLTx9A5Db+R}1hHJX1lytUimZ`0>Ni((+aAGOhu+y`?4bN7@E& zh*UIrab@MzzK5%;t37lXXS6SJp~j6If7nSl#NuhqWIQvAgCER_y?t3;o0-&H8nL# z8T^i;B3B-QgrjdYTH;DrAg=Tv{ZINQk~8iLRv%e2bzXTweD;h{amU>~Px=c|)iwpH z>Ti~BFZN(QXTxadSc`7hd1WEc!=+|)U3oZ~>C$&TTOrl@vxSIOIW)9}CBoA6+qJid z(HRgvDLzN8auaEEZFYPA%%;!f2n6kYnJ}ID;SR&YZ4!`tg|k?A?LN479gNmpAjp$W z&9YCysZx;;UvC`aXR&MRE->&6o^#xp(2uP4+sTXH+)is6?hrYmm;>PJp-J+xfa>yCX+EU~2|-&CTwn**e`{21f+y)Dn&nETNJZrE-y}$HGBR`GR~<6;JfSXr zH+2_JyLgaQT%4CWIx5tp5)t(JGpJ2RRo(kQmU-`IQxp-7 zVV!1}Dsx(qMism_vE8y?eK@B}2SJ|*T6DiP*0{i(#t$Ia zDDEq=Iu_~u^5R`PH`uttI6+5)S)})R+d2H>L zp9Iu1fo6U+<`=e>!p_N7e?rp6@{;Ei%tB#H0Qo*mgshXpFMu^h# zd}b1Lk;GZ1koJw0Yu0W+aO>SX@IHra?leh>Ex{0A3jsqPK_Vj<1vOEApFKaXtrW6? znkTvkxch2QnJmn213p-OiHj8yDyEC2kWJj?AgLr)(7PqSoR;;|a;xiWb5Di0$L;d| zP1f@{J;}Q&$^?IN%!((8?Ut6d{n4sMZ zniTs{LwoF*Le@j>vgJ!7t$lfkss+Q%kATonDN}!VN-u|5hsM4?*N*qOL~-5*Q|HwK z)TSDqx2Y{(P;=e=#2nJ;X?c6SE5}MixzS3{nQmWsF!&vKnng;e~iwkPN3YKriu5`nt+foh)4kV+JpeQDb0%*|aA zlEVTAJ)huX3!CvR(`8~Fm{*H~Xs+z60Bb1uJd1l%V$=5U_$=Qp`x4u31b_l?ODKpL zaXYoCPECfJ2{HerM`dqwSU~h#L~A zCD0j1!w9f=XU09A54DF4Br*eOEK0p79Hc&uTl3h%2baJ}FS|s)+Udz%k`&LlU(Za+ zvhNlTrfePCZ(s-|;s|F3loM$&GhJH~HvJPHC}{LDV&QE}CaLmMMUSiI^AL zy8oHO;>X`%LVS#z^K`k^^dcFAyv%eVF6Y!LAq}zBY!W^8BRz$Ap1br4~ke zal!I%4#I9*e--RS@br)oeqB}b4=_p*v5NdgmCxsj=>-7=bYfyW<8Vt0vosd}*=-Hx zcAF!Tz(@t%+a%rDpAg@Bm%illaj7nDWe)B2_0d0Px|zb2e}v)_T$!C+OCTisXJ_bh zCybeZ`Nhp{`7=pcvf(JjhaJsm{N*6mxG^UpvBNnY&?6|EGdoksA{hi&2apZMR)m%k z1sQ4ko;w5xGBC^@>C7EE=Dc^0ph<`so_GHQu60aW6mk3@?r?8m5j{gj8=EuA9^MSL z`A2V5n<@I*@&GMs&?Nt6t*XuW!CfRBW#zgSP$ldRZSq8s&X=z}zs8D?pYNn26??Q- zHT&>EZ88^7sSDx@fdv`x=u$(AeD3?ae zJe)f8j|R5lo}R^|(i*x=Z+*2Jfhpz+c{c0)u=WLNVoB~NTW(e1s|4%z365A}J+HXY zH-JG{Y4yN3ZG4glt0@lqagUmDVqlQvUHYq{wPxEE$?#r!gk~~%@XZ3KM=3ls8-KO& z4={t|bnZ5Ve$WePnAFH`;$!q_x=lWIojUkr?icXoEm;iCI*M*h)HIG(pE7>H1d{2M z%RWg96B3^v1B9Sx>;=+C1KIO=p1EAa7V-G?XNafAE&JxUqWu_swVS}hCsS3%c@|6D zsw$xa2VzBIc7pFz)lAJst9t}hu4Ce=OjX(kv1W|_@HqXf3;H6LN`I$YU6bZJ6GIew zW&t+v>aTW`!%Q3KV2vBz!vkT$pmb*a8_%{Kt3;=&+r(_nB@B!ee;}bUv8~_2Ybtvm zC<*(%+ojHJn4kyLqcsF(iVE6}-6^jAfySdjIRG-<=SFw1GUktmtSv))&pNw%qTwbz z=B$ye@giPgP#Wxq&*_cucUBvZ9?0jL8W5+h%Rkn0o!CnIvPT=dL^3ffp~lW&CYnVB z?8(e(feomX!gO(H*QM+q3;km`r1P))C^Udmz4f{lfmq(+$&8D;^c&t@wH5)?^`(S{ z&YYP&)nT<}7{5VA>*Yz z*`NA0JMW@35rhk**3GJL9rv@XUCRf2c@&Q*dguMGEubsfZTIk&fKlReN$T8iw1WA@ z)MxOB`==s<&YN(c;sa%@SF>RLTM?7WUSzA-n^r}O(2?JBGh~3TJdPc`?Yk-L5ZlJe zZiNb+ihnwv52{-4gkKzkO`7c+Nk;04&J}wDOb$SQK_(UCY*g;!&~yp!24PE9=2_Mo zE6Y$rjbc5C}c|t8!GeYhJHa$aB|P| z!6dK;)mql2jBcNGpG0J_{IG1fb4OVeFSi}O|9NTv6PnPuDH_NXHMkmsus3_Z+f)*l zGY_Mocxns%tb=z7pnp_=5vZK$4LoNJ+8o_rtWTjGtSiY%!ihCDxXlS-T4vmHT|Ez;nHLNb z!}Zaxp97+)eYtLv>$!0L`d&x_8<%o4YzX7F>skNU$>!Q>W3#@TtpWF|%3q4TL*gw4vDL zwhp!&j5XStxz_E`OwtD*1~m4SPEHCN2Mqz<;k zvAR5P)U$DrD#J@PX$)DUI{w&?@!OH?1Ol4`7=qc4K?vG{f z7=9F&aen+25Gwr&U;oF{%YNN9u`HK$qdUnE)L^MDlN4nqf_1Nl!&|Neao0S9{^qis z@xE4g$^l!d+P zUh2$Hwd9vV>li1wV$;nm?v@pIyoaw*&^o%U@LR}nCQ6$>pz1wF11OKH4Ky~7^^Fl` z|Bk9l`o9h~hN#!vbyd(TkruocOj{MO$`cQ6arit{h(O;`Fi@6ln&y# z;9Nwo{N~_X5DQI8e2EXc)b0pHsewW`OjROM`rc){jqlUr!$qR@q9T_Ctm#Zk%PRjD zD-Ky|T@`x7WL;+`7&WNv1~*>5WffM^%9MRdy*I$wH0~X;yWJ z>KhoqpioJm0`Vkz4-WNkaXAk_;qq?gh!j_0|_o3C8U z@s++^S+!SPT%7CfFkg2Cg{Z5pFa8xv+*oYed)*MtNU3|tVj$tN)g5`}0%&PD7}TA= ztJ2HGO1(^VEwJA{D&zcTBefB{VJE`MrOfGxuUt)S#!JUC!pI=E)I>Z9e?iU7JvI5- zBS5a{dp5L|kW#Szs%vu@S3-Vv)B=Mtu@|<5^_b)6P=365esL25X`pA;S&;?TUccCJFaDU zAD+0d=)b?a%+1HVEvuF_qgK0n``W5Tz3E*CW%C;W{qF-LNh+eUIYCc2D1v=Z3eQ!7bCbH<>hCkP2lXz2@xkgO3f!IN~aeCadeb4GKv(YJ!+CGK}zp;+=za-gd;f3 znnFW2P8pj98iRt&wD^Q;EoW;axwPF?uMdi zs=PigZS`|6$sP|g_3E#?gKF$V*5>BL$3vjSsw=E)nA;CU*tji{U#5LjOIU>U^+03! zUG3Tjt%KUCoX<1!P2|0p6ieM|$$*9`z~XNr&f?&kg{NTnYm>pjfw{(kf>z{Kc52+q zQWiFq$u(3B*}ww(L<>obKcylx6f}S#vi;{e^1VGRbqW8+lFanEv-`C7qo>!0b2K#G zji%WgM~M^yat2W46!AKK`_Zi^r>T3s)$F_Wko1c~6HLGwSdn@kj!JQ~xMGibtbMjD zLc7*l)hPO9YaS8RN7}wKgB#HK+V7iAMU!LMUC6&QK{te=v~;$)==_KFB!jf4Rw~nk zsxN!O@JWXhYAq=y^EX+z-^Hd3?aPU&Vy!sLh5+1EWl4ZTRcv|Ot!q0Hth;LLHtL<0 zuTcDwnsZOq4ZqaFqOp9mLb}SX**G(uS9Jct_2*dJX$>cKh`G}5VqV6e>MFW9?E}vL zxBFPAu}$nBMUE?V`zmE-`m$2TCCZ-=RNkQ{|9#%APi=2rmE$qtt2^B zESp|^E7OQ`w15WYUT1DDBztPz+3@~}>cVzzVea^+GxWt9v{<<;w3<1agM)+3&CPm! zRz}8t0&f5^AZevxpEU2~vz#Jis%=VltuSy&+$;(X2seBZ!g|wwr~BqvF|i0;ZO*k( z`a$Hw^_w}|K6o5c*fo}2rp-;*Tlc?X)4+K-YgH%7(Lzu8t*_8y&Xp16n#zFv9#hC* zXpUYviK;JFDoL0}LPngMORCn$V1jVE$4ubM&=ZdNX(&|mxE-xt)97@g9}^Rkh7j;U zksqpdbmS)M1z-G=v@!F8E>1c|;&C*&&@wzm$3m|Z%nTUSDlu#bK>TO}pL5&gki73} z?BM{5@}s6s!@<=(--tP<(5rb9gYm-QJ~dFMv+9!SKh6iaH8Ana@$#O$F|=<{Xh1rd zJ+ZAoJzZu^?dIfh2L~s~1(sOZ@;h3brsJZOxf+Wj7zC!K(|jiC4}<|kk6(fizkvxE z7DrdzC;NTSj9?310)fEEbpJi>qx`-0j8##zf_SS17 zLJ1IBjiNi{IDWaodBMzH`Uy6ia7j33uGg<4u{$SVm+stWB|l;Oq^8c)Kuo=`^~=zS zZvjzPxRfpo-^zvGsm)4%h+jzxh*(hCEZ`bxwjsM|3*Op?0MZbMu;5?nv}YjflJILg z%(-Nfv-}tUzkGzU!d+P!6C#sv;&eDdr_?R?Y{U79;1})VwFzH~LFN}219v3){M+9Q zJ?)@`63e@~_eYboHU-vVpLspOP@sp*?QWt3yINsOE4>94FgL_N&AK_PZ6>YnyO6ea z2|bW+PWn@A@j%+4NER&h?mizJ(X(t??u?LhBh)a%pgZqG`=|Wa)2{6mig*1=fYEt(B+WNzzc3vvPXjsdVCy zw77UrTnimZcTX03ah6s-b*>f}b7=x+Ht2yLUC{^7PD@?gWv;|W0HI!AQ2Oa&ZEH#F zr-c;0{hU|#>T1Cx3P>Arkjl!u(T=T^h~3=b2xelEXk?b~T)&PxxINtS4=FgZlB4SX z4m{7UQ};x+qBJ&Y|k?cVXr8+JzzeiN|vhByhcv%O zpVAN&bz~Igcc>NiD(vGqLu9vF@BiV3?d2%?`FWIK05RAg5E2GY78Hb(atn$K;yugF zm+60|m7uYF1~3c9j*;(PH7K0KsBlF7nry1G6%j#rF(HBL^l-axyMCgAD;NdU9ozcH zmKDgEN?R{GXt+M%1#?TCg0rXmDxk4ZFAJa5{S(yf zIC$8dKM2qlK8@EiiEY)qIybRt1$&+;VII_TB`qC%)f-tiH$c;H!-Tbd=jCLW^%Jhr z$tr4twrXM?L_SqRMdFg%pG7zUk(U&qA{B&J;PU>r*ENLz?7X~iZfsnEoSND;Vh4ns zMIw8WeRDr9Ry(tQ_h^?isp7e1j@>CA#{GcN~ViVGcw0?-v*Nky`YUayaUR}yHu zg}2;hwsZvNBdUD`YP_;JOdoS??m&`@*dh#?G*d6!Bsb+gDLrT-IkZViRmlMU#aa)h zbntIJ7~6xV2xb0)ui?X z7+NxjYgJ?3^wa_Wgz>JZZ^6Om**><2Y3rH`gQfZ9AOi?WyZ4RdNhRY=gKTxlRL<#9 z=g?dV(na}`W7Ta?)F-OpQ6X{x*v1Yp+NS;a-|%ygmSk|_{C$vvSxLfl!V$Zfy3JCJ z0A9`A?@Y_LuKrpxeRN*Hn|~T+)dc~RFEQQ5LUsWXW`Y?MvdbL>oW-8tr)I2?!x^A_ zz~xA1oWeGVcGDHaMF^UUC!KQgihd&XPWsKRLLu;+)XvqiG`@=}0g*~HX=*W=504aB zla>$u5qoq*C^faGu=bDn3p@ZfmAZdf?@TGEp>JIC2gGsp$HOYWw8|??O!v(Abl7yh z(Rvg~ihb2V0{hKo&c-g#ZtDA!-Ctkr&@XeN8&SqUEvEK+w=9#uDx82?sTaL%H+wqS zV98&5!FUZviq`PW^J}8?c^&z!fBaUy{#*$_QMAW~pkM)%m)k*y(fRT7=UaC>M9zL1 zx=Y5K&I06OQpv^5mH9JuDXIomfS<>m*0ZhTpE(B%InFphxty@AZtpT@nwRaBz4;?_TsFS-$DFj@{2dX zY#Tk!lwVKd@ASM?>Mvx$Ftbelz`2ua-JSeURGkJ}D26)*2tF(}e4vN;9w4=hsMd$A zHcUC)k=zri%n(^r0PNTsjB9b#l&oX*xjEly?et+@SDU^QIo6pMnU_52k$OlkWI}50 zKfc~G9VF{VS=?LE_w)|o?38;#ArFd+8oY%!M-LYu{@lV$(c(N~ zxBNvJ_2OS?MaNHHy_p|i#|B3en$qFbFKD*Ra70B3Sv^NrS#R*u6Erk{#>6toK;8Qz z)MQRUdpY7nx!YdW0pa z?TgK$&8mZtvr{SPG`XEW9C>(ex8ER!)JrFUpdylkN<@Oo2h|Gehibj1#5aetdH}qmv|B~` zOty)vcH{T-@#1gtnKBuSu$hwl&QRU-7P#J7BD*#Q9S2SJDv{&)RP}SOhyp~`+|$Jj zi)L1f$~sJ-yKdrzuSyW|ZV(RRci4*iO`D1Dh{(MkN?cwhsEcNEzPG6DQFOL+2r$*1 z1!ZU28~l95cDhkO*n1%V&(-lo;s4pKbU;XA=H2SoT$BfV*Re_@?PdXBq~pw54N_~r zm}vzYX`&S_oM4GgVYlepd#$&VDC9XpH{M<;Q7nx46klQa7^3ho1S~9@VOazUH8(et zXJHVNk#f85&Z4H1+Cd-amK)e%4H6*Q^_y!>5j->@gH>}jMCl3{S&oBO$vVz%*$kSF z?6|Gh>!~55yjhLb`}=h`%I6JZe13kFTvpA|*c4!*4-b1f{I?WLxLfyBHA7avioV8O z-=DJcD=viwlREL~v<^W-14{1bv<4s#5n|)^Nc6Ez_;%Fr2aqyDgq@>$*M;%iKx;_2_Z<_CviLvAFH0w^}EdQU?w?Jp5XA-|nc z=O}<0%#ser+5#r60QaJI6#?{%MiI+rzWc6f!B%NCHD*U(u%bDh`YS|^JNWS;9G%+h zv{EYO?z{b4SG@7%_+(xXulH(X7T)ZeKC zNoRyV>K%QbpUubyfFdYmfsfOF^0$}Vizuy9(eIzsR7;|Q!?&|Y`Og$nq^sCOFf<7L zj+`&1mw{Xu8`&)Au(XN@X^_?GR#DaJu2AFJ-i*jdImr=-K({xqGfnd7XF2&vtpztS z)5{yb7yeyaH^L4kyULtm%k)i;LYy!6>!gpQG2Zqb4{c#_gb>$Ss7o7S+Dr$?lz;H?T11tO= zfWUKQ88`(CIyVcoRfM+B<$zIWnB|3sZQ+p0(|XuVJ;wEW#UuB3P+l@UTO$e!Y#XCy zcV7+vg#%O8jLSbu#^c0q0$0PRQ>51DMIn|=lgf?h7BRI&uK-2n%9SUuM#Nb9*0G{M zmE_Pao8>RbBwT6Q@x#os#bf2m+;frKqC^Rum&8kLfke(K>d%?vIO*(`N=Hj7Dze_1 z9K*K{C#~uTHF*AL3pTT&742cKICXsS6sM)c8(Mcl2gg{@tc6miWOR$M)?E2l-mMMs z^k0unpXDAL{qv$Pt+NVQl-(ZindXbv8Idy}LIZKT%2N=Ll58Q4aB13Udgs@_0UB?h zuDf37e%==8fLl8M^s;#wJ>fbxzn=1H4Kt9&iJ|Hv`1%-7m#tpOt%o#Mg8Us1#an+H)4;)Kz`C+5 z+!BBo+Nb(qSJ@%SLxgG>;3;>PCr=%yA|q_fdUZKd1e;h)uHP49#1nN%G;!LpuVlBv zV*f7x1FP?l!W`~K#<^U+*tWR(1i86G;>?ww9#N8|Hn8a6_&byq%iU(~w3>;RiH>pp zLWqMjb|7igT1W7<5GB;mquDQ!TLu5{{1EF$ zrM3;ZlSg{?0dLI`A$61KpX*S323a^404(9HckfmT$2msJTrQqD9bG

80rGtpObw7>Kytqv54yi!D#&f#|u_Icy&294^W8cRK$-96%nOA1tqNWWOuw zc548{#Z%rH%S0&cmj9{RmS6xF^Gs955*V%jem_&xyZu8Um8RA?#?pwd>`C|)hOv6vkD;));L&dgp?kuyO8@V3A8w=B0MXr8 zeT-wI$)a4(?@fN*g?19H-|iCP;$7X{fRcQFgm1G$-JhQHMdcw#K^&ru6EEx>h^N#3 z)gNU=D<~*vZf=fVF{5Fda`6K#b=_L``I;s=7WLgs|6m{v zVk}si-K%|-rIO7yFKGa=sU&o9)8Xu{=t{AqZOSQU{Ey~03;NzIQ75VRB#8e;$bo^C zpkOng<+9B*rvV>ojUBy4q0UX|8=J@oa<67bImQPVQ!MwttyLMg3#e3&<0m(jBN3Y- zOk(CEx4#0H8LxeLvP3v=88y+tSy+}8;DTH ztW`?~IZJ_#oo6ByL?mh{wq@nE=~)Hg<(GLN?kiC*|0DeO&hSZ>zSJNiR* zmeS^_j*$q$3ds5ZMB}{SBXu~D6#0PG^$z3GXLjalb(RG2;I^BaxlW5p3^BAC!XCQC zO(@d9E6e}VPYOUP?@zG5IIMC3Aqj`Igsf@*OLwIsDRFVZ=|V)wpBHLng}>w{xd?&s zr35)Cops(LvpTHXl0O$A`?kRR)4s9V=O!A9L_CHCvlvG9_r}pWo$!;TTqtP`EoVX~ zRD`_#aFxIQ5DTT92&k>60#w|1`arMrV}q@M@Vuw$8YwvpF2rUN5egE6B7_Ni&YowF zRO-!ox*SkJKZ{%U-jvQ#r?ijGh~T$V@E;*VPezk4`~H&@{nsjlzSy)Ey_gE&GZ^Ws zULJ^RM63lSSL=Zn4tL9=joC7`z5^vw(CQkBrKDGsS5k3ROX@me&LN3?MuEOU(Hz{f zUlKt#n;TeP%kIJ+*x7%(sRoBmXR)x)Ecyu0NYjzV5}_K>`gmT>`}>r%Evv?)Ei{Yc zM!Npw0rYgt+lnOpfMCP0;ok-)IAIfRQ#J#?>!2JF z*4LM|rOy!{MO z^Qz0&2JhD4;3{ABPA3bHN4K>O+SJy$YduxH2i)Qj$PrSy?uv=r4~+f~J>7lb0zd|o z6+BuQ=E;qQ@xAAOx;&)qbh!Yu>7Fi84 zb!gtx>VG**<}C~wx?wJUKG>tDn0Sl@8PmQUJ{`OpEQ#1m~uKxt`d zIGek~dZVD`?=)cKYPQxFFmTLZ|6453j=tDL)i7u6eSbvH#$9_gbtV286_qf3+WG*= zj1nSd3Ixp(!SJ3XzyDc;pubiJ1<`jOKYwd(qO>{snp&~G8%SGA9ivrNTGFh^zdWXj zBs+@bM+Jt`Ic!q_OJ?XYQJ2ERP6{aWuoaG&S1L(`0IYs(JLQe^f3Ir_?U>!c>&(7i zX_oWC3lYQX_yRjwBLRwO4K@GW?#K@hH5x}!d#>d+(yuhQgCVRmIjxgrU^Dut)sQFX z#6rWZwurz-4al1a0md3{FM<6V*EnDj0Pd5gE4a!i0pn)!p$8#D8YY&bT+| z40u{zRYd4~Gs`w<6zBmXgccqBS#uIlFD)-RtshVM)(*S(Ft^{d?>+ln4g0^B@&P?! z>Lbf8?P0n$JAVL5Ovn8w%9om&tje4jd`L|V-@B84#pgFcJcs{#6t8F~h4?V%n_h8K z(>%5ujp=`61^CFZ(f{iFP%^6OwZWA@vqgS!-d;X+v6yv??L})L-I8m;9 zkGee2!R;sju~uOx2eTk^ygfuXlQa2~#YTyBZH_UlXX}l{)*_Eb%YN4a;BmBF<)QVc zn})WQf=TmwAF-dIgJHmWXnEy|d)Mn+j^L|^MwLb4Z2D^pBA5SumGI@?`2t+p3QMKP z)AG84N#j~^j?ex;Q0pFrDd1UbW+OeOaz8v0Xv+H)j$okdaDXMlVXWd--IfkW~6QWrE`>n5nU z4qLXhI;zGA(N@qae{)8XIW8*ds@n&obkOXM=of=}gHcfsM9Y7B86Uf$TUQ{Aj*9hQ z(>i5$u#F@56jv#X*l$H8Y`v{d;j$hb4(QwetpfhWkDSHaKD^IEcRJ0?a(Tkm5@ z^IUjvczw1uEx}o5kn!BaBKB07@2u`=Ldql+Xgx1BoDx{{mi|*)1El%P36c$RytQos zB;@Kfxmfs*2NyR;ptraY!``YI8s37BvEM=OJ0Db*9VSRSLzZEkBxP>8qVEkKJD@*! zHwz0?ZY%^Q$nJZgE%C9{@8eIaOH@ZJvGF%VYo=cPSZtGjXAU(r#b-ZS;78t;eD*n! zuSbvGIS1}vL{%D4tp$Ew8W(ZiJVBx)wQdJ+Mu3Idsng_{nJ?YdserB3rJo4Vb3J0r8t3AIOZ%XyzY)1S}N-Ty(xj8Cme42 zLv=ZSiq`>3w17D{S_64+j1Oh~pbh48j5O|GPkS}$8}LKj80g;ssF_?xUwvdmjg3H$ zD&@ZkOJgkv!Jh0Odnke2#*F_<90o%A@%f2oVo6Z9D3xOS_bYq zMBg`F+bH#}*{P==B;FOVlgMAjED0)yCH*CJf!Nc5-)*yOYzdQ`Nb+Vw0jyOU~cQUe3{#)k6(t( z>S{ZG4z(D=qxpxb_#1F(TNZ~F<2YkO-~JG$==8;z}0uh>V{ll&~oBpNP|O1y_kjce^kz?eY?^P zpV##~*6O!~l#g4gmutI55CCle|GFdln!Dphh_A}1n@%8F<7Ztv`@J@O0817TbqNFY zJu0}5Py&E@nImiA&m58RlC7OMrF^qTyH}n6e0^c{LNd2+PwZ*yXCMKXEAY20lxOoS z?pjc03rlU9v|v9;DzbBxmX?-W$Zd$H5%?9?Z8Gt1aB;Jbu0K)&MIKr#gz#%{Wow;r z_cQo4khH7RZT!GDRK9t5Vv7_$-Of`e*At-<`+^quh+i+MZOwmJY_giLC-`kkjt!)- z@dlWBa3kGwgt-m?V=nWdhKt+j>1mBheF}y3%d%E_pdyr z$P#7pf{43mIRr6mT^pRwOpCUC6cI05teW>a{E&phl3QDnI;@cFAJb15i96?fY>pJ? zKp|Ct@=mHnR*nuKGJ5@G7%daiZckK$72-k(ET?3!>F{HGc24I8Z*Cs&lvER2@gsn@O6Elo{L_1BQO+qZAusuaFB?fH5Xy-;(RQIZEo%pY}*YsiHd!%K>P^Kj3Dl9u;}#^UjD8E z^VZ3zpy&n7IBb7ZXv+Hk%u8}6ZLlemX0goQfBouFy-%c~3g+S}+OQZTQRrtDfOn@- zSpa&X4vnLcVGhy+@F!Xy+f6pJWxqEUcQ1-9@XoOQTmsy2+ur}f-djgS)xYhd7>^24 zLn8t*lyrl%zyQ+H4N6LPmm=NWA>AM?t#l(DLr4xiF!T&@Ha_3?J?}Z|{qy(FS?lb@ zTJG(d9iP0f`?~Am?y|k^yma1Z9hX#aROIlJGdFc4!OX2poveE2)@FNb&fHu7)Ue@;My7aDa4tWYoQN6Xws;U}krnkg zPCINkv$U1amvanYlYhS54=iPjT!uWU&kCtvU|}*1VasAkmRi1TS*E_LT@X%87HA%Z zKuoCD@a8??CxT(2m_D$TLnlvZ!0!1{8-;W4w@Yw$2>KtPrU{3a+UKV;UzP0I93+(; zN6$B2HueAw{v$U9>)7mN0rncy-{fz0?osdF)=GWh#Mh9f%_8luy zqHz&RS5vLASU`Q3>u3KaC3BnC(a<~{k+cbCNa>>mr$@dy55B3w4|t3r;lI@;Nn24l$fU8!?l-=SB$_bdXEi^kDEY$rj-9U530da#Op7 zsOjq;M2&dz|3{|$i%S0}`dmnyta3$yA7rmO7@{^#6E%`Nm-pZo34ZUQwFZ^hM)(B zj|hD8QF+rvMuuli3;pr?+eq|?)FvPmmeKwN(eDB462N#CvW9nQt<>KB60&tR+3wRm z`Rb9iU$_r=00Hpx-o^{)&p<=$y#za&E@54;Ia{V=37c|{pA*1DQxbmd@!Ga?%7Kak zeN5TKrJj-q9Z+BInc07W%5-O;V2jW$3;pa}D^t6up=1=~i`a%&7Jo%67 z`)ico@cpv3=Ss#bTq}Lj0QCfdp%XO#-%_7MKeS~0-XprmkGIqZ234j1&yo41`EeBJ zX8W=@xQpL4i~%5*Ut)Z3JaoeSg;TzK8ZvLW?(mcq@#b&di@%dDir)M_=MmbGPSv5N zm97>5^tG*{aFEb`squC3(EjH@$Ow>!>u;VsG?L=d<=>0TJRf3)s+IP~GGCA)JQsv1 z&;g^tf01_10!~}Q_8ms`RcBxt6f$lnamqE%5n&xFD;n znA3q_;oSETyQT=rM{sIbrK4}smMa7A=_-u)hm zJrDD{;}I}Jk&$Zy&#-yvN6W)wNDb9;{^mS3gwX}$gHC7EDr(5LG9|~jzO^PG8cwHq z29jh~?L}ygh2NU0)J;+WEp#XRS5q>GV)3xDn$Bn)v`JO@>$_Y^n~n%KT#RQWSq|ck zJOI}3BZ?E)wcq)nNaX-(ner~3WS9^uMlB~aFlo-7yEs$qfJVA8cBUb~*simjDWkc5 z%9f>WDWOS0b?i!DdsOJR4Tg>1!Z5kuwlz-OjD&M-kgcb`Ib$_ph~l6m77qg--G>Aw zytoz0#b*z}M9%_dPgW^6n=8F5FU|&B3OxJ{@$l9zP@(T|1z3)k1zm;P>gwC5#YqG# zSAllOqix?SN1sDStT@PB7$_$gy?Ecn^H?^{t8w^qvg$V_S=G^P`Z=cVWwPUnLt4fWlKn(n+Ksij5~4ad&>VXCN=D3nZFbXke;LSRD#Z2aW)u>OV95GXOjW z?Qv05%zLoF1xyrAIi$m>5H&<}kqkZm%?r*Ro8=kvTd1=DfL@Gj3mFJ7iO!z?{_T74 zF#Z!t)6ho}!oa{FL;>GR;A(0BGQ5O_04~VmKngmA0QX*VFI821kk3WS3Tgwo(YBWWCYi|&nLyz+?{(hFcwNYrwm;)=Z z@)*+BU~~r@s^xexD=9UcL5JDmjf@#{>VY?{?W4+kg$GpLy?YkjS5{iOyEIqdX167@ z)1Z{jJ7U7Akm26@(d*s2ceCoj0Ri%VziQS?-@rgo_~N8H!pXVOP5%rCNv3-zwBFzY zwAAlr&k!gT4t(z$Yq)16x)e^_UCHZ|UZAkf+avse&2f`b^H8-V-52G|^os8>xwh`2 z{jT$V?V(B|TToG?#uNbU^s_r?`j(318~-D;S1HHp3pCZ!pX$!bC&wt*b-G_DtR1lt z9(nX%1Ecj!_le1W^3{H=2Mx#Zb^Xhhn~%*@+hcI6BaAA#Rg^3O6eGrwSe z%zfjstM*Oe{(YYrume>K+Q{WY@ly&<>>6Z`2%C8~sv*sDFee?@<|+IVSuq0lkZcQ(g94P=odN zgUF{^bj=vTJhO zWf#|Mt&R&59(#`oBrxKK1P?x@d9(#PKkX>G&;>4eth0(PeS=ud1Mslq?kmnwd(mUrbJhWoY;Bz z3pBKpv^l=X`t*?r7*=5yZJ+?7j|lDFPaEDmB?`SBGlfAQQ?R7O{3>TxZB3X>N%LLZ zV}VkKF7?(VeXXI#0uje5cJB-`ZK6iGUveK)&VBnYUw}lLSx2}@zHP;`bMJn2q@!%_ zQ+XsSkev0!q-&(HbSdwfsE+>iG-kCYM=`XELVhyscvD zK4bb!p9M{Al!L42V8Zb`Z7)YQb730h3A|6iy#vvdK%^hLc{pO(P6^0}K4cjbf|tyB zF3Aws!b}QPYczO*SZ3|p(@7f3B)p%kA6|MeVYsE3u%%#%%9w#ax5E6t1A! z`qi{I`K+svzFsfkFtV3vtw3dK1`sV>?;FNsLZT#dqVlxs5d#Us`pryne87%8G#Og5 zS2{9;kR9Y-N*nN-UdI-`!;f7r9_aj3CnLK8rV9zn z!YB1QflF$(Dq8WdtTD31U7-csw7#wp87&)M>beDKnKT8rGM;0N8^ymQwtSwZSq zp@K1JA=$a^+D7ju&Y6A{*Ue-)*bYHN+2DKkpCz6m!X$9}YUdb&l#qSqq0q#^J;5u- zri?iqCh5|Fl~b-Mb^Oo*gvh#%-e#e!iYe_A)cfRWP(ZXC?k6`v8JdsCR(`zL7(K;g zV))#wKONfV&n?~AJd9Wgd<>7r-q^F!OGju;na68^>4;&NTJP4Q$}ZKV4B7->+dq9B*8bSPoRHO-sNPx+XosjShFSkQ*k%{95;cvEo-8XY7)Qb z?^t=oceG!_R^T1ZJ1WTa0X|WAy7jLVHNK5I9W1XQ3iWILL^D&bhj z*kKn+BvN5KHPq^UoI3`aw5-oFK;)}xYF-qH-jvkTZ~}X+mXi|~Ncd_aJ_oQPN0|!M zxnO&wn%_By9rb9QFYF#iz@8W%Pj%&U&{Bwj9a9?$3*j3R;c`VkFN~#+>Do-yKbj)c zcAuQ$!CW}XPF8#wVnaTC6pQW3_}=wlv2Z*z$m@V+i_=DNtfAOZgmI#m;~ZbIDV@_f zmUFT!RBUPE!K{OMI9FCbJHv^5l~v)yBhj>&Yb6L?b-s8e%T1qYF0LU=E5lR#717!KV3n?JJr+cDE6)BrY!gkX-Kb)r~LMvQ(68`>kysfkg{Cc7#?Z?w%w5$l?5k zWAi&^^sUz7;8|s44ez=SUF}ptJi)bLwEojmB!+@wAnPStJsYU&h_q=JDe!x6)AHEL z%u0B?^QR=w%;<|#y9L!1E<3#u+*rqU#$}sWyHO^e92%SR?`+r@-qjGu!C|{Z%A2Ax zD-+eDb;YeBk(|a#X7(Eq>h0M^62W)3^k9#Sj2Bu-)an z0P!ks?;uN?s+IJ*!Ru?d=|O}Zh2RF0f8fv$v2@?JNlrgZ*4G%059Y4L&~@Rm(F}_4 zrHzXB&rj;@zMJlSb$eN-M#=_mxC*LX#>8J-+0!mdG-2?W>5gOEFsh7mIknh&?#OBS za=p}?yv?q4*3wj_Ns8iBNzko<-Nhx!MrAEc~yYh5;g0O0HZg?|VA-=`GriA@`r%G?rg|GhVj{3a{&j-hM5K19F<1r8jNK zD?}Ec4Cj^9BEWgu5Yu02e?|ZuL)l7$bL$apo7GFLs5JdGcT;OL;}>4BDx~w;=g1fE zD4m&}WSJt_EhR{VXJQclM`o+-dHd#JHMU71ZvT~s*$InuH)+;ZHNSp5O1+y6M5f0W zP4bCkW;as_&z8;h?{AUMOXf4_h$d=q1O55K##?=cu41((A|j==91_H{Wh?Z0=<>C2 z@uV17)8ddfp1tYSam~EiaxI4%QHLIBw_$SXrvlFAv24zuOO-2e)0iqRC8MRn;Z}9z zDOOwtH?1~Ct)H>FE$64NEhFX*o>E3RzD_~1`AU(-8RoKQ(2+IzFqNegOQ z`L&20T!)nLW8ahH$*YC*D4Ces&Th||J~vcIbV>{wxjN7HA13^Q?XCt0(V0wSr{`I( z&03nHs=~$iQp>{^#~T~Lp(SmF+w?jG2_GRUrik8Lu7fYw04V!_=b&*oI5kYn6`|Fv z)mTu+`ujMd4PQ6m5to4!szLbom&m;4S^U&1-C^=CQO0_6k^B}zn?^yPKDxp-*KQHQ z%H{#RBYzD-7PHHUHyQ;f3+0i`x53;*w{aZbe<>n6V-MRJYFMn)R5-eZH7sX{Q!*c5 zxe#D^OpoT$Ke3pJyW8kLGVMg8^6l_TNCW+gz0f{@JMVSg`{VPg;0?-uPha&@{Z9n? ze*?+?lRy1P@CF-JNI#kwfKH1Dn5vMPA>01kORQLeJqca7-|MoYe5M$ODB-@23rsqUHy@JR z8@7HhTZR(Fw4TDf>= zNScj{#h1L1c52#g+K5=Iq*?jj(3(9TIuNQwSY=8)!|B6UZC7Z;mtHVNCLb(^;i880 zmYwVG^2{c?sT!K9X*TcEaY2} zC#0K7Bf^@8%pN*}<&4G8^&;*km+Ax0+ts~e9eHW}!Qi^VEUXEW$ORi-^~apW$6fyc zLv0QywKAfA!Bx(MBk8;~x`QI|e*-k~jtHrAnuetGIW$Ru8SbjtH%tw$uY~=$FeNaL zLqq-xN2Qy#Ym|LYAFOx~>XhMLaYcssadt&Ud^WFT#W_2~JrF8<5ujBot3X(Yae4#r z1ZWYoGWd z4@XBW2K%8IbAG=?t!SHC$GWJJhD;3_3Vk#3?!O~NC+50qcHPmjn|-Z)<$&~_72x_n zrSMp_S?5QXq3OnS*;%>^rg95Z28`a-(XI=|iyPX(INazEVln{7vdHq-ThKmr!jps; zBu^@c$9^9Zi-15jY;hK_;ZG#ad98L`+oY!KxM08=LQu8@a@`wig`HjZs~q@hr9xpnKkmT;)nAYyfAQ<1K;2f(3eqj%2Bl5iaPmu|doS_hKxYsKyC3Yp7M=25a z-ANrclowh7ul3gbnz-35+w)CzwJ4%Qi-mRli4F2W>g{(1`W??+`sjGoM?(g8*`j?} z<_wAqIHMccwG^zZA~QvKhUIF#Al)HH8r5EoO3!noNY(UUoC=ls69)4MEla`SS`Ij+%%SO38_eCM}lY zwVJ4?@2tN`Jp3$j|EXP?7V+I0dDR!N!TCp0;aYhzn4i=h@ST+gE@fsNS3>T@26}sL z;5|7?$jTJ-_>#s~fGc@}8}i1^0{PGzbro#{2kktH)cI$|0F-t<8)-k`y?WTt}Z z9kT3ErKOmxy=f~G*ORDy@C0Fn=5l$RjC=-CMd~(>e`kyn37f8QZLltC?=P6BlA{41 z@|C^z#fV!|c6L-GgJ zNj(Zh;H?$bLoX85hS_Tq)6V`eGyjwO($J-fp7W$JMbMM)4U?Ox!NhuD|N&t z+F@$e2o6=8?#NM6k1#v_dZKfZJOMuNXl2g|HE@(_F8f1 zO7Wb~J8Cn|FS~p+FLNHtryZqO;==4ajuPoB9k(MNSSu}h?xaXV5|w1aXt!Mrix9+M z4nGUxjvl5 z&;NSq&%{F6sVnoQ375E-6z2Hk{-GCssg+Uj&jQ6$5nsxegs~nOsA@#8Z}RnCOT#d_ zR4_*%FLj&w)ye5WK^TEGmro72wH6_eqzd#nmx9XLrrr0ej|8;^DwJ@0%_-p`*``Vw zVX}0fzdy|y;p6vQ)1oIRB-C9V@O;aFj}geJZh2UJ$R!AXrK$`j^yi=;nKjKqShApP zD+U-`wu+T!qEKkd3u=mB8zp3{l|Rh#xho+WRKYxZWWX&7)iU^|`p$Oous$WKIRVGQ zix{3rXEkM8TjHORpAlEVxjrc?lMiWNjMmjm=#Y|!(9D>Lu<*ed%a4||NYEnaY3}(> zG_*(T&AD85R-{rWsmv4kte~+}-h%wY57SktS`CHHwG*vk>D5I{^F*AO)jd~R-49hg z3`$eVJ7g66CM%b1OZg2|wKEumGZLHUyFn$q=~35^f#DVRZd*3p6}X!nIXvIWkUKP+ zg)`gS3VS=z{FK28QDFc!A~E`r$L|@wb%1|dc(yMt)FCGGc}#T zzMEXluVhAqBY!uRl%yV7Up9>iiq%5th&9i~v@Lall-sDhTGVSRUQxULEbCU;`4)e= zR9^pOquR$mT4PX>wP8k#Mx*WM98|V6zu{byNLyv;2_q?{m>%4VG%b?8Wd$moiXrZq ze>C=IW(_f`_|$PBJ(xVigohv1$uKne&%dQa6C=u6D^J%uEebkD93PJgLd>za7EtvghYFB5_#R{tG5f%&K%!z8TG741Ifq zkM<$s)xTh$WRx^KFup|dJj&I4iU~her?$xpO@vxE<9E6Q7pD1@&(@=%HQ1nO4N2;u z0uyY@w&71ER7X{E&@{9yRWMJ5A+$bgXEcelKxMx6C`)UYz1zVy_liO1pY%It*>k%v z1$EXaHg#4_?ET6{zBw7@_%jOv+^SI~_s-Z_sdyiD5p(IScPe+#p5%c76Z3<0dqy-8 zjJUrl$LG}_nTk;(hP3;%+M4?a0rHV0R8Q`59aJ5eOXt!^?cg*Xn@2Q+E;o)OBOYl1 z!JIv0z=vGF3}`?n13R|x+xW3(Pus&}hMD*X%ZYSdO$(Xh`V$!D;W|%wIqY9b%ilRX zp_vgXi=WoXWa65TM(RHLYs#n;*?Z;5UsUFI^tDaCAJ=X22`(Cm)%7aQUzI3j4T6WB zC(p(@pmsjrZ*EfkD9Slq7W)StAK`}3xX&jnw@Iqwsm$G4R(fTnr)^$uoqm2Zqc^iw z8lF?E)KMMSL++cal6okNB7B3{hd!DulaQG9)FZlh?Xpt7=z<8*F%!KR*%n5! zWPa)IiCCzUmzKSK!d=WYu**i6SQ)&3xK2EcI zs%{{-_ewj$n#TO$XXT)O70D0m-W*-_GkH>SgnM{! z#fWC<9sE5-Qdf9VcFChA3y>~>2?_n&x=dF38ObT^9Q@3Mc^Xy{D|Bcc$Z&zJ&mt!Z z_`!KcB!++#QlFHe=$eND1F|q7b%qy5#Ev_f=GiLWlw49DS_$Pugs0FkzNEEtK^_vZwQ22QV}Klb zGI@+KnN@xYU>P>^J;w9u6KzG#GV)zsG%$n zR~g2BTY+B!of-E(0U%Zx-Aml!!OH90&E(u6OK=hUy$Ad})F~ zg4k%iDj_RyoVW3Rp$1yS^S&&#O*6*2g#3T<_m*tDZIuO3j+?RB5q&t%(SE-FkKlaF zQ^j*fRsZp0s7J6FO*kqNW3)!Sg8v(j#s-a1o9O4y;{R@`xcin^C1sAO2V1#3>kBBj zKt_efOq}mG>vg02TeJuNnM!~Or2k|6|4I7ce~=oVzpXMV#0LaCQ*{-5NIqb*9+pjj zLCN!@_Ym_eNbPFz^ujJL?9C9{bd4zz<6`^iz1t8P`wY;~$KhLXxm^*K)x)R0=w#bk zQvOT|@dfRZiEysaz2_JPzuAqQ8G(M7x8B=rd_Mb5`qoJ5?72ikKD$r6116O2{y}KH zPvZPOzgl(gnCs8qYAuXcCrF%$GH6;JXR#dmFj`&mA4OaNH zg)5_pM@JZ8A^k2BF$IyyG6CWPivjd!y~so2#6t8yizn+dPfb3?oqmOMYoD7G6a(xD zm3S!g>iGVIPG!-X(Hay#U~(g&3!ODf&SF)>!Y}MtX>$ufp#I3J((Yc@>g@=~ypvBF zfSS+wsoRQci68z_Y2!5@qD$zX)`r(q#s`kk-ZMU?rBm1I!W{0}>ui(L4tP25U#p75 z)I&KTW3AIiLCQ6Bs(k6iW!|fc<%q0I1G!4sH*OM=vxWd*-C=^sc8UjljJr(Sx!PQs ze4MgLnmSY-#22yndE)P&(s2TqMJ%WE$9BG6a^c6z&>gFUnTs%H12CA5fkl1rg=ozw z#iy&{gvgm4JW>E5)0>k@rb^OloqrN5g~l`swD?nlgDxTF7|-`yB!ddQ93o^x}4spd)YM&;&~+sxw)&u4;Z#q}XYJsn(S4M(MdW7(80XK#tX}G z+f+J*B9;El6a2}B@#3bbmt-r(1ehw^5dEbnZZ6A#@ljhPgn76ME zlGDq=K@-NEXONee$-*Ek2Z5;57Suatvc~o+d!!YZrtM9x6ucPmuFTE#YDeP|6`O96 z>;A!r!Nr4q?BHa|!Fg3H$!ZB(glv4zav9HktgaN@=BOT4BiKYyc&$sV!RV>6h0lT4 zF27v+0*x9kK6$+rV$MvYSZGkP?}46YklH_$Hu5Ow7D~2HlQdv!+I)9SP~kt~CqRqh z>nUU;?n`C_G~l+#Cmfs%^V+C)m47$ezq*gSr=Ev{van_~zeqlGvD`80=G)u`9oPuZ zW#N<6yciQmTQ;&>Jh?d`B%^Yun|wG9-7A20BV#LLWb{zHt2-_WF~78vMhv96&B*OC z8$u5^4X&i8c57Y;1)c5GWG|sgR|h+2#difkpGks5|KK)G4pd*2c`8vSlry<{WJYa0 zsPA^@W^b%ZcomOqx|tsXx*c9hPw9sYZPLU|gbdz!{=cvnA4NYq zro*B_;cnwaCIh=5hq0GYudCy9nykUBxoG-ezm;PWJ_yuTk55eysd)K!=Usm^Ao`PL z)|y%1&xG~ZZ{|Z3^bB#DpE2wfS;=EzlncpJY=?dl8cv*IRLy=50>!GesVP#<@dwtJ z{}zE2yM-1!YUzbGxf*T6WgR1;axWBKx)dyMnleiy$(Cu(sz39ZGdWmx+uQUL4xeHy z3~`Z3IPqd#>8YV1#<{ePmmKOhZRB&~Q^W$4wroq0vP2>d7CG`*Y`KG>o@c?K1Pnxv;49MxFZLr;AEAsifqiL*(*WPp>Cw1<{|=;`?| zS6YRgowD|sjHBs9Z%u!sq}?`GeDP_|(;A%SZY2UD8&#-Vem9dkDI9~#FP;h(Zx7kz zN{b-I%Y>VK%IfFA(kg%xv8NrF(vF1>@qSv{Le{*{F2fQIn%U>)16r|-3@pK0Ax&Hli|{z%~d z+UZnaR|os6d?na+VqwOttyRDSkVi$(NML0Vq9*pI_=>_w&a64s%xj@L(U}Os-;c3>8x8I*Mw5~PgyVThAVdjNOLL5K#`-c4N_WyYI zw?$}ZVt|vyf1mz!7Xz-|#sG-r-~Ywk_z!UW*PZGA|LOl1dmsw%9+FCLG`+|gbDhz* zp_cva-Qt}18gSQ|nwBV_HxS;mMZ|vivRmtyNlmiB*Y=ht z32@Q&k{V+f1fnrXDVJ=k*KNl2dWy-eoNRkdHvx4_PQrp368 zZ*bxpy0L|RJ=?847&aGuvt8`!>K+pE+H4*X?NWO^c%5x1qM^FN#Rv%6pf~N zmAN?QC9xTJz*?IICO4?Tc#9>kQO81(cM_#TgQ`*b2T1tQvsj%9&pqAMMz40z$()T~ z^b}>O8+5^^8z&M2pE18?AI?CuI4E7;c9`fkc3_79q0dzL*$>&w;@uDUorGvrA04!iEldOhj0R5m4jvc!i)yj3Uf3tD#4iWVp42mrlft=CPX z`nnwkZwj2{P@ZR)_>~nKh2Ubzk0~iqoh(sp7M)R-9EHYbdb3`vQje`%T*|w7WI`n% zZp-mL9ouFK@2E@0jwCM<&DeBBfqbgE3O@X^cOlQuP^_+ZMSl)G3g!)|Pv~>CFRl;! zaBdIDa`gQ?+G`HVPR<1Xq#0Kg2%gXzOr7uBMVoIa8BU9w%$R$zjV&WOzrq|iIub(O@oIJmJ*@bfc#yqVCWAH_FT1Hu$aU!WX7MQa$tads5Z&F$V!oiH z4AWL=A6J>%jPQw_DGr5w)H;LRm#T_Cjix6URyK|L5B+8q@{0omY4J z%c)0lUq5KNxUny$EB`y@o3*ta(_>n^kFAyX?3I2u!V z?5}#oBh5|>?`-dxfX^k2Z{qJjr#?K>gr}~j3R;i+F|&&nPuA_=9_}-5TxDGoC@YyL zeZT#3x2nm2o=wj(Q**Bh4$1nJFV}qPJ?eYHHs;ZiY9@@=N4+vF#TN_TEkTNS91B-N zHHO>kQ3tNOPOB6AmK30?VZX-dbKAD-O%W#eF{(hh(cpIlXp!vXk{O&!XLS>t?8Vn| zanr^xMc87}ZDW~-aK3!O=7xJ!x|a_1Y~+X?6LnRBr`&ynyFOrhBl=fm`m>jg=5(vu z?ry3ya!W+1yxmMGRA-ar<#!yaIi_brty2EpFN4$gj`9{c*n|xvER5^x6xs?ZvmtoF zMenFT*a=jN#96TsaASq7hnqcxfdZsPzV+VbnUsn0AMXy#(6Pn#O>X1A-f(NH)4A$r zHr&jPw&N7EYJ*JG>?E6}gJDVS<$6E&ibZTkxC9mX1!aCRhiY1Ta7#JVdLmvj-tIA( z3kX5{TZ|2wCyh&8W*S^EXlk?X4*l~rpoiyku=#`62iS7PlY?P-3$l}A7d$%h^;GQ-t*&7zInf2hj~~7i$e&y^ZK=52i(amFF6-%UZv2^#&lSSMaR;RO{gte=Z33X zF)svv1CND^TniFLvo`JvyW7G=b!;f`hBI;u2$FA(UjCD9-KUgKZe6EOy%uOU|J`w^ zp3PBI=x&v8hce3qaqGV9JueN3_P)qz+-f`!eqRff-7_#~@}2oGJ!&<)%>usk)nC||dR19^o*ko1|lVOcO3 zOrx#Drt4LU{YmNSc0K*}5ZHoW*{+6~7i@_wk6_0Qd)`*lYCGicl@%aMpSt-e?@{Oe z4)9pru-7m=M>h+bD)rE%IHWLx^V{8wi%D0s#24pP`{*P%&n!G8HXFfzFJZH=oMYMB z23JaIBNnm|S*vsxdN>|;*`#|*E9D@EzFV;}EOL@cl-BZII5Who8-lBUzV8RGYS>Mw zmouWsrpJ@VQPN?b3e1*cs^l_EP5S;}`JB6?!p^4>f8(~V#78qE#ON?erLHl{^4Pub zIZ=0yNmW+SA@r9BlbMZP=2gbnN#a4*^RWFE02LSQ4s{(eMy4kT{>hLKn#$6>`tb_9 zCNsl|wSt22j@2jm6fIhH09mA)-*=3Gst*T_q^$1Ju1@Z5_w8(WDbWu#D7-$}UV7}Q zqIR1pR#)1Z_p6yCgvh@NndWSLy+aLFH_%an*qncXH+7?*pC1UcL6=J|dpjH3iSY@4 z;p$fi`+_&uGU|um1}z^nHco&46P_Cq;{A2`Kp6t;BBh8ZzD!5gpkNN=)~NPJq<^Kn zr^07jU~9GZ`{0^ArFyD0DEz#tkJjCK39(*hr!v}fn zmic~fboGkQV+`Yo!3yu><5^@)r;EJnfEP*-22{TenLz7!$#mYDAJXCbWt!PC%o zDxfPy7H%yEn<$xxkNN@0s*1ANoCW(`IE}F7H6ie`+Z->qBs~6!_O=^neg+0B?Wrp= z-rORzl{c&`ErxHZRJ}5};_&jH|FhAw*wDDI*OUwWcTk@}N4eG4_F;5L@yJKie0pD_?}FJ0-Z<{`X!rsm z!stPCNdMOUd*&Z*H{jTxuv+~~qdj_-?&pRVUujpOX~1}pG1MErt8KTu;?>3I+f&Ir z#BE+WCDMAbroOYR;bRzx1WTfzP@gsxvY%51{Vwt)7LsT zN%$O693R_r!>W4Xpd&v_c#+eQiBukA*>GsGsU_^0$DvrdKCroPU3l@*!>?4Jd#T*4 zfA(X`81^0%d_9(JPdMGLZJD zK`s|jRF@$fg>y1v%%UJMA{6~Be1j|6$4$~=K{$+TH=gb0Om47JFc^Py(AR9UIvMT- z`BNgKippDSzBQgrl?Li0hxWU<+&KgXhj@?r&@agyc9?&!KMe9L4!--bU7+RYHsK7; z9gJ_Vqy$|JMhoh!H%mW0{iRx^bD6S%T=9QndVVbuee_MK?~f9xQs(uT-(}Rhw-Ax# zPP=!;g<~P;Fovm%4pdtTNp1kjxS0+y$Er8%1;4CWL{O4n<~Cc^jD2$xR6d~Z^Ly_) zzR2jk6O5BJcY3h0#Mb?<(V1lG@w?mLo;_^UpCLs3W&zUgW-1bTamN#TfehRs0@yxA zW#=HtcZNEwc({RMtvOyv#7^){L>K`ch?>JBH%AN z44@;)f#=}IkOrMGrtOf>KwtRrD*gY0hVrM&d zKnKNEEVyoCc#KZNJ2<_T{`6qqyvI3l7?+;Qs2_X}&P`&w$h;WeWV_I2xJq}698^oo z$>v=4&kx))2gSm=YD$-$DiU69wI>2DH!#H>_^tZ!JII)g^T95aQ)1;$6_F`eP@}^| zbpvm%{bt)99GjMc%?MdH{*3Z|qxJu7^6vI&7@o`7!7Ojn08GebTpHqE9Fp?{#+0-e z|2wl+ey?e5^cPOsjLx(;gn)xf?Jdo-O%SNQVP)46x+G2=aC1xK>_<(xmY-+a{JOFX z-h7Ps8TiYZxMgDNcRwkl+xQZ)QLArBxg%q3*Z=?uHf|$2kg~g$rjno4Sx4qOck7-$MJ{U>ZKr+if)WeU?RwSN z^#y;e{mcRL^2}JBeULO{BQF-GI4dWGP!{4m?{kX*ihXsSRX;8)=%Mcx7`^Ama$`y7 zA7Um{(`vpcM4|LMb8)G&joUwc-dto7axoZ}vdoh6T!Wbb)iWr}a3Ms*vOTzLGvnkj zeM1&1uy9V8&G-qgIB~UcdG#fACLlIercd~W$5!P4IZBK>Uccp2ltFo${gr4bp4;h2 zT;vcM|Jf3(!S-1=AcD6dpRPTRL5h6`26l zqGGbx+Q@4$*v@BnBcyCHs}`FS-c*?MOKEaSWm*xE9~1TMZxGW5%QOI<+CB2qndQ?T zosYoE2Q-jXhz1{^hJ-M1VJvFUYQi|{8B~= zrJwX*gu74HgH!`6ucx5cv8FT8!G?3|Q+Me__En1ukC2e&G1@<|pZg`+XQ+~dpHuGg z^_yXI>IPSygYbsaiwrjwoVQA0VJ+8cHcgvr&3^jT3LcY9NaN5Q3e4X=EWCS7{>G8& ziqXNuva^-glWFX#xQdVrr((=kwDiSczU>z$9Rf8MaDi9iPYcn$@33 z*cIBcc!$^FMlA}y0=8^f%86d0|ED?HI^dTNEoSm z+IbW&B?!dj%;g*mm9s3L|ugcWF(!_$4C*EjnpJWq3uVRh@Ptz@%FEMZtC zLE90_CQOvGo+nc<**x(UIrBnWXtp^mQk**F0fSXHuyr|YnhR{#6mhs7>dgP4jo8mQ zL^(K6k1rI|x>9^Pz7%S}-bpIRJ9onA5;E1y*b2{F^|m}|IJho8j6HKQ4ha$Ry4!=> zL{|p3fuv=6&)zOI^yVmXwV@usDkG)B{~8@1*P~AxPR$Q4Eojb}HI7I_a^#Gt zL+DkUZ$7vgTOE8@PqiY`frf+-SGK#w4ClM0I9>db%plBO325R;tV@fo2~e_Dk~iYbgFPc_4Tf>+vi&m z*M&&r^zytuk4?4);gqt8wfu}*vfnke_0Xj= zBj0XJm!1kd7W{NfeR<(s__AT;bESgo$YdYqkv6y=IdAUKw%5$qI%veb)k?~_%oxUDP6jJ!|6|*7>u8|XjR^*4w*blF z1%BMF#8Sg?pM~_#n%&kFH0M+dkhPy@n@i$(h=_I!Y!uhE07EwngGx10y)tWf4`3(Q z;a8X7(YbRCoLYA%;GHkA!@tU2bNiH zSg$_I&lqg;40kjH4-Yi2<>euv>!KR`XL$}jzo!&Wa#?jFghH!*8&svk^Kr3Cv zB2uIwSxHGZFlZ@Rr9t~*1KjRx8qm6g4#+8T{|TG_8j(z4oW-P8(%1k z0AGTDf;9vT-Ys^~qeXZ;i4vy_p^reP1~7>3m?3rZmJRu@>j>R4bvq{yn3s+`ZDaOK3_z=<)~2=h#Ypnpat$Y}U%DbWPlIe+aq* z6wV;v97bLD&VyoIU$ zW8R$YpcGCGH@icbzXn-r_#+mx&HG{;SH+e z&g+};E1-VUYDOY0O||B8hIgAM z(N&x)dc||MCW{Y)fRZ?gmSCXWw$1-jfJ9Qfs&K7B`)J>(a_wj0BLUivJ5?!1I?`}@%HLhj=uXZBlGi)xaWxl(u z`wa>m)cjJo_@3bA#d6|J z8)`;Mk=hZyGTZS}D^$Qh=)}>ht9V(%>q!L{b`t zu2Do7TDlpf8)<135Xq4o8p)xD?wL6^==1*GbIw0-J{&*k^Pu;Rwbx$jx~{zzt}7T< z_@>D5o;q6=nGpSMFKJ(rkr>i>d}dzMZ)-63%Xrrj3>_VvJU)tj(}!x6be%)bP$~3~ z+e6)=BIH!y0g0BjC8%?72F1fv(2-n87%>sC|3L?0#yYyz!-J%z#;C|XwH8b@F!E4r zuk3q~#6(ry@di#N6!ndXXJ*`#6l0z&@rKjg4T?@EP|x1LUOVD0f(;0gxTe+MjhU zn{)Z%#~|8jRiJE=uVD{XV6cLn*WXh(GZvj3?5A+ZDsR@wrItlSf4pZ<99!FTwBFD^GBLHbV=9D|^)qUj#SS6SaNGf_hYsZ-?6ifJP%b#+ zM9Vhu({X(ZZ0!VV5?`GtdlH}vZ``xo?VqMUV-d&xijSErl zE;CoiS{o@y%sm0b&%{0#RdClH`zAkfZ%FWHwj>Gta_XRuXLf|saW9|3Bc=aAJaS8N z^G=M6)5*;I@UJqlWN6!TStudA92T)|-#WG-slr!bGa4 zbzXu#aVCFBzBXIVS=4@k{$8_S?0Tf z?2ZYh$}w2a9Cv(r6!ZGS=|cP@6*=Dv(m$6G9p0qWAU((_?Fq->qKCeE9yqh(uMLANs6YNwlXsl z6CYZ1?4Y#<2DVBRrNg#09AOM$47VZ~Zr!}9@QT4A!=kd+g0Ck9(vOaQNA%(?v$7Q? zpv*+jdpSkqsINY@R{|x1kT~gv9fvGR7{O_;2Itf)m(31uNvMUzm8D<~(}Uj8hx-L$ zPG--K)KKHXFV$aGo*9a~ci0RVNJ=F+jy<~dB4|pJRHO;}+fk9C*7R@bffzO>+ff*6 zir><(J1GM4T{rJZ=sT#4^e7^Ux}je)gC#Y~```<>&SD^*6QO%+jRfEDgm5``LKWQ; z)rY*KQ5j;v*h=Wu&;bs!LlG4hz*J~&?=eb^|F)OD{m@MjXS;DdFAK zr@7U0-&1*~aB1wwRARTxn1Lxn%5!>&6rMp|)58)d^98pReP&X&Cki8@K3H^-X>DdV zXP)GmM_$Hw-@Y-m9$%^p0a`&Ra5DCC40w32Y5vxq0{$yy$h^3W>cM|nfXmz#_`}b? z^?3m6g1P*y=>+`t|Nhth{~!JTEpLrq^1Cqu=PiQHesSf)M$b_KqDS+SaE&dcz9q}tO=@$LdfCT)+^ zG5gTw`aYSUi~6RooWE>bEE-d~idVYBEP$cW2$^GtoZS{RXr~+%OO!e3VZ$4 z9QZN3uS9CoQ6r#$wNlcCeS*@#X(MC;AGCv?(9~`qNDR;gytR2}iQ`@R971=;6L=WQ zYu6F>Ktx1jE{aLjo7w|ixI?LErOc_2R88yZUzz_}*Bzxze$N-^YllHYXPdksf9!(I zP9=wZcix*(et5x{)N^h2P9TvN%o5@ACc>O%fwI#}@8?dZ5X^ZAl*i1$fPLCQar(q*t(-l)iO~xq!_ZkRrLoEX)gpau#tuR3S|{o5JwyV z-D-){Y<^zTms1u0OojTY>BRkrneN;udP*33e3PQ=yps%$NJ>pT-KatYixDwprIP1= z`$7`KEid!P3J>P^P{`4!*~k5BHq3mhXmgT!5Gq{vggXN!Uz{fFq;pu=>7gZ>nQYOt zt?8W!U5FIo5)ZK)pUJ8~hg|hXfsx1k8DiXx8uT z^Sy8NQ6IgwArdbZ(w;f|pr8HoVu#)?UUBK3&=k=Y8w`f^?WH^ZQ4o`j#eZAiG2b5Q z96~gk!&e_<#Dz?Wp-WO?h*XwLw&pha>YQ^mT#GK6^~DT9Q0(NU?5N9+O%HCpFAP)a zK?XEfBjm`)2eBZ%_tRK2@Nl)TZ)H2wG@pt+q52e=9|B1)3M$P zEfzgVJ<%MQi+b`R@}Usd6l2hp$|#^-;VUoaC3@NGcLIQ?uGEh=*B-R`9oIQE;nEV; zLm$Nw#X)i0iBEsk|M7sj*bf(|&)sJv_~?EP6l!aGKTrSSb6!Vi0uIpZ8kJP+2jlB5 z>d-n>Hi|QktY|*%VEOR4DO@CaX8DH84gF4_{veO|anO~=c>>R#1&amoJRjWw>ZlSBhCy$|K+qRH z{cX%keSNAeq91twKEQ@$1UqvB|L3`lo4Y%(LjU_O@fpnl-r!dG@dkcW$pU_5lmP2S z8MR>@Ll}4w)sae$y4}b*CG{Qe%OvPJRoOP+6M}U8F3Pt8pOuT!$v=vQ)0WzK`g$|= ze4@_ClNesKS#jgkdp22s-P#b(zDaxM(ce;rZ<9$b3ff()Q_R8#HQQ0N)3$3MLl|FS zdye*4x=9oTb32;_NjV&M63H?bMcn`VvgjQ!Y-w|d52(0SL<0M*E`Z(?IrAv&9U4r} zZ7(r6F}F+QyaRguClh*$*8cB3M)WY9u791P-w*M7wHto>yt8L#S`xZ)?fo85M(?}Z z+%DbZ#U&T?{J&|0%;dfALqOdYj|}b3S|-&-*tPi*S1~5c!DjLsm`wF9{cKs{!RK>< zFVCfUJ~v$~SL1)nb-w~7DY}~$ZD&$ka5mY$fN(0CF&Uz!RC_6p&WN#7eko5GEFWJ% zLL&1TCLhnE9Hz5VN=06ieouGInQQ8@!WWU|{6(ch!0<%lTf_%;Mw1Sng|IryKn>lC zlNPKWgkVv zNnLDC$Y7`T(?drmv_Sx2C;c0j3FDb>KG?Rt{yy!erPC(wsexY?xt;*d)zQbZfJFBV?)R4!CgKb zK0%>XLw-n~&%ug-gWih|;V*Uw8MTMzpU8KV1Ge&(m@EbJO^*8S?8>-Rpb&JIPQK9g z(>5MD`1Xu$q)F=^)D07|P@C6@W-i_y>Zu;-BD_zeelUDK4*6Kw=yzi->TgJLGn-B2 z?3eZp==p|T9g;hCWq;c-3gqtzB34h7!el=TQ{p38(%4|3Uj?@5x6xO`#Wez4IuwOL z^)7z0I$Yb_k2}{&Yqp>;K_t~i0+BH2!C@O_g{PeZf=46)@{a8N%Q)<9av;|Vu&JHVpR6%ca&on_6@s4L ziyB>Ac4;*uBg0F?VD%q7w)NGM>qn>FOl!_3SCOari;GEfPxV$gngag8$g$P1*%EQB|n+H?B44yC$B{pf6?(6ErE*gt+(vLNSNMSeYo zB51~miisqY(&cR-Q%ZGMz4>6Em)k80G8~buM^ZV%6i_qvxqum;HJ7(D69SH%sa*|4 z9zc?~IqqA(#KVip{yfxo_q9i?&&R!T`lOv1?Bm8B-! zEI2slUf)gaFVew0fS3BNJ~U5o5nK=zTTo}KLrxze%W9yQ@{=ggj7fy01s>56b>ozs zCympZ+g?_3W`U6ai?7>62UH}{>M<6Pem(*p1#YVI)e#(l^yAXkCJLt|=ZnmdSv{Mi zG8)~?q?H7Z(ys>rZuHju#vacd1z@FMmk@Wf)p`ukTJpqI5R}K?Q8`sb#u0hmLWGtUX+-o}v@-vFoSHVn%(oG8J z7bF@$kbL0MxL%by>KF~$E{&$B)vnu;7~pk?kAZnf`8f{7JbDUT72KNTG;uM-8jZCbWza!vh5Mxcvs;a!)$<0l%u?0Yz zG4#j@tu7DK;#e{~qn?rkb%xhGL(Md(=C`S%Nsf@7g6`fhHVJj_p6d_}_nh5(M%gL} zq*5m&5qM-co_Q~+^L3OC6SbXcFJE#S?R~%pl`i*{J8ZC@bsf})F^a$Gie;h5{0N87 zp4AN4Xcgm+@p3l&Oz6h0Gs&KQRZvji7{P<1LtLAjDE8%HK%Dok*=klRpuo*{`XIS7bqAp8~{G;J4ymC{U^+T+kQ(U<9^%xTE z${V><$S4Kcj52e7cE>ug!CjWDX5XXCGd80;!1=fnl1c-PPZeGfOa-h9wK8?G^-OMc zNH_1^8~wHUj&OX?xDCx$Z+ExIH03;47%(XR#!qrDfT0jt(2DE?S^4d+3|u>G4zCuL z-i9~`3}zLU>Ee8w+B%4N@1(S&P-@7t#Z3$VUV@qO;W3rH`%^hi##ZdgBUTw}_j$Ha zg)xU{yrlcUsh7Gq^$&anuHk+A=&U{I>0ywVm=@xpRP=&3-u)`?731F*VG2E}=X8sG zJUey-j;uYq@wp-Q3?L-VOy!wlf3MPFU@*RWYiL9leMa8Hq!i&FJ1>}lo5}2z=gFNP zw1BGzr`2|Iq&;c_aP@=(^z3e&-Nittz6NBnNue?6eU9p#Yk#whMDEr_Z}(=tHgKFhZ^3`Xh^EU|#9+^JRkYh>la-@qf|h{-ziG#Z3vT zD^c^=so<8l z?a^)URW-0YWE97wq#201e4umF|@Fdrw zl+`_YTJNJ`S)oo+V(E58W{%r8S~jJbv&c$!L%n5^mRL=X^;ScRTv9?CqbAgvVeF%_3X361sz`+47B2WJl_ef{QF zYt9|%aUVwd?*g$9b))!wLZl~OXZW-U^v^m8WIn61#I0HJyJEHC>WW*&3(z9Cm@PzCH}3nW=4t->5Kkjb3h!g>YszEBP-vF&w}mr?2&=34kH0Weu$ z!3s-@D)sfXbV|e}KGA~$;!)Grv}u+y+*d)R=ZCqZOSki(Ig@s!&r&S4JmlX4iRRlC zGAHBT*DjnuWC;*2VL#L~XQPH61O$KnAppep;P{w=Ud6$&M*sQM=$l%uwNrn*Uw!{N zPhdRjf8|Q>^$zH*9gnmOB-!I5p!LU!imYo*jR-8l-@1}7@^D{G5cy>_7i6!L+`2}t z#CG{Xwg>-xaJ!^&YO2)(DJbTz$j;x&+`K5>yl+L3+QM6U7CFr-5g_&BWefXUOz|C9 z!4F&LQSr|>T8VcLuUb6e=LemX6^{F~*x6>fd`r4Y$xMc0WOkJVRSdQ$x#`u%|3W}e zzG0mWt2hbF0^yh;Jiu0}W!JB&pGJzTRA+tKbJqWzh-ijmU#g(=t%txCWGSAzNuloo zvAKNzMBh&9l-Kc4TdiZcDMt*=o2?d{t5SW(nCA;i^CX7f$!?qTg(8+B3bYcEr7vui zU;fhX!Z{QaeW?X9G#}y21fIRlTm-iQg14wMXWdV7qn9x04zN%a)RD1%bh>cH{w1bC z8-Z*6o;oeqUd`gV3f(8BH5)%kFn_kzW4bIJ4}ExcuEq8nK?;~gVuuFB{&(rooky`% zSVT)Okh(OeI`!9O*G4A1blPccZCQsms?j*Q!k9FZQ=KOlOU6I2o3#F3+dubpYU`h< zZR@)q(y0bs;3q|Art*t)esog2B`y?O6>Rfv9u!0w$D*`sQ-o$7#Fsr5wY!$Wji7lo zc~9+NvN-ejKy|t=x>V0+8eC=TEcc2suB+PbWS2n}Xl(%edcV_UW_Xc>nSh`LlM~Y1xwNNHsZr8w>GkjUk%Pkia=bWKDVa}|8h9L za`Scvl{@8T zm4sScyw3Uhauk%zkwz1ZGElr89s*vl+t+b@mnMmqK!w7z1`%0l-pxCuE%|gsf~#H7MLK$%eGdYPsi2&&1v9Z#1Acp@V34D z&CaH#PulY6^wBs$k9mygjS0+5W9z&)1EyHB}`g)XupMsx$;RTvOwBIiCo;`))_A(+(^=NaHhu5{J>yd~xa|G+agXNR zDJPRjlfiN=mupkr&&#UgyB!)v1l?Pmb0dM`x8VzP==Y z-b(#BD)HaKL=B43H5N5}Dd6eMi8(O*#^K6utqk|iX_$|zn}IrRCPT~91627R-irJS zoyp}`_T8QDnT-A~nZlK*mjp3pIgFIv7eadGnLPFNFngo#7^^-6`ta=l*FvIgt!*2V zCd~WvcoUu zP3o;*PYZuc{oel;-rLWG50A|Hc=*p|zxDv^1+*c8-6x zc{|)HubyL6vSv-tdZ2ydv<~iVVCB8^nS_d%J>h%v+-wc1yfq-Z>Rh+g%Q+YTd5h&% zu{u5V7%&?a{~v}6D<~=T&^4^9L(At+)|L0mfzA0zfa9&5nHCRR5>vOFuM~dy32sG)b#IHqez zY1E;}|5J|e@eL8(dTd{Jx>H$m*>Dy!Q}#|Cex%1}kS8yS_%ECc3?**ZH6h~QL`JkZ zsg+G5PpZa7(W9Q_jlFD^f#le1sBEO4&y08Zq1?sCx2;Kk6y|m4e=NG^Nd|!QWLXEl z)iL|))&|w>4!Bp+kgiH!RQz&}d~%NO@<;8Q`9JiPZxD{f70l%e7!>@pq+#X}f=0}G zp!u%g*9|D$&P5}k&S+ToY0z}X$p@eZxLI-&*@2oPrPye?%}abpcnhqXI*SZQ_gEWy zbcwcrN4Kv4XraDy=n>KFGa-lCNPPirU0Lv=YoneAc+F*I^!K+D@$%AyBr*y$MaSZa z)8(Qi_x(B)`FtksN{2Pj&W#qt#JtX3vWcd(Io}0`q%DL3Ci`AivYdG{*kyG{_6E)a zbE}Iwi~15fq`Xt>b*hEn4mende=}7p}hP{$jQms zMj0(2g4*;ycrbdnW|g0|Koy+wI#yZ1JYV%mA3uu))$k*iDG0Re(Pq{@I6ARU3de;jwAVH$-We}ymi!e0## z5wGUWik2VCfoAl_PksqwWZ+ZN(<6RaBgYxQ|0VCC-VB zhi8>qVTMifb)+)B|1&pkIGQvmi}9>t3?iR-WePOUPjv8ynFsqgmN6gsWExX%!yz zsBD{FSjd>6W9k-SnR>$o2re({0zl~{%{kx(?ai%_Xvmz~Q=q<`e|vYD)#A#{y^VmoirmT^ zw8y2yKY62e#vay9Ohd6vj)98u#58eM&)eW-ribbie`hpMo66F@qFnR3_2 zqD>9s3w0wEkP6A~lEh^US_UNVC*8+Y0%02ub6Gd2T<_#a;+cBfr$2~>H$P3#SJaCsnl{&`#CNG2J*}F&U$HXNiNPm~g9}`=1 zsu*gq(Pbb1qpmd8%mXodl=|7}wMcs18n*@vFcKg&{)puEb1UdrT8gdd=Ded>#;)4D z@5S{*5+Qh<;@|0N+1(U0465(ZjZaI!6gfTf2R}Q1G#Jsmw@NQ0fkW#;PnU`Ve^u=2 zN8@HIcdsPAyQbcBNuWK_)2FvtSp)j@!YJB7)@qpoWQ86!_s%Ryi680QR8 zoY0!!g3HscOIhgW)pn%B<#5r=9gggHUs@D^WPN)Hi+bE>`Tg1v@P06O>r>@^R~_hF z%WUL!+I#bapAkP|cTQ4&(@J)?|8*UK3GR-Sr#+;b{&E7Yi7?eMUrF(?eV2sN-TnM) zPkQ8GbI1HbS$6|hh`pGYGFwE)PnKs+=}mNL;tTp&|M$E-dLvwmN?cyDg+rO1eK+z+ zsTYV3i7@!*5MNnhGEx$8bX7tgK36B9)TCr}D&0m$GSm2`0D;nANRv5}lJ(rRo>}Vq z>5W*$WXJMWH2xKnR_Cv8+I?Sjpz4DGp7?^(lK!64XYA2;n3U-6w3job9NC1HuFSeI zn_@?ml7Qp!@)w%{cvM;tFfW(Xt)#jLU-WZL_o_!CQibYY(uxgI8^ql=B`{WZvVPs2 z2M{9m+jp7`u;+3kqqSQzat}1Rf=a^cXq&@hhz0H1JJFIhhXO*iOj zAT4>j`iu50GvCZ++TGSQayFQaI=Uoeln@3)9pA_?Rd+NhV~247p0@O*Z)6!Xx-2U3 z!Hn`w$t_{`mpXjrS{KQ~i|*6&AKajlN|9!!7+FTzgpzuH*JYMqH}(Jd{P=$eiS##J zhtrUn{Vu^HH6D($=ID>fyUyeEcuaZ#vXxIes#>S^gQx3uSkG=8Nci{HGcP%n@S#rl z%t$mPQ%rkRW^$G`S)MH^nFdpXhjNkBQ{300j^%xTa!~D4=~mSUVWfJ?_-~0BPsOo6 zPL8Gogl>G0!~^3Cb$a?@*l&lK`ny?3TgHWuQZm&TH5|Z7^Ey^{cfdLE6(*`HK(tLV z;#&CPu^CKZfCHXvSfC5Hb5bL=2^BM30}c_ zE8m{j+bmUcQk7k1c(-CUz#5L}=2H!wtQxk6q924c2fL97WWC_N!f(@QK_ zZ?Yrye&%78Aa=8j7|W8yQ3ZWn@?0LU?FTnkRorrxe(*<=&1eMV-glt8c5v6$?gue| z_`ik!eU3!p%+%*NEiL@+(7oN3BpeG%xXzO&?SXHd!~cDkeH|3wv5#(y5|c=0BWe2> z4YCsiwej3#wVV-qDu#%E&fHSHWb6AkgAxosXz z6V|$w%J2}fhLd@3I6fok-ynrjV-)Q-0%X>iX!T};S~s75fJY;q7WiJG%PD!>P|>!O z_A$5B*3EWN3rkjxq7JF7zY3puKngGJ zDt~=2?K;Q)te##V;ng3^e_bT+aY=M-gON!K8WwM9wahx%=wA|4-!SH;;*x5_vFfkI zm=K{F{+oRGfB(}kqv>Gnp8Nf#uBa^UIF-tGtS6a!Xm~Kqfwo0XlL3X5t7UwEKctO&2$kvL=lV(h0 zP3d%Gh9eVo+GnLfHn3FkM~7tXk9^M_@lNqx4Fa%uz|l}%mra76Y!W^;eC*$1xi=an z)ar$D3P_MGjaPtO?_Y4OxA3VN*9btiA(hh%o;^11GCF6uf)}`+{k{*q;Z$PlsyAr_ zj?XM;vJ5=w9(bG(!2;l^OxpPbo)LIvzzO(Kei3(4f3DL7`V3M<@cD1_K6Jg?sVd>q zRM9p1d-(Un_~u^M((O|+NQ21J>GTX;8{R_a0Z{K>eZV~x<1MxOlMT(-+bO5Oak$n` z#j_rn&__>eD>6K+ZN9#IL!B$Z=D}c~)@D=e{$Q`(*I{~J6!i;2fK-8Bh)XhPRwTTV0nThJ#!dO}Iloyl?%~!$oEEYWT2MMU`Fp7)i^o!<_VaA@Oo>|Uk)PKW@4e57 zEOOY->p7q?QWFjepa?%ZXc@;{sQE@ZU2$4csyS&Gs|q_ zq_jzBWp-UK>PE%L+_>JjpW_b5aq6q8P#s_km}?id&}W<(KzbfHc2<+N&?-D5nH3Wv z`y|8ef-2^r@krvnz!>npefzs09vZjX!mT5ZZw&O@#^l}3^7<-waNc@^tQJ-q^M(ET z6yv_{%&cJ_o(s7*_a8s<_n>ZSxjySGaP&y8kAUaIAf76$i&@J#O!}TZJj&Q8sBsTx zq7v9mQ{>N!^5WW7_1L={>xZMce?AK3k$>z zoFF!jXX*=#oT@U1^;|0eG?pMP$Sf16=RATR5>?j-j?v;<1=7HfG+KIkpu<6ge!0Di zm*+4*%nJJlNHLoC`LLiUqU(;nJb^46N8H1w_Vp82pezk+wyj=`Yb$l^=T4GeCd&u> zpU_m-74Y@HIXAdeZ1jBogV52KJd4}#daL(Hmg`X-ioT5UXupb3RTe{t*Crs+xt;y+ zr`w@3U6k5OMc`_xUQceGUNWZ~8 z^U^xPm6-r`dWl*P;3^lqViI`971n67MW<5!XAI0`v{0jiUF2$A0HmuuqI*PX2I343 zUC3WG_1hv$BgcBr$(NMZ_-|J&LCY6UBYw#~_1Q|g+xN)1gp%me8{6NqN24g(*T_3Z z*G3bEJg*1?%A#zcD8w%vAJeh%iLKomiHE+z0Y^KB-sVH(s=e_%IqLGEY@F<)cd%n( zNFtxuzY0qA59Kr4z@yBgQa|S%mA&$mWG>54ShcxRL!Hr7ZFVYsjJXHh*!H1nql>c3 z&MGeQVL};AjC4mJ&`A;yx!wFFns_VgI2t#4P#+10f$Oy@ZEQz3zqY7uKr>T!I9hjfOu#V?#NAfSMYBP9Ty+r%%&crM8{lx+ zrEv~@YOzu-dckqBFTW00DPS&#n`3hAfKyS>59T67j<0^K`q@%Wt~Udk*Z-j1o@obT zy3`50VjJ7|F}1AXuXJzuc-(s zelzlGJZxbTkjz9;un@F`Gf9KNe2D`bme4Ve>W~Fm2iy0u%WgfQQ~8cJyOlM#)03(4 z+@zWL5Q5+f83?hhLHdot3FnJei0{AYIR?BsoNP7N*-TBb{1Bz6`+k!iAwJR!oxG3Q zSQ0W!juB=?Ig83?4@9=xb!Pn?cjpA6lwRjqy+|f3oen35pXRPOj&Pz@0&|t)>I`jb zk<};O?yV-ai6jdoT5P zqHImypO$Nl;*$BDpl6R<)b(|G(Q6bbDC#Vr!zS$3fAW=Yf7tdMjq1B`r|W5lTK1dH zx$K6A+FJQ}h!Z&aNdvEw#a7#qn+IfZ3WvZ=Hln`tvGD3CTF>|(N=Y>>9=TSpP+XpY z#WD9`x`Oj-Q#7q-UnRB=P;#4-MTXNYn$bQYpV6ev0pHgtxO&uD9PKBxlr&n{8Ic96 zD?7Xdr85STGYfksYJEo{{?k5Noyh2elMlc(HyckB+xvt4Vxf~LIk4VQdqnJ(l`;Cx zfG!5_O3CNrZkZm}e7!U=Bf3_Gm^W?;?BnF&3j8mi?RvIms1GOZn^soK`iFLl()lj$>^+s+98~KEK6`N%@ywdV zBM3+FDWEqv!1p>#&vc0d%m1QScqDLNiejk)G^>vpPkd^+C86QqU~{w`z{Fmx%x8Ef z{INVz16gIvH28>W)#=k~kD6 z&hKaelTk^QtzOQX$IAL^Nz$~g&9ZpLq-K3u$dt4BG_Nsf$~}b&j(0t2dIYEY6o9%( zb(i3}CFf^!X$1-|)BW9Ld08>bu8{5Mg6dwqS0a0*18zMd9nmdSt$A@b)z3jdx3S`9 zBctJO+Is*M2sCvmEWfMR0%^$?zbH2s&W_O01prcJqOU)0c>aGhHrFlkA5Z5FDEBG=5+jZn zvfMYC)pwX|H_RkUTm?K%uF^O259XXBJaM!QzoGf*8cw0@s)jiBljD!m} z+a3iANsfKN&4gA}Tr_if{=YB>x|C^5Ka1H0&iE|cb8Tg(W61SDaVKKYFIt-8Y;*;;X1r{YMP)^t-7;z5wFu z887B|%_ps`FHXZ;xrdV~t(<=9U1c0lbu-V;b&UKg7LXJ7aVS$I@D&)|%+k8lMaRf8 zPw0)G_v;>nq-C&fNPy$vu{`C>4*fQoz*LdAvd@eF9Vn;jXLaxUgNzQGOtnJ*c22N{ zA9^~Y$=_BvF{^npp~-3D*xNW?>5_u2ZN_;ohu%CLD!OrPBVbDZJcXB2ai zR4U59RX%yLET#?7<)ZQT3DUM)dqE-&gy1s5oS@0$)zRkI2Cnecjhl^ph-xQ9O*+~; z=|xcwS4O6>&rT>xL!O1Ozn@bT=^*i%6jt^`z}O@?D{Qs@+hx9K8b<1o)VHcK!d?|O zOaYg;1*dnId^z4UIbmW4xx+*?TzO-Ge>o`!mCJm63)e~}jKpgrjq7OT@ZX4c^$+-} zbLQ!2dNz+=HV!qy{;770{PV?Xk{q{^D@4)fYvG3@6n3C5^{yH8!ua0}$~Z$bPz`K4 zA1AN~aT$6XtP6z=jEv18&pf>c-HYZ4HrpU~xRyrl3%EIy*aPx%1G{w(NK<-(k!fnn zCxL7p?T3?flX&2k@I+PEnK-CEVwm|Pa9D79QDan{gsW;W!4>ycz#uN2mzGp4yv<;zUv_d=YV&i_nY1~vm6@G(G72t z25Y*;m2oPhNp>eYDFS!oaUbhw`I@MI;Fl$sgKXvww5%zo2OhYDL;ZaXh9qZDw1I;1U1 z;El`@gyLz#;XJwmtnOBC1czczKI9vFj9XIlJ=)*knq|QN^>yF=CZgdQsKVujwAR;u z<(XzcpXKXlRN8d>_+eu}M3nYdwC3YA(dh5r`XJPxvH3kzv@`r6;j`A}@)iJMm~~lG zsgFpoU@b(m^(elS54Gv5%{DAHl%P=q17S|^eZvRW8I>Or^h}K^x=k1DTEjwmQ8cZNW#*qkWWoO5B=G6UtuEmCMRrb=i zT0zhhP}8fdVQaqb$}kd%`-2bq`t5qNNvl~~@b^x@^E#7PjEv?Ev(wftj7T=dZu-V$ zybv-aFf{tjc?mYnQDu~s0w#me`N6Z$&)$wiP}26(|rX9 zM@g<%Oe|Zl5DRM+Tl> zRc|{qflL?G)Z03-7Nll55igUJPVbS)86YroSX^P@K6k&L_PVP_aMeOm4nuH)o5IwA zN^yrZvn|dz-~ot+hQ>w9^1Hq2nNIcJ{{XLwxR+J|rT4*DQ+*E|rx+?zjEQM9Il2c+ z{Fh#sJK5SeUrXG29Fjk|faJ&>zd7Px)n6T7T;Yy`cpB>xvYNB3hIZpyMNhbWS+ekr zj<=j>@NbN&1wirU0I6=i022Z&EVw=VwPt>PZckiVi5aZD3Pif-^&PTh+$jAH0z4T~ zH#v_RjF0p0e-#j*vBX*DB(^(L-1cPy&Z}Q(5tVnU%H;nlsJ!ag$CKS&Mgdjcgnnb^ zyo{KEWIss#Y6lc;N3@!Si6gk#=1@2NZ1D?z-`1|NA5apsogrznAd+9d*C4n5712$< zb*_P~A61iF=`8=YD(&;xNu+skYDQ$nmh$@|VPjo4AHv7^A!mJ^BS8Lu36uv@-!L>^ zFSB(O7Ecc{^YIJF2NXn*&b*X^B3+ze=Z+t`r;^|e07?(CTT=OGSG7%^4ZKx<)as$U zmJ2jo2PQXQat;qcj*S1dAVhRaZ^lzCtme{+Z7QMg9e3&+r@g0m%q_eAfM6@<^9Qj?XaJ6?Q@UJ&zs_~@2#)pocF$8w7q>s1z`uj! zn7RaZ=xTS>e*)sm=x8LHUg>Txb{J0gX4=!C#whPcny9!yINDNC*=j&Rd=HJ??C z`!F#fT66(kGZU!dxvfY_jP#x|K<~!1e(#9uu__f`VbY09u~v5-acbTF-a?0bk5m!p z5-SxmHN=FLpSP^mSjI_>jJRLR=y=PNb-YR8lcDWT{Gb&pE z9n9G|_?^%vf|#+oUDhmc!bYt^imiwexY&{71XF8ZXGqxdd+7svcn>5eRugTMzxWul z7|fgD*x$Aznoz?0XlH5@=wg38tLd0x6CZ@L&&QSUhJy1H>jOFl)2iCEIC@+u#1Hne z5haFoYoYF7*LvHDqksx~Sw3Q7Z#&TW-o*X3Sp0GU;u;6+W?S)*w|up3qsY-iHGikZ zbTZN;9OgsX&lzlU$0$(Z<0)~>i^uA4$79}N1`+kA>ux8M z!2oen|Fg=Yk)ub2B=o$A!Cd|DT968`clL8C(Gt9S8%vlc#lw_?kiA|EsF{FjRL^xm zniF#0z>tO}n>Te&QSoVh0Iskn3FBDfq)bgry!vz24_8SgPCRhr1Gl3tW*H=#b;fm1 z>5t}lgoG@nf;Bzn#^!sKCJF$G>(AhqR4^@24)ox=(t3|I;^_eVfXCAf{gB%?td$oc znurN8yo|}D@)Ija@{TG&yaZi(p$MpLU#SlJrsq2LgV44$EKvJo;h)JxRCxXDb;jo< z()Q`9-j9WJT8{Hm9B#cVU{7!UvMtX2N#H?ZO8*g%zC0HOj*4W32mEO4XV`n+%umq> zNArb(>876Ax4+l8{}B#=#vlX0Je*1;-P=HIFI8igGgrsuQq$o&b*=$Yj}v;S91H}O zrXuU4ub_a}lYlkQ>sfcq;=bh8uZ(t5kk8KpMCJ1@@)0sDCwu`Wj#rJQfl4=e54O_TIzkaLn()(A(A^j*;Zb z%7d?{e>{qRC;;I61i<@ECqPEwi&&+_MpB=I)ytebF4cI<$W+SoJvRLhMf*3a02=Ns zk(rszy=VpGhu!yhmp4tB8Ao^jGyfHJ8|+`Cnmqd$vyZ_Px6O}GQ(To6alWjOjAOv z0UjHe*MrUt0P1dHP^6S4l40Wg(q$TrXq~ieeS+}XA~l`RI2-u9{EhnE!eM6IW?7er z0wC3|=YN0yVh3M;I`$OYnAq+59!v*Ec-ro%@9HDVy<2V02`S9RLBRUrJ+Er3%38RE z{!a^#o!8g5V<{3m??y|k{mcW%lg{o-G!K9d7AF1uX`_7n%L7FmF&Rf{Ey?|SxneMW-2t@^p+t=7<~v6h1z2!G3L z*)r`egY=mh;`atXLb)ry=*9IfqA@L*_I=L3&g$8pQSDT{C~OSgW16?DOk|yfFU}9mD81TVV-zBSs7*vlWo!Yi+cYFE3>WnDgxU_V2-5 z`AGPuk6V&6TXR>H_GQXV?cIG~1#H@4fQYD@Nz+6zP{+H7jg5z8N~SgI zY&_4O@M4RNqjHY(r4a4=(X-sQY*Y3RD|d{l5r%>#!)dFKie^LXXion zX{1cs9fiB_>iH9MHsR9tppqi!SyxEQ^$O$7asV7C?^nt2*tj{4OP*QdH*dy(8uYR?YX$$On6jdV5Nag&sB&RuD$zD$|S(sF}2G9hJBXUqkYCEywO5LCZ<-CD(s;4vGY^yae?PbWszkzCkJj_hL)-84`FN?-WfuRP0Yhwi<4wE z0zQgo>0?lf^X%%k6EE-A&3{J0iIp`@_M9EQ>NxR^AJb*jk4}3JpV7L<%rnJrx{^pG z_4mr9O>Q$8hATN8|Bz!!4b!t<&GEpDH;zjOD_S>SE~EI1AXuc91z&}jvw4MOKW^U( zKA=|0Sh|F)>XFQ0447s4b4>wEq|R|R<{&)Bn2HRrYE3eM45}0I$eBWM)8=aaOY0Ff zd^o1sM>nb=hn!vGyt1A2mW7vQj_1)U^A+^l!Te!$Wc7-tHO5h*|J3L~^;Srp*CBG$ z(TFYD)T^e{VCe=vX?-1aRn!1LStDXZ*>)9$PXQQVMe{|!#%`*#UBc-g2vSwpM-h2@`D~1Z!FtOX{iuD z|1ffmiY$t+4CXwQs?Nq_^000F)~c&K9wje8-zYXAZm3oPlUYZ2@35N%TTkwFX{WnusQhH763rUGZ-u0_) z3|+EAioJZ!maRo^{r$$a1P_cH%~bypPQ zQh*=Bk&VcgDWRJVf)f*obrXqBty8#o9?yeIc|rkjUXVSKq%Hsc7WEOK)GE&`NJ96O zb;RfN-K+3tgVxZhiqI68n@Lxi`kf`H8{Phkz&lB}4`;iLu!E&eU5NUdH@~-?7#?wO z+z@B_Q>$8GGvDm(F$=PSV`F1`A%xE7#j2N$LC><;lmXk6sL5Olju<$nVXP8sv5H-f z07d4FK)4gl>@!}*HAit&L-ny_RzJ)x@P+`VhxVk?$HUgm$gl`Fvb_iE|lse8ocw^zRGktLAd|+q~+nYlVC{~qPYajJgeAZT<6>b~xg>$g5 z2;weq#N?fsM0941n)RQ*A|QMco_a1WF7n0e3D;j=a2`wM|E&WKLlD{ahjv7}01j{kbEto}vZ>G5eB=6DG8 zLB=~?@lm6-4c5#|ZEqi>2vcUq>KhwVb6ZE}!pV?wZlkwz+9c!^I5`EUpXYU}Ps}0q zyrT}?*NI`hR}O2>HYPsyZT4_I`XohL=yCQ$9Pk)MMn*dP31M)2o*OZoxq-NdTW%3A zLUEG7K%&SKc&yj)AFdW{$5rKfX&{QIomxIAhTKvpV4XAHo`f6`*BUHDjT?`@5EWx( zWkqnk;0HSDbkv%)b=byTalNsD9rBBkONg@CDYQl2L!SMmid%HieD$zz#rdiAnNbfI z)Y`6G070XpWs(yh&ytWTJ-sDVgbOsQ?cRjv^@&b48f5SH`BPbYIQA@3Q2N|F%9S<_ zef@*tu>BV6<#rR#oQycW&tq@@B#pn#a%Co7&EhP#KRR>ra{ZAxv$Ehn;%5J{@ApDp zK_`ZLTOo`G!<-XQ3%kCRch;R(n3e!Lj?kg0Kx{|xo_UjmcrZh z2b6M(`Lm?c-;pD7(}GSf$gqF@-$?KL;jmQkhH$EhArK<&TC56rDnFJE?R9*m4O#V0 zYngYFZPrB&biY2g^ixg2sumejsTBxNx@odqBb|On{9v=%mPrDU@+lblrjpilIoE?Gay z`wwZqJolVm1Z#Ys_jCLS_VnNa=A*lpRwNl14&$j8 zkdDLL;b%x)yk>fKJ#&B6lq*_a=}1lci^*xROO_r{I+@|!cPFOLHYe(z@7+jb;L1?x zbba=wq(mba-C$kaC ztiE?=mcK23r||J-SwllMaa55m-vSL4hURIa)3$@m8_%wghOsE0eD(_Kpc8bZ|G7#+O4jY?Rvzxh z`|`Mv$w&t>v2cJ|&hN>l>RE01v{Kr@IMj3ZIU5KQnCy3keDfX4%T5JH^$x@#`Oa$PR>HqedyB~v6-i$x6@}NCUGS@jJyU75X$*iE8`WMOtg z_(kljchP(@pRet>aMBDWA7N{bd%p*Z9^8uKp$V!t-}BO>g}q>_#gR~K3OIeV)po`j+7~LIjLLsB|M&+addNk z98is7ad^3PpH>_=1+?{=@G`rE?s##b5gjA@@>*Ah_^=pW=d#v#gPter=nMN%{doyV zdP3bj9j~zYb=9NGn|lYvcbE4hr#Ul&Uw1w{X}?pSaEKev`x_U=>G{Lf5^WVd6I2SwOTOH9Mzxzw2C6HWUa&u>c?+bJK% z;^_ViDp(U}XKQ!G^V3-5msDHO=rKI%deU3L1Qik$XH()dF_PrFkTS}W2*Z{nNlM8@6Hnxy=>wqD^Ysa+`0?0Duy>VyTs0i+G9O!) zk3ahA#sZ8rJ270jw-|FzVNsgi3JT!gDZze9xNOW zAOyqPcpM#@6cv$t7;~sZd9Pz#L4@DIW3JMI=LinaK?cpOz!`%83RN!1WL3V+I?@$4Rx^ z%Xvy^dVwJ=Qm}Umy<{%UWD<9@UqduI&1ZI7sySH=&!+Qy_xCvkwdKb`c)qXtOKn4< zA>Gr+tlE5A!#h*-zuvCUgD^e&%J*GS9Ex^(s~Z0OUjO_2^t*8k)uZiUGQcK15Z$DS zV#iHst848N`!SG>icE>aetuKx=~7JAe!|$DU-|3!yQ1o6lWE1Td{1X3t@n3XFm)sI zI6Is-261%!MrBV`89EuWIqS;lIQFQdYqYJ(z5%=^o(^|0_H~XC*5_=ehc{VhT_f|& z;z##OUyUJqpip>|q^_*q&AOnaB_;VWj}Y_z*j3X9;f6!=3W`VG{?=DT2(Rb_@K@}1N zD%?wnAkBNtGP;tO#yjQl$AU2#Hc9ZP6?i$Q&Y$W8Dv^oyr|@YrYxn905-Ws*SGm#? ze~79C`^O#Da&o;-C8|+|Sc0LLyX|IPbx&6c18D#H;qQmF#qy;xzUm2Zwa+v~WdzN= zQ09()F%AIG0jNfy{_a?#^+E-@JwyHEv~RS}!)-FaG}e{E>ilGzYQh|{k(k}PFYis5 zYY8zZ|KOhCbecY7W{A+^qZh*-%sw0{p{4A&`_1}1kb1-~ifh=7cfWJT<|wMRf@Ws4 zo6BN{B)YG^>0E95>HDj*)-INn`#!!g3gYH15YQ%oA03Ey{v;vI-NIsP4olQVhyNpO zU&#Ho&B)S&2x%>1PD{wb!9kjg)@^r1S+$aR#BAtruQjrva|K}>e^h!H!HKPBVBYlp zeEzn=fv`cnqh4CY3yNv=8wu03n0lHACRu*?G!&m-8re%bDa)NpG<=xxlP?dZs?5hA zEj3LnPYLf^`rb`Sc$HA0UiM~OC$XP1hRdGFXNs5UVRydbK%P&WL$5+8DJgsL#>Z5t zbETs+H(&SBimqvC1sic#q8f60u)@e*>PJjH1HhGu2a(@n6CrG9C7v6;Lg&0^i8+s8hDC&x}gs zF-Ld)uJFpn)+GT>KL*n%3rC`p)&5+;qMs#l=DsJTcG_8r_tTrJ@k#@8>5c^x{he{`Zg|E+H<-kLeEGXCmgp;O4Tb;%|5H28?JRKiZx> z+^)rs*&SMr2(SxZL^Q=rj7O&33sAP%7n{l$ClS@rtgJ<{f0w^U!uzJO>h7eNB(HI{ zZN;das@n5t&7$xVjjQQOKP2C+*5OWUks$$UDWqSyf=I6l zqW^PumY$L*H@dcZa*oKk>gy`dj#{`ltn<|3M+>^0)#Fc1tXjRi>g@1I5-eDi1Q?(n z{fGy$wJEoSeVNaQsa}gpXM*LGoPFTl_B|)CdBopp5?$Co?20zda+b7{OCLVmnW+Ic zuHmr)!`-`C%LLveezz+^f+TgZq!L9%dswWGrLUz`%D~yZCrk7F`vaPLny-9$OMM0c z4i=!<$hlF05EF<>xZJN@_p{bY?Q6+yJIqvl@`|E}3Bo|@DmHupy5~)O$YvS<{YesQ zt|Yq0Pz0EGwqC1C$2Z?$_;~Ij%Z;(q%s@P~%2(H}_N{_FBqHkZy+C41>*FKkEJc7+ zWwkq&MTranN9W6?^pSFbC$JjRrFABhHx&yE`OI$FOGCH#c>gzNAko8iyfqk|-#Bwu z+Trc6JK?aEhtoXbs<2+|_CC_x>{l&!GtGSv=f7>yxMtG7ZB)0DL`cWZemc)cw1KmU z05-$B)9uJab$6#=vCfNOqXtw5NmwQ|{LEp%zV!qGWTVGg=&S^X$1PVD)-ChOUaF-> zo_+Zc5Y>Ax!41RG{i5wtzn1Vnm!V``%INy21ESltj4zo3}2q-Aoi1O+CgCV zM9|f&q5x*Dty#Z_a4zU~M0ew;lf_92oEScU*@1eEwNC;q2l0mr<|8a_VsqI^lMX+M z^A2`Z?`H$i&Hdm03Mr(oOHBBG*176zw0*kXzLmsyldRSBHiY9YzmAUlV}0k44zdzF3Wj9mK^{P zG&l@syOLv~1v>FU!HEq!2h?M!?I^1K93b;IS|G{G#iMVK+1clc!|1lO`&v9Rr+;Da z?$(>_6pJYeN&1Mhuj-MS=C?8rzY{Sz5@*fc`*-H47zocYNZz*JlJu zeR^8HkbkYciSjBZa^EBms*5c54b?s!*fw;KoGPTC)7WLHHu1VVkq)-v=fpisroeG5 ztO}JH-Zly?RH!x?XO}80cRTT^Y&3}Ak;5D=YvY=_2-Pf13S*TuzNJ?j=;ExA0*+nf zioLui-L>9J;C3{_>=@GO&3*ZxJmdAPPk)3ZS6h$)gO=3iL-|xM&aW9bYK!;Ihq@4Vm(u3m{< z=o!X!8b919y?KP6$ciVU3H)qHR2kPpQ?-yq7x6h8LB9?SLFae{=99gQP0yw~;C4qO z>1COkm$|Y)vxY}iY3AR@hTLjPyh_zDyPZq6dj(@vTN0ZF6~!s*>}QY}NBhZT8sxpu zFf@O`8E=2?#)!Yuof%OYoC>4Pb8>atss~p?QheVog=FJ>QZ5HOzrjc2Mx_VTR_ALF zE6W@M+{!x)cMV1|TFr;$g{89;o4TTLwPlBIcp#xrLE~Ky_x}C41qkE+%phtZMWk;2 zd(15EAo~-^B^FY|2d9IQMu-NrYd02|IlYVBo=*CHWND$(78^$d6NQ_Jwx`nEz6aGj z8l&Bw!g(6&qn@@=l08ZgDT(+IPmXv0;D_r&_2H5zw#pD4Er%~Zma5y~!iAkrPR%N; z3I^{`p%xQjxR&JWxkCLeC7;+-q}SrGUOBEq_~eVI`TENQ04-@8$yc|`L+@0jA73&V zVA0iUSp78p-K%oAF(6Hl2LkA$h};O~A=CYd9ck(Bznn00%byhG{3$M$GrKtvULp9` zaiZ7&tw#fnqNogsN){agXit#KIX*mcT4byc^?}XRXnk5;%i)5Fe7xl#47Z;2rF7?> zXWK;rt5qlztHZw>l5l+%=FTEo49p24gfBQ5*DW2u;?2jziSEU{qY*aV+{vGvc7?xIue z^tKbZl9`#=-u@t8a%`+i8|3-BHmZsJIgT|7QS7uQd#{4Ex{0{bErPJfYi$&~ClINE zuUw1|xSn;_AfE<`%pDP>$@sgC_7`?l#k%YyBxCk!2^AgOgo;Sh4Tgie^1}N?1?tf- z-@^O{^Ruw~?Y=i?wA9rpg!bolYf{<(1`8sWXjRSp7X+HC@j`T(rx?Tdo;H9-~PlMlrYei*yw9Ik0RABF*R0Lz2{LohU&<#XIF( z<~pfa$olOg1z7kfHN3HVJ81FFRMO(111}BgBs~zSvp+Tx{Hm|cZ6=UL#ECSlX<)HN z(!?Zdc6awKov_3EJr`+dPN>HqA1xgn-9KRFAvLV7wdu(_=cIhJBAMsM-{mc{Pa>Su zybewgELu-TuEw%HY|6Q^R* zZU@`JI&d^KB_W?-IB#mjmoM#vlfuYJu!V%i)Ev-rvPDy{04qP*el{^BgiC7X09#r{ z`^Zvx_0Ec;q6%*at=QDzF21iz*Ou!+T1TyMp;r30=nJ9ykDp;Ifcxm@w;@newfiNW zl{3G)Gd=!#D2J!9^Us^oAXoQ6`)o>{H5CS#QLD-N$jzh~5zKTwY&;SDiOMvT;|%e^ zzH^PJKOH;0>m@B2ker8)@q)o6t-d@b^BC@Y$KBf2mW%-v_UQBdQBMo2D>(cv~CP%H`tn?AgbccLZ_!p7(aUgl}Rogm$7p)N#|^|EtRr@h6KmNU%#t1Vuc& zCCurn?BN|fZ%W0huqFkEF};;iBeyR!S(OsiBz_)#b&D!}TvEq8FJdhyf};F#63}A3 z&v2EGt`60bD*Nw&Y*B04JN2e&L3rCZW(h&qc@8vm)L!tml)E{VR07-JrCh@%PaC z7%D^jk~GH+jt3e3d~be+YGK>8mX)>UC3=(SXuGf&FL7S6bL`Y-BUy z^l_>90e{d%yen}moY?W6xWJe@$J~JdetgSPBbG1*14m&#q^QmSX}_(ca1w`u^@3Av zcURZ*6E|=uO%^cu3Pnt50g8}EiV4T<7iq{?Z@v5;dV**_D`w@17AsxCjai;`2ZUZq zmW*CX-*r7mjRW$R@^q}{ZXH^aPGoQ$u_sB)WuX3rGk{n0ne(gpfra_Zsdt~W$h%@f7Hh&bBq_l5){iU?H z@0o+$r{}2^naRpbSw0(m1T9a1(CDiej_ZS);P51V>7`0CJZrCUG&DoXZ@%*F)zVib zzZ&96x06l7{}5r1_4m^-#2z_A67}rL26qm=>X^uNb(F5ey*^9%1U%^!$fjX|PCpHn zMd~4L+VKIAZ)>a6q1LN^U3ScA(Mg;TBwEDXBI9b=C>6}Dx0&H&Y&Cj|_UQMpb7){0 z3Sx}@HHgScs@-z#S=U;C9*!5L+mgq8u`|C|pr);%L0JO_j`NzoY*OP=drEmU(1>F$iI?iL z*oFw^02U;{rzrO zyBJ`?xFGH}R>EYqN51A0m(t?->1;zbhLj0dM^}R6>~BHiF9YZDd6?67eP~?fSOrlH z{n*uNVFqQUkecNt{|MuiY|c}QHCPkPE@3@McRWsI!ltTe-QiJU!i{NetM|4motBX& zwAR`u%f(<3amEe}8`Z0yVXaQ>nMZpl*{CFe^XeQ^#&}wvvl4{#n&e@xeamw5l22yj(ciq!&^&KNvsctwZ7Wg**0fSCPT$| z9t8UNEyaPxTfaA$8MLM_Q@=%3-t0&YJ3IBo(433*%%!nBuXbhE$(##0`80_YZqEVf zg^m^?;JND|(H-j2TdMr(rKRncBWYoCuhZ?Dszz)iC!iUJnuHQ#c%I~dJ)RI9F;jLl zZRsp3$o^gAc2lwSa0p6&7-iC*+MQXfQBLn%NEdwsT{}vz9Hmi19!T1OXU`NxM0=~7($>{dH#O=GO`!YPhV{%k_8 z)fB(r?t~f}ThwWMCV$@|NyjZ=2@eA;DIZOWxQO#^oM{oSi|4DIyT2ULwya?1nvg=L zmv0F8Bk+r?Y%Mt~C)79Bl25G%SQ4!o&hWxm6!OsfpY;nkcKw!OK~k^Nr^(4$h9TvM z>d%4p-0vTaiKZ!uq#v4^w(rIQ5x^oDrW&b%uf zTm)kHEX99oj4qh1@7)}XYLS=qcwj9zCWwv z%WDbmu_@ou9a^Ac4M+P8HDmlCtN20BQ@LZq?b~|lmK(jec?c73fX99&{X1QovrRBF zKQdndW2F3r`B`80<9q%(be8YUxbVirlw>4?5x87-vI-Z47H`=`W7;P-X{;C+eB`u7 zL0Ie0^{JX*`93zkAM}Ah$d3Rj=(T($u`PxT<8qz5HfFtj@SD5?vW1pT28)&1TZ zMVX!`Anf?k%HZ_o-788cO(@g((hq8(;nrG5TkhGZj!zbI2i;{+ry~b_v zV>qW-{Djzru*m%U;^|Twj|r@>ua4BXaf>;0WUIjP5QXnWfB&ZGt*u~n;_8OCGSQl| z^+Annn|Y<=%0cnuiRyXbCQKWu2k&R6P#4qoOB+1?;K^mDm6kB~bbB;;qc*P?bHy3< zVxAw@_h8=d#I9w_-I^rzNF@wfMA_z$zUyC>?n>LPcC80en?1+0B-iqGO^5<&2GAsO6W=DFI&JPghJYjULzg z3BO6^Ey&IlkO|=vQtW*~Ci~8hegkn0Hmc-2Zq? zO`_|EJ;wsKR=DG~Liq$W?(tW)pAOuLPS&|_ZL!9wO}eRUHq;={|qWHme@#f zpV%*Zld$k>cjQ(a^Y;t;`FxB(hLcgb))xnFK*dA)qTl9;rJVXG&MoWzFTij<4oMft9v~w0 zaT5<7(y49<8=MebP-`Pr99d<$cRX6*`sL@i`d8&ezBqqWyjov|it2>$#I(nW1g)N+ z#;gKaiX-INKM;1=sXB{egT!wA-p3W{&{|7zT{BLRpTHphG%~oEg+Ue56-?|R?wOz$ zn#`k%JY8GcfBg_26I^U~A*69gcY&dT?i5^QweFkdiVlx7eJFl$hW-{im&P2JmoiCI zTc>!zpj=YYDdZnj5#m&tqTi9PE+*q{on zMPL7RPR~hV&8_Cp6lbg{#^NQtl{gsIXIk^SVz`*lTS^zr<@8PyaGk;qJ!A7qn>{OK zAE_m(KH&$9c+CNxOsisEL$=w`)eBTyA6b&?+WSfUmyQT)_>U9f6I`ZQUSYrfCQmGi zpwRFc%vl0#3XSnjqF0r=LR+o| zE885q6j239x|Jx__hz^py7HIEq4jZfBc^w8M=z^`{3OF-OIN~sY8f6XN>~tldl1ReNxhBZ%J^Vl!wGgAiLlL4FsFWfuB328EH(ZkJ zD)@x@4fgiDN}q`_9_!>iy@2H`re6@k9VK?MxwYLn~*MZox_p zQHjkY-AA;k_UIWQ=ypJVKfq3{Ezz+ZTJVHtDg{*fk(dRUPQ zACFiKv=tiqWANsQXz}h}?0DQtWK~)gd#}>62ZbCmZTse{0g!n9)mz_>;6AXr?M-L6 z5x?p&meNk-1UIox>vlI07@$sRT@cGR3q1PywuOR8F+j2)qPVG*5^eR&;C2vf*rv2~I8$|EeY0lv$AeAI3EV2r^-~o9XuyZL(nIhl$ zYjRol@ye$}rY_O&C$zM%IT+h32h~7MkgE-PhkFu5bg$xEj{KA!t2b;GbvG8Tk72#; zqJVUHS((FJ;Ab~q?anS@GvqK`^lrX|qXVk!WrfM)+v2l|M!ttmgP&8=m4n72f4?Rm zjm~em@psndFEOo{kFu;adc|kkCTiBxD$~u?n2UJ>R4R5%da$!?0gYe^FE2X5iu#HT zF?fD?V|AsA4E3fd^=7^2yt(yOo%4i;N@E?g?B1N$+^m2wu6Wg@bU6L3QGO6|?aUaS zQ;RX-oT$wFH6zMDD*WrMyuT{j^Xiw9@wx+He;qsaTeyMRe+YYIxhqNSbl634-Ib2~ zi+M`&qN_jPv@V-4Ix_>P=SEGWmy3ks*w%Npq6GfcfX94fVL9&%qPLcFwV25Qa5Kd3 zAT}{@T)SnI2%8r2=?D62e!qykRY(F12IOD_Y;1kJ>~Zl?CR}T8QXvFU#y}fYWWsH_1A)7YmrLkQL73LhW?=>WR!OL+hFzgl)ptYppqF|ilN@C_R*)&r z{`2nOP}{Knn!xxjyu?1qVu8GOuZS$uUx zfYyazTP+}S{w1dR{jL2gCR8+}G;J=%zHKEVe)L*$>8R?~3}Q97Ax~}T*zl+y4nqwY zDmExZ91pkC)>$gYqni8gGo+Vgm8%??c4-W0uL-zst%AD*?b>dc^lamuh-hcnJHYC8d$^)3LYj6H< zRBB58au&5~!rO0wO!DW`wLSC<{jSI^=1`T{&?}J<@~V?MOu)=xo<#*VeF~}3UrsNA z2kD`vJc+LPu5+yGGFH8=fP!eD)>WEWQIauvYOtS<)c<{~EGTP1#*?jVEm5XR+0MY% zD2BE^hWvd6fc`ct|L~ihZb$;wHs&rf8CkJUneFgU{tNX=k!df6>NZl7kt4(E)!)Zm zJk~V*r$?Ty!s@-LC54ocwyZ*|Roqhv*fm1EU7N-;%cPb?6RHCwews{AjwUU!L`2EW z-DFWwKpf!K*|H1z8u6b43Kj_A|I&naGYEfAsyVe_ch}OC>Uf5Zc4kP7MMV+j(S49d zIJcdy*pWanA~Mx)!9J@wx^P4oncuT~l#+|4Qj=;SN09XQlxHH;Ng4xrJGA(982@De z(f~{Sk5u<1;4HZym%HpCg*~yz;W#S#6cpzx=w74s9k)J8qu~Jriu(Bs3A&uqtRkAv z)rH>W39A%Mo{}6TEeyl*>NguK-0VN5rMd5@go_goPe#vVwA~tX5Sa?i%UYh|;(4%S z-K}wLm4k&C*?mJVrl!~XoRsPYVMZK0XQum&@;s!9Iwwi=^b(j=_nhaUPKGGn&dJ)? z6FwhPW;o{r{B@jO1W?t#M-COQdg&NYK4!qGx62-+ zB9(0$XmayaN`KtPOBM#FbFmwq*BxaCT!8X8$l&@mCRnTWqisE}7Cw+m^!am~-oC!N z<1z~>jzRL$5lSPbF}rg}SaZ31BdKUxcw!4NA|xC-F`W`EkmBY?fAi>3wNm=twF|-F znE9&I!$O&LoG3>xK`CwI-|iW>C(dmpS5LM2?VQDC8UmfqhX{Pg$eM?VVp9EZ*4wzE zki@k@w0{Tlr3PWUw3^mshr?}J@iqD|zeu~48B>>LPT%gq`ge7w&j=JiLN?Z5MFd@oSU>tKG;SYbm`-t6R?Efa+4OX`uD{D!`5@r{xSt$`>BllmDn!d$2NrhXYWc9{&U@jw^5e11z&Y0wNdI@ zBb?b;X%scsMO?K^w08f}+nZAJy`PlL5EN~kM==aaw`x-!1~A5O=@MbV{5po|A|u|^ zetYn@Z^(k3m-~k+hlk>*qI1!dINPiZp~<%2d;WoP#R2pyU|_w?O;5UawZ85OSupM0 zO%gvdArT&ksg#~FMB1}DW1N8I|2hLECH4(}c=LFk6jLM~M<$~wv=!B^-hLWX_HMKg zGP3>V&H}XI$hGe}#?G#_bOCTyn9#%SY^4rOkS;YDkwjSjKej$tv7d7zTL=&L{DM57 z5>J1hoAHrGG{vvNKs?=2l*tP7tV=8DLpJSd!cvq-B!O*`)D{r?Z-R_qw$^- z{?V2Ed4Fl1Acd3^zsVW(B8-y$zSAt0vyZp8LMwoK0c4PgC4{5u%3C(~iB>3&WCAZl zK>(fOFKN5CuaT?jQH0taRw+-3i}Qp0=#?0KYMhQqK*Xr2Vqmh&tk|c}!QK8oia8Ly zFwdlS!iXyd)OeGg~m*U@klY{-7a}TAoBFTLjnIxYNR)9 zyOj69iG-6>sWgI2GEBebE;1?q%#bfYBUrTm=Gmv>92L8pQ3ucTK*D zT#Hh@HsXa&z+tp0evLlCS6ol%rqdh#Dzu!dl!G0s$7vy7i`CZ{SaoRz+gs3K=e?Q5 zmGAUXS{s4tCn#n+H3ZU7=8_nKF0Y+&-5uyLI+RWnjvURndzqgIjKhzTaB~uOD!UBl zbAmmM?0hHQNQ#*2oJLbI1c%c^8PvIcK8`1o)eqpc_|NRoxrNY}t9$;8sw+Y4u(8*C zC}rxcSZl-~`kFOQg4Ga$3faH+7A8vQNeXLERz%4+O*!7d99MJ8C+`F$m^+Cid6gge zv<7CS2y3+E4laT2Z%=c_yp&2ybB{)*y}b3a-L)Gz9t!!xgG+vyMj3y~MV=M;Uu(W) z`djAT1{AXN|CtfCeYLo1>3z5JWCW5=-S#3hjVb!;O8Y(U`X zoddGh18}d&T~kstT=!PI@|6{qC{B|Juhl8uS0h7TWr@7DK;WG{%>4dK{HG(TF|-nE z-wOUD6gw@z*F15%l(UX_Zf!8Va@On2^@d+)C=7E1clYe*Lvoy=z~J=1b=R88D($PX zYGWP8j;lY(7}lH|{n!~k?Iniel)a_vkSr1>mOXtN97p zbthwR=(NjfxwEm(TB3B`Xb2qyD@ypDH~vr*BzQ>u!vhQH($q>RVkhow>z!sSe8Rsj z-pJy#JqIiCo;3wFtGAdb0g-EKYAbxZZvo_0ZqAuM82MM4(yIQowu&Nagi5&>$-80h zq4!Wve5H*}vciVGOc~j7(l{dHKCyb)qRz-CMICh;DQ6omx&(0vJaPsG3t}fzKJCpsR_mSjRwvoFOMs2nozdGlCJzgb>ZLNOu4Uxu z&X4Ji)e8k#2SF0h6N-YzYvDa=xKH<3WW}LiwFqeeAFT54&NrDU?T;1Yavk02XFI(r zbd7;wXS3g#$hGozT<89g_U7@gUJSA)C}NVzwwIl{HP(TGYgCCN2J{a2%uNp>;`dH) zUFShf9%H~UxKFxv7h3jS$=22u?qX?a36GvI*($i*ybnK_I2i*BFWT=q$Gv3ki3+Oj zAI;bI)^Crrx%P)ZTe8cHWYGktD{Y)ePwo3vVxP{=Vq!{U6^$%jP0=-}+J0GDWIVaS zx4d2{{@Q?%;b2SqWTZApC1y%&RL&=Dl46|KjN!82m!se-ZEbXrnM1dfKlgk&tiW`}jkLsk+5cwP8x7%{eAM9=m0;zCo=3)+F?2!q@?P*+dzGy0hY}20 z=gCi|l-^#kHVC^g4Q{u&a8dMLi{!5ovCMvgix+?s+IEB*?sb%BQqj?w8t1&3yFVS= z;FLlssJc{(@m7H|N;Xn@xIQ0wv^*@$G%Lc+!>9Jbq*q4}cjk52D}cELV+{SLLbQ%; z^Uq5WJd=@^uRmQRY_HgQv+eWFHrMO>v!UUrtO zSSp+yX?bZ?JNxFfrunK2nPDHL=3Bl}Ny)al&0Vc%FGu;st69F0H5&F>3SN31!Rw)QO6W}1l?$hFtQ0(_Pi()9P^p>niX zxGvv~zTG3D=>jyR&mi0K1K(ed@bG8R4@H&F;?p}Tm8FXQ?ZtXldtR}3T#DcFDnRF$w7UcE;IC)>74$qLzqr6QnFrd#GYmZ%$F$Rc zOC004wqWL@Um0!yk>CAB3-HMEJIu7O)oMQziV{N1;o*$&V()`|u>! z+8z`l435ojqegpETt8^hyD#{l*?iiJ;*RiXsKI^#H?u^f<Vuf!J9|OIA*2%AbJL>7RJ_WFQJDbK~XRX z35lv1;UEzApT9&?5-`Dd9|8|tjk-;i*~RJSj*<1);p-SmA6dl#tc}h@YTNOpdNVtP zg(`hp{h(3+he9Z~8Xu_LB3ne>+4lKegZIo$oIX<)y&SGqVKKdx}9yVs03jPg-G7*^2(d zq)PcxWDR(V|2aw$WzET8@B_1gMSI7|699cl($fdUtXXU@9~nlFDxGEt4AO7DYY5`I zf1Ph-iePOF*gM2{jrYZ?kpm{nz1vBmoPOf;PbEiG*8?Rb*O_!LsbLvMfkuy(@HtiB z+i8sOpIIHqQ=NPSNc3W>M=5AsMyWQxwBJYC%0U|;3kaK5P+(w_Gq3?(1@CfJTU%R$ zn5}a#=Hy=Y%#`Qjssm;pgqc%D>l0o*?L<=)<~nkIdI+cEv^#_8co2H&qBjwpu%8Do z>7k+rSNadmLzTtcye`Buc)A?uZw&$NkMMa4nbUdJ;8Hz8SoSFip zm+l(qIg04R2TrpMt*4+7=#!`eUQi|BMq_DKVf&Jx4^XF96ZMk7b|px$CqxN|amqGH zEx5h`?XTnc%xg?oWF#ddTEMRvPMzsFIi{eerE=}k;M7!|aqry}MJ;=EOa4jlah;xZ z6+YE#j+j8djA+Nk<}#RU;PC>LJD_Mhm9Y^|O&dpUbG3uAAsVX*&;cia7lt<)$<-G0{x(obvuZZaZ}7#~d&}jDSUhJFO@zEKtddgbH+(l;^M8 zl$NIU4*%YX6$oxhck&ip7|Siu1WsP)X+r%8;LBtc)GdY9j~bEM*eWChlT7RvR5z)_pC`*yBJ z^MPef@Q42NMY}>G#yoD{;Ym8~S5P<6QD|MQ(HZm5)#r-Q&?E znZxd<&Z=ik%i_c72vCeuNO{*^8wEC}&Z8-0OktA}zhOTF5AW*S2?m1{xR>9buOsRi z>+LwIT`jV!b%bI+Onx+|J3$2K|I%7jrN>PWvHONBQUz-$&HvNcTSvv+Jbj`>2_7U! zkVys&uE8M$_XL6k_XKx>ClK7-nc(iOgS!NGcO77WL57(fp6B>5r1$h=JvYSfgP;3C7g!t@tKESP)l)sB2S60iJ+~B>HWY2+|$!4r6 zW^VWQrKZa93Jr6!6^?Bzx#0i`D+@a(UP+Q*lh^)X@f@t>ptmYab8n?aY1@GIl&b^~nL8!|gm#Zis?=y|ag(?P*=yd+Rw3 z{pk$>@2uD$PK73=6$oAsX0p}63+R*%6!p5OqS9Ew+uq>hybXoeSpX{OxpZ$b52e-6 z@G($#dZ^_6|4vrE&s#QgW!e}Kb8hQ3nS%0^x@2LIAi?*u9bSEuYVS$#b4htnX?o=Y zTu@)qmy!vF6ABB4bhFzC)Eb#D33!ptZ1LzJ~akB2i_5DIkPF zrXEXy=34wWa1u@Mj`^0v#f&H~%ISmo!eR^qZ_@rWYQ<&>C9h@1sN$$9h1{=|^(0Jm#pBzUrwmI`RdgxiHnO#<)Q6!nIIJCY_39d0T6#a zF#zA9t|Q8eihx<>ATRaoaft^&q^GN^D8bfp>Z@Mdnl>9;c6NU5;2ZVvjP0}LDN46| z7SEro6hcLs=3l`GQFdDT$>YWQo^L#yxp~g8J0OG>w73m%dXd2h1QJMA9&V}7#SUzr zpFsC3Ku3CrhQ;HA0TVVboRe^>@y+8+?Bj;9-Pi$>@|JVCS=z{oK9os76)8;@d%w-* z^ET_h*ge!oGR6-4!!GLfL{|%4l;{@@?}mQJ^T{+MYtjuK8hcVp zUczLtvGt4a4!ZI5uFuME48q<>0A%OMSK7K0lq403pRq|MJ)AG&zh%?TVs8>?E!9gD z^@Vui4_r!gTbWJ*@^|udx>d=ZquiRP1lsFBu_s)}M`+L4W`wpo;$4NIyD;SGUofgr zNV3~*NT6Bn8Z%1MuVt-UsyaMh{comvw%{mlndqNFcO>8%3)g%!fscaK?zhrQiRUj2 zU-%Nq{opv)AYOmPQs#BBur1}T)}*5dCjO4tvPhA_m-@^Q%oJ0DKQOKSl!QDwNNL<( zDY1F2vS+;=h9`MepTIYt5eeRCbzIqXURks%hm~^)nCgMnFX@Mg58gVH7ht~jga$rT zU}%?=K^)^gWR!hy?H=^%+!EmxMR}dj%ASoWp)DE~$G%4WgCfKm8st?^-K4RC#lH;= zbf|`B`>2vUDEdkH@!VG0%3}^GERgr@ z0BQ_XJ-wCnuE0{the}XrIv4|B-HQ77(<>J+EaK|Q#WK*ql!fK0?ezC1pJm`f7qr=* zTdS;di*i2*XMN#-f11{SG|0L!kx2uOL&!f3Xbq3(Ez!|t4LQDv^sD(~bA6?8099~| z(e%2>dQ0=5E5Bc~e(iZ`0{w&TuJsVXT7g-10%f_OjYs$X9^G&wC6WOjmU(7*oYa(N zFmv2L!d5z!`a?Yr+s#SkoUlWscj5Cg53%lvXrd>hf=(0XcpvUbifGn=mwss}mxEEG zMR(3tj?5x5NE>@w+tKw%(10CUqCm#!pmP2+_5L@sk9*#-ibrR-^YUGh609He*T%y~ zyK&Sm!2a{|K3W977;UXyoa=%!}@zbn-1h63&+Sq`RfG^X#efGp3dPWxY>XT=QKBu6*ud66VqJ zw|HQ*)s@}}pL{m>pNM*wPr6}~Hgd{7+INe7U0GDYuNk=#N>ZxXSLq-rym2;Zu?bqI zrm`L8YQ3W%^H5ecWAB9CEg^U5LrOAnj~>Yd z8*0qJpB>g*ha;N}wvZVnbG7!ZJBaUYY)Y`$&}XV~CM#2yf$8Rm-Q))1 z&TCP$pRM=@n6sv{7pbXt_4M~9VcRAQ(2sKJb!O+{7!Hy6?7)A`vjYSeN1Fe%E0uuKj!D?bqYqcwU;PTgw$-7 zvfGYcK{Jj@#v1nOC(LkN6Ki<(g~L@5mk{4>4z}mcNLR$IYbnxmb4NI9%XL?T_shD1 zB9303Y+))Elm3g)6NIbRtl<_?&jeBTV^mvyA*UaMSSt9Co2syrhaG{TikPwc(_xg(*UTRV zUk!cj{Wp0#T8u>Sfb1sfjpU@XKm@KO>+Hu~*@Y2oKY6 z_;$p1IYySjeg^UuYxJ>xdp#qLX}z1y{jbXym9_<-hU^coiMao&)PNJ{%WYOK9haNw zzW?1+MUQRR#BCJ64BsN-{^U9R%g#{-0g*IUA|H@seWlA7-Pja0{>_TEX3P`Sm2*#He;Plxz#7s1u zy;9bpT%GMm;aJ9@8wuD32`aBd5ixHLmQoHcpUI0;OM8yzO=OCD2IBHeTZXESlv`@m zdp8v=n*Zc&XCOc{HduTfh3^g$u^OEW+sAqgPJjdDtxpL@L&}rqFzK>oqplnD+{eWj z6Y*Vdod~0kzM4BR%{E#Bje252K~}0a0Xzo@(yFnKt#7(koTE@*@%Id|0|7@mf`yQu zqr3i;H+6PuWySmIXuf2-eEFKaP(JYpbHT1coyN$K7wjhKW=VeUUf>_(&CS^;ss;8? zr`xwb1A7S6t#qVBQVF}CEH%#RHOt;DR>aaPBX&hmfmFoosUs*)p|1h82Lz5w94gs* zSZpi>y~Jg=2Rl1sr-L^&?z~oSQPsrmSM1s`pSDT92{?w62fnK8UL4Vgo%E{l953=Z zHzxt}E$qzRr4I3$6Z41bbFrY(1g@%D*ZHw#7pA7TX!eusSG+vxlCoaq(~apSBCR$i z?6j$HYDp~_eb|H1BQP;O2Pw~Lb9;aniv^?E=jQdQ+Fjam<4N;JSHDYbNlY=$ZIlJh zwv!L?GRv+ym`x205%~QbLpHy;0h7qm5LDT<*nLj}ZGM#jHaa{Tu1({l`0Nws2)%9o z;OJQG=O=C`zWi$YcGDNVtb#p`a~%KM3{$IY(NtKP8s3_~m7z^b<;5}hhsvu?&iGKHYk$<=v?6ec*lHM5yqTy~}?U6{~u0;Jk_X~Ycf4yaKC-AE~YT(TUm zL-2!tM$2?h8SS^YVT{@9UJW6rx&SI{+6^sx3V%y9^gKDg*eL5?3t|i)Tl>2v{gkL| ztuos)cCoHEaZut@-Y~xypPK5yBp!i_>+0n@)x%V`xKhT9X25m}gV-LwTuWfjkW((= zgez>>Xa44*;H|>Mb1e>V=CxDf$J(_mJIjbUGcpPr#Py~+tTVUtw%}|_R!8FE)Wf}J#oQlJ_uvslSV{JU7#iHOJN=7#RDMvZ09A(jfpfnHJ zO(V}6SJ;Y4H5=))69Wr)#inShDN=_*DB)qn#~FP=vJNP)%hF4^{o&J!+E4|0y|mc@0=-- z6?pOz-ClRK>Zhr%&4%!9sezo{jSlOnYE+X6!}3)7#(>3@X6aF#gHF{xJeurWquH^O zqQJmA5rd)0)xz0|AT*br6^Bje=BQ7>Wi2|NtdH^{;x|_W_Whu}HHmre*HH%+8Y63P z_BuPQD<=P1alXnaU4KCnFr%3a zKH?H_8C4ajOQ*40U>T~04ds=;IGHpmm`>OC?QQshtukjP`qOA@YBxatR@L^#?ozn} zV=~`gaM9^}><3*=ce!>A5Gt%Cm*X1|XVO})kGO97wwQkSL1K%X91>gY2yKc%&%ksv zZhU^+>=vI@QL~3$LODm-M|K!Nrv21{-Ju&aK6~I40gP5y-=sTJkO}s296#W7NYBSj ztl#4~1e#~=4kETM4Z-TE8xeGUd}`3q>@+c8$Nb7d1FJz zGu~gxc&xnRnfaVPid4-#>;6dn{fkqXMLU3I_86 zX4C*y?DpMPF-(C^i-_t@q1RHQdve^hiq?rFoF|!Q(Q>I&&mdvNlW}|73{bJ2YFjhsm<;wzdyqzNbZz>wZ~E&J{pOdgM61>?Tp=ao*4mX@A}Q6-rfXl z)I*Vu2b$xPMwtlzAziG22dfc9Kf}e(A6$PZGT8iII@6#4^*{6;_y4M^W0eb<0oFC) zf;GgJX_jo1opo@np<9%`AZI3GpY`Q%Q>m5~h~hM<&V3!Um9DpTAhH^xuj00rNF1DM zyR|tm7WSn=J+x}*h1qh%5`03gHQ3yAFiM)9RurGvG$^Rw=&xr~F+$3`MrKRI&^SXj z3>u@!zffi{p6C+?HeP)s&NVg|_m-NTR=J{@xM5Gmu(uPwNj>3+V7vYp+*IEa1GVnR&AZC){2shLwLwpXuM;> zmKqk-qarap?s!{oey>T`(q++p-E~vTMUAO{Q=NAkqV1kpO*>&!>uU}@5cvOTw5a3O zV9%t2Y3J^}tI{Hf$K*7@x!l%OD&Of@{&qBYEDs7#{OmY# zB?-m8ntpJHp)Eg_LagI$6w6OJhcs;UeaUc09#$k15aEcqxAFMySNr7fYZjuidN)rg z92U!w2>z>iHLoXfF_2>ia7|VyBPEC>f-Ha1(J?hPMgr@Dl0)^7i{0!1|0T6DQVO!; zT`Lit)j6dd+fdhx3-Uyw{oS{TjZJsImD4GvzT~m;eihsjJQ4wK4J!^I*rcZQSz`xY zwHbEY_bBGS=9g~%{tK#*f-@wVs2~>(Yq(IXC`XAmDovLz3bpV~P3o>6kH`<`PZ*+} zKf|fnyP7@c6)0Xjd__b8tC+Ys7{Cd1zT;O`Tk8>dq)xWBYc!>X^mN@kv?wumnyBe} zlFkNymsTJ;Ec={6nAGB0%sipkmxvOly+82jyA%s2cXDlv301P>sFyn(1Y^*V-lmBq zUr5qggN!nI^Y=#&+Xy-c%s+$X`wt2#Qdj5y%iDYQV*rA*!-N}y05BWcD9SIh&AFij zvH`|uaa6GO2KCG`?}kee%5JBhq{R=J9!teWfBUN&a$IK69TPDOx^C(;ov*zo)aZjb z|9O%hz6DGSVWnhrbbgyXJ}1|$Q=E4)qI145tQbGCQ_V6l9?GfuF+F&6P7%v)b=Sn( zge;byx&>75N44?3=3%Wq@cKskTpKUkB&Zvu2_Un~C6pveeXS$m`jPGRFS;wzJ*GAB zNNphcV9RM z0#+M`IL0UUSi8rn+t?_zV@pE+Oal)?S3K|+kh^&Ozm+WWPP4HnWaf#dFY+(<* zn-s5y+3Wb7Ey9l?xzQC^fV;m82~}bMwLSTiPn@(#ZvsVo#mYRRtSy9W+BAXOdpJ2i z{&vw-FQ*y_#b-=?+pFmW{Rg!j_DrcY6s7q?c_EO*xM&e-k-6heffsnncmi%MmD?MJ z@UsA^^Dm56CHdnz`S(qO25;?claC(xPl&YDYtdTO6_&u?{aOtGc7oySDa=JVy@2JPDsE7h!Jy+w8 zBUn2ILb-P7`fwwIUW5j{Am3qfBobZ|w7UK@c6XeOQi5w1V2mom*gU0VAyoa~VA->f z&7SSRe;Dp;IUi)2iFnk+)2HV}=?pNj9PNMJ1gjxs#8e0CnUH~^&weMe;N3%ar~?mh z!h2)yzP`(|*X58_%TSJSk!R zaMFaDS3UKKa{*uWeqiR$FGO`(?5+Z`+e9HPeaj@_Hek zFDhLAy2hzOS6pYd)U=m`x3Y*GyXv16SFea3T`qwU1nVaj=qzi0Cr7kj@o_i!LHI{* zm+=tPyu#~Z=Ju^BsQFS5nJQJ?`aS4T6M5j3i&e){ zpD=?7Yu(tJ4%a+&R5?BOCh0Lvb}pswla>f_tceU=ebY=A)#a51eRL6uE!Hfmj_;6C zs$Hps>}FnY<@fwH*_RZ-_A$?tGC&DL4E3eSFGWkkUcU#+5mN~KnY>$~_SEw>Yr$GA z7;!V6z5=8^b+9u@bH9s77dCg#8QJ#IxgQ;2;)Pi8?J~Tu-fCB6o6bqxQACwXHbI>P z$v1w%C)K+3>GMR&5+;!kQzEg;O|Is(P&8lEp;k?7<9?02ezGKpQnTr&mf2-ZQa)9| zu2KC&dPN^>lfxk=ozwC0U&z^3J3!IseUcvXK|%j-#Pxob&_PNgtI*n_xh2XTFFf)~ zNbv8x=gWwN=hU-v_sq;I{3NR5zY&&?kMb^**MgqK4r8@KLwB0m zb4i>sReMes3+*S)JoxNp7FPlRo*r;rCu@~H-@R8YcHb`3IgB&wVe-!sWmCjSTsh}& zvDdemS)_GGPNgLR1(6M05Vqla8CAC<>`RpbDUn#X$!U)vE<(ob42F|O9q-Y$PUS^w z855OymiKlCw2y(ea9u9`(*qFWgIXQ5pSaD44JxqfU~fW{Hti_A(&!C0 z_J_{Yz)jt<@Wh=I?hnpwy>{T>fhQ-Lm$P)FQ~R>zR8*;rbeEs;&REhr$!oRM%NYvz zvKC9R#ss1E6SHk@xG#~w1mE=OhHeF_QmJN zdWry(1@x)!+v08wA>i$j^bNlYKzmWeI^coD_SzShDR8)#^i3+jZKwfB>~q>U>g0W) zNX6qd8kSV%Llf0#rE2zB+v?z=UwarHy!Xj)rY=B?5Jb6*42erGzqNB-87taqtTxXZ zPU5gUDT&%C)#CTyCy01euG6686+~^6q`rBk%2J{^EOPouUh~F$qNByA{b8QyCFr;Rq3Rctl3N;|#o2gwLbm-w@&tAs`#6=4627b+ zE_ob=Wpat>`q?sCAS&wj7FZ6`t9utx8EM;Xc=QVQj$tYaw~;%!r3>GMPST8GS38?S!;+N5<@&Ez1ACHSHO+^9XwGsmb-cn+9TDG;^`iDmHZ6XLbp zFu+I$d4ttd-MafHz0M!K^i!t*&4Z?RaRPKSO%33|(j{!DdEvcFs<% z3}mnJ$$Re_ldJdi7MAxT8=%`xQht1DtmG7eg!C)*^`lBIuZkDg?e6UVB7D17(vgPL zUNpqgZU#qlNobAK<%=;K=J5+_oK8~(46AnsP?C`$Mjk@^+EizKBI34cS+7CiH{bp& z1!D^mkLjyfMd5^pM7$P>DstrHbZ{1m4jl{n($|vqGqx-EZ?g}^JndQtLr_3p8G7OA)nG>fp(8#-!wtYZbXN-5>C@=E^0h^_p=@{*;u-;RQCP- zxSB+jEMZO5n|H!4Dm2+FbGcw~TO2evE9paJRaCwUW_C~-(fW-FuB3ue${XV`3-vM< zAr7a@6fz8vGG~*+^^SGTdVCx@|UwfTIg}wP< zi^fPRz?)&w`4dsyH=RF$T=>CvqTDyi-dKr8@Dr<;j~^u>fRO%izvVIdah26ipcKkQ zOZ=`2gXB@Ird*dXjc}W>h|r6KYKf^yXy$Ly@)L0#m8DjJfTD~D=a;d9~Ez*4dEb^_w7OZuaSS<2ZBZ8K;l`lzYZsc2a`bp=70 zFv7M?1?*3AqR2%gKC!<~=~p{(A4+`mQxI?2_CY(K&34LYCh$(F??;j~dY>sro)mhC zdbp+!OiavJ6)VRWbOvecD>CMbQKt)61QFHT-k7zfQ4rP;Uhns&E~?NBZ4nO+^l3SNm5A?FmSE6`URpbu42f<%a7O$;Ccl5&cWUyNTdNV)oa zW^3#6!oT4KOXN!G1w)GWfMBdR{Ah!ccat;8i`^NS9GRH-vzo=l!tR}(N^dHwb=Gsw zHRQCiTqY6ercz@`;|LP-)K_yI69>JDp>^R^Xy za$ea+OA(M6(yeilvT3IrGoR(pn_3x~!8+^Nn4jddfLXmESzf#FO=)+1nRK5SVlU&f z$k3~V7dzB<&ghY;DJ(2BF*4g3ZNC_Mrn# zNXMQsTLr4<>?hla$BYIu@kd40O)XTt)K)qT^x_gNm1R|Vh~M)cg>!74sEPS)^U58v zsFg>`_T&A44V7rlPZQFjt=WkEt8fg4m;*8Oeh6$T4Aw1a)mJtlan~>1-YJ{Z<#@*l zcOlF4DbNHuQvQLqXxemCq`VM-amtE*l_BVcRX=4Bl-T$ga0R;|M5l(LS8#^?>#v+^ zs=oEopA~rC``c}4+8+iVxHo-eE+j2+D^q?}@e!>d^?&hID%}0Yu~}g+^H6W;fvhP0 zMboH1$pimY?I_&Z`ybVVih%h4?b>c;w@^C?a<`D%Ni?&RK5s_lzvn4v<*hfJVCHi( znSrBfA+Fr`<*O-Ya9_BsgaZA{Ba8fG;v%=Lb=WnSJr zv%63)>B{aNN#K5wJjU}&^3eK>3+x}CJn!fQo3O8PZPi%^3lBQmFdjzwS@vrS&@Pn( z9%Q?vHQLe7b{h#yA#^R%TOU<(KE6HVxw*NEIG2#%_@I zRrgwF#@Ee9DE&ANG1p&d$qs{h2m$#F*KbXr3;X2Paf(o5nm)|kduZzV_t>u@W3zA* ztNq+CIXOkA;)U~w+f-w737ZZKE<^@<5G{}(ObxVUd)K65yFu&a)kTm;7onnOdH!?V zue-pF1#3{mbEO!4yt2~`i@S}jT>cE@nHAGd%E&y^udzwN!%v2h?AT0yq2f4q0(pLr z8J--#K&Su-6jBDDhHJhXvb8xEiH-CIQ%Z(M7Zkd z-*@y`9Lq`ro4oJS`P{L|_o*xS2A>%B^*-&V^bfGb#6o882JcyU&ClS?*fMh5g~^ff z7OjqV&gu_n!be><0ypU4-}%(6^s4gaWF)!|>r95Va^E*#Q}Qql!rE@(lF%x-^4gr$ z3d=`pku_LDiQ zriKk2(8$Cv*4VVEbg}rxQs*CuT+7%tIYv2gaR2%w)I-{m6e9=8&_k5M;%R;UlzE6u zN0Le&<=BnuI2RfQsC2xvMQ@T0kfI66w3{>bzZ;%^djy-mBZxhvb0 zkeK>ZRjNY3#UVAC#MH*j>)uBjevz=sMhtU-k@N6>{i*?<#s?ZvkoJib?cuLDB!B57 zm{fkX079;3PlrzI=8cSLYOC1Ztez2?0XZSFGY;My7{5+bf+Y^fuCM3V=l!XA$fsAy zCjAU4g6s(@kEXR6=jgeYpxyx#-I#3tHHsVec7JpfwziIV{`z@yq{|hW8wcd6hjqm2-~!B7 zTs9n=U6PK|`s8`aYJ1>M8q0uK0Tn5yy&u$l05x-kSzXQMm6b1+RiqKz1gO*wYS4LH?#L^^z*`1CAqK-+)WBwE>ui~oi6hwADF zu;2`^(Mg^%W9Z6KZSM5^GMm-K)|R;xpZ{6zvuVP#msF#n`tX|JBqF`0?2oVST;iDUD}f0V+iiwj z;u4hRH~eYU*+EC0d`xT4IGwU*hA})?#e+udOOQ%w|vh^a=5wR7=Fwrt*Oo zeHlo&3u2U_Sn8fhL+83!Pxd)dj`U^^V?3*|XL~x~IKI_LH9H4af=1(JFd0JhSv4Q0 zEvf-(M!j3|NyqtDUO<_!BvSwyxo7fvRRI0$WS9}IN zTT|&RGU62m?qcp^8Z_ znG;<$ieAI$S9_I3S$XmC2dI)s2aZODZZ!`%T_#SUvQ>Oe4p|2hXWBQf=u;LJnDfM& zs}qN{&*m$O1AEhjg>{)P7lhGAc2u3WA*HO>pw93x_txl54;*qb#=`l!ebw}FlNp~> zP_6iCWey_Y(42;Oh}@$1qAx7XZab8B7aw`Cm$3k=cAZUTX0aX#2=PTFg;W(c6z{ya zc)gLoWoe1$vXX}!ZYzaY;E{z%7h$e`nXPrwf76o z!Ru#rDkb$Pv5C7be7@ve@Gxt%8NXeV_ zpBn~*pFc$>`xA>PYNsd|G0w)m!Baf7biAXjb_hmicWUXa+ zC)e*rCq`v-mg3qN!QKQwixP$f*HKaL9GJg=57z->5pMdH4D&-Q^w}%Gz4E*_;U?K@ z@}f6g^`_JLDaLk5{61)<~wKJIlOhv@;->3CV|7+%}7kWD|l`;r>r$UNyrFtz^zOP9F{t=S5PzOaiQ*EJUjG zT~8Yd=O;b_gTI{YlNYGS1OSN@4E{w>-Fb>HEIXx-55PaRmw=4rC?34-#p=N&nAZZ?PBW|=b8=v8y(`@exc*a|;So})nAsq~ATuDjyH2g|8j zE(ed{{_*{i$5+7ua=zkKm~*MIfr2L6{R!J;Rp=0=jS@TZOI_caNYcU>U1P<6_k=Lc v{|t`k%6>?0e4zh(+vDHg|DVki81j(=X65_8WUGY_KarJGk|_IV;QK!SN%p1p literal 0 HcmV?d00001 From 3f2c6962504b2386a62d50f8167b3e0be5b48052 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:22:43 +0100 Subject: [PATCH 205/217] fix: [#893] enable color in logs --- src/console/ci/e2e/logs_parser.rs | 57 ++++++++++++++++++++++++++----- src/console/ci/e2e/runner.rs | 2 +- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index 2a1876a11..ff4028f80 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,9 +1,16 @@ //! Utilities to parse Torrust Tracker logs. use serde::{Deserialize, Serialize}; -const UDP_TRACKER_PATTERN: &str = "UDP TRACKER: Started on: udp://"; -const HTTP_TRACKER_PATTERN: &str = "HTTP TRACKER: Started on: "; -const HEALTH_CHECK_PATTERN: &str = "HEALTH CHECK API: Started on: "; +const INFO_LOG_LEVEL: &str = "INFO"; + +const UDP_TRACKER_TARGET: &str = "UDP TRACKER"; +const UDP_TRACKER_SOCKET_ADDR_START_PATTERN: &str = "Started on: udp://"; + +const HTTP_TRACKER_TARGET: &str = "HTTP TRACKER"; +const HTTP_TRACKER_URL_START_PATTERN: &str = "Started on: "; + +const HEALTH_CHECK_TARGET: &str = "HEALTH CHECK API"; +const HEALTH_CHECK_URL_START_PATTERN: &str = "Started on: "; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -59,11 +66,11 @@ impl RunningServices { let mut health_checks: Vec = Vec::new(); for line in logs.lines() { - if let Some(address) = Self::extract_address_if_matches(line, UDP_TRACKER_PATTERN) { + if let Some(address) = Self::extract_udp_tracker_url(line) { udp_trackers.push(address); - } else if let Some(address) = Self::extract_address_if_matches(line, HTTP_TRACKER_PATTERN) { + } else if let Some(address) = Self::extract_http_tracker_url(line) { http_trackers.push(address); - } else if let Some(address) = Self::extract_address_if_matches(line, HEALTH_CHECK_PATTERN) { + } else if let Some(address) = Self::extract_health_check_api_url(line) { health_checks.push(format!("{address}/health_check")); } } @@ -75,9 +82,32 @@ impl RunningServices { } } - fn extract_address_if_matches(line: &str, pattern: &str) -> Option { - line.find(pattern) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + pattern.len()..].trim())) + fn extract_udp_tracker_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(UDP_TRACKER_TARGET) { + return None; + }; + + line.find(UDP_TRACKER_SOCKET_ADDR_START_PATTERN).map(|start| { + Self::replace_wildcard_ip_with_localhost(line[start + UDP_TRACKER_SOCKET_ADDR_START_PATTERN.len()..].trim()) + }) + } + + fn extract_http_tracker_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(HTTP_TRACKER_TARGET) { + return None; + }; + + line.find(HTTP_TRACKER_URL_START_PATTERN) + .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HTTP_TRACKER_URL_START_PATTERN.len()..].trim())) + } + + fn extract_health_check_api_url(line: &str) -> Option { + if !line.contains(INFO_LOG_LEVEL) || !line.contains(HEALTH_CHECK_TARGET) { + return None; + }; + + line.find(HEALTH_CHECK_URL_START_PATTERN) + .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HEALTH_CHECK_URL_START_PATTERN.len()..].trim())) } fn replace_wildcard_ip_with_localhost(address: &str) -> String { @@ -127,6 +157,15 @@ mod tests { assert_eq!(running_services.health_checks, vec!["http://127.0.0.1:1313/health_check"]); } + #[test] + fn it_should_support_colored_output() { + let logs = "\x1b[2m2024-06-14T14:40:13.028824Z\x1b[0m \x1b[33mINFO\x1b[0m \x1b[2mUDP TRACKER\x1b[0m: \x1b[37mStarted on: udp://0.0.0.0:6969\x1b[0m"; + + let running_services = RunningServices::parse_from_logs(logs); + + assert_eq!(running_services.udp_trackers, vec!["127.0.0.1:6969"]); + } + #[test] fn it_should_ignore_logs_with_no_matching_lines() { let logs = "[Other Service][INFO] Started on: 0.0.0.0:7070"; diff --git a/src/console/ci/e2e/runner.rs b/src/console/ci/e2e/runner.rs index a80b65ce2..a3d61894e 100644 --- a/src/console/ci/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -116,7 +116,7 @@ pub fn run() -> anyhow::Result<()> { } fn tracing_stdout_init(filter: LevelFilter) { - tracing_subscriber::fmt().with_max_level(filter).with_ansi(false).init(); + tracing_subscriber::fmt().with_max_level(filter).init(); info!("Logging initialized."); } From 555de77864a2d128f6ff15c742279fc0f7341e56 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:36:14 +0100 Subject: [PATCH 206/217] chore(deps): add cargo dependency regex To parse cargo logs. --- Cargo.lock | 1 + Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d138cb356..d3f4fac8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3600,6 +3600,7 @@ dependencies = [ "r2d2_mysql", "r2d2_sqlite", "rand", + "regex", "reqwest", "ringbuf", "rstest", diff --git a/Cargo.toml b/Cargo.toml index a45c0c9e9..10f0d488f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ r2d2 = "0" r2d2_mysql = "24" r2d2_sqlite = { version = "0", features = ["bundled"] } rand = "0" +regex = "1.10.5" reqwest = { version = "0", features = ["json"] } ringbuf = "0" serde = { version = "1", features = ["derive"] } From dd10c9a69785bca5b1ee51462279f85138625a8f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 14 Jun 2024 16:44:32 +0100 Subject: [PATCH 207/217] fix: [#893] enable color for logs It was disabled becuase parsing lgos to extract the services URLs was not working due to hidden color chars. This changes the parser to ignore color chars. --- src/console/ci/e2e/logs_parser.rs | 77 ++++++++++++++----------------- 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index ff4028f80..a4024f29d 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -1,16 +1,11 @@ //! Utilities to parse Torrust Tracker logs. +use regex::Regex; use serde::{Deserialize, Serialize}; const INFO_LOG_LEVEL: &str = "INFO"; - -const UDP_TRACKER_TARGET: &str = "UDP TRACKER"; -const UDP_TRACKER_SOCKET_ADDR_START_PATTERN: &str = "Started on: udp://"; - -const HTTP_TRACKER_TARGET: &str = "HTTP TRACKER"; -const HTTP_TRACKER_URL_START_PATTERN: &str = "Started on: "; - -const HEALTH_CHECK_TARGET: &str = "HEALTH CHECK API"; -const HEALTH_CHECK_URL_START_PATTERN: &str = "Started on: "; +const UDP_TRACKER_LOG_TARGET: &str = "UDP TRACKER"; +const HTTP_TRACKER_LOG_TARGET: &str = "HTTP TRACKER"; +const HEALTH_CHECK_API_LOG_TARGET: &str = "HEALTH CHECK API"; #[derive(Serialize, Deserialize, Debug, Default)] pub struct RunningServices { @@ -59,19 +54,43 @@ impl RunningServices { /// /// NOTICE: Using colors in the console output could affect this method /// due to the hidden control chars. + /// + /// # Panics + /// + /// Will panic is the regular expression to parse the services can't be compiled. #[must_use] pub fn parse_from_logs(logs: &str) -> Self { let mut udp_trackers: Vec = Vec::new(); let mut http_trackers: Vec = Vec::new(); let mut health_checks: Vec = Vec::new(); + let udp_re = Regex::new(r"Started on: udp://([0-9.]+:[0-9]+)").unwrap(); + let http_re = Regex::new(r"Started on: (https?://[0-9.]+:[0-9]+)").unwrap(); // DevSkim: ignore DS137138 + let health_re = Regex::new(r"Started on: (https?://[0-9.]+:[0-9]+)").unwrap(); // DevSkim: ignore DS137138 + let ansi_escape_re = Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + for line in logs.lines() { - if let Some(address) = Self::extract_udp_tracker_url(line) { - udp_trackers.push(address); - } else if let Some(address) = Self::extract_http_tracker_url(line) { - http_trackers.push(address); - } else if let Some(address) = Self::extract_health_check_api_url(line) { - health_checks.push(format!("{address}/health_check")); + let clean_line = ansi_escape_re.replace_all(line, ""); + + if !line.contains(INFO_LOG_LEVEL) { + continue; + }; + + if line.contains(UDP_TRACKER_LOG_TARGET) { + if let Some(captures) = udp_re.captures(&clean_line) { + let address = Self::replace_wildcard_ip_with_localhost(&captures[1]); + udp_trackers.push(address); + } + } else if line.contains(HTTP_TRACKER_LOG_TARGET) { + if let Some(captures) = http_re.captures(&clean_line) { + let address = Self::replace_wildcard_ip_with_localhost(&captures[1]); + http_trackers.push(address); + } + } else if line.contains(HEALTH_CHECK_API_LOG_TARGET) { + if let Some(captures) = health_re.captures(&clean_line) { + let address = format!("{}/health_check", Self::replace_wildcard_ip_with_localhost(&captures[1])); + health_checks.push(address); + } } } @@ -82,34 +101,6 @@ impl RunningServices { } } - fn extract_udp_tracker_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(UDP_TRACKER_TARGET) { - return None; - }; - - line.find(UDP_TRACKER_SOCKET_ADDR_START_PATTERN).map(|start| { - Self::replace_wildcard_ip_with_localhost(line[start + UDP_TRACKER_SOCKET_ADDR_START_PATTERN.len()..].trim()) - }) - } - - fn extract_http_tracker_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(HTTP_TRACKER_TARGET) { - return None; - }; - - line.find(HTTP_TRACKER_URL_START_PATTERN) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HTTP_TRACKER_URL_START_PATTERN.len()..].trim())) - } - - fn extract_health_check_api_url(line: &str) -> Option { - if !line.contains(INFO_LOG_LEVEL) || !line.contains(HEALTH_CHECK_TARGET) { - return None; - }; - - line.find(HEALTH_CHECK_URL_START_PATTERN) - .map(|start| Self::replace_wildcard_ip_with_localhost(line[start + HEALTH_CHECK_URL_START_PATTERN.len()..].trim())) - } - fn replace_wildcard_ip_with_localhost(address: &str) -> String { address.replace("0.0.0.0", "127.0.0.1") } From 4f2e23bbecc236ae78739feebbd91d5fdee8732a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 10:24:25 +0100 Subject: [PATCH 208/217] fix: [#898] docker build error: failed to load bitcode of module criterion Command: ``` docker build --target release --tag torrust-tracker:release --file Containerfile . ``` Error: ```s => ERROR [build 3/3] RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive- 56.8s ------ > [build 3/3] RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release: 0.674 Compiling torrust-tracker-located-error v3.0.0-alpha.12-develop (/build/src/packages/located-error) 0.675 Compiling torrust-tracker-primitives v3.0.0-alpha.12-develop (/build/src/packages/primitives) 0.679 Compiling torrust-tracker-contrib-bencode v3.0.0-alpha.12-develop (/build/src/contrib/bencode) 0.763 Compiling torrust-tracker-configuration v3.0.0-alpha.12-develop (/build/src/packages/configuration) 0.763 Compiling torrust-tracker-clock v3.0.0-alpha.12-develop (/build/src/packages/clock) 0.936 Compiling torrust-tracker-torrent-repository v3.0.0-alpha.12-develop (/build/src/packages/torrent-repository) 0.936 Compiling torrust-tracker-test-helpers v3.0.0-alpha.12-develop (/build/src/packages/test-helpers) 1.181 Compiling torrust-tracker v3.0.0-alpha.12-develop (/build/src) 1.891 warning: Invalid value (Producer: 'LLVM18.1.7-rust-1.79.0-stable' Reader: 'LLVM 18.1.7-rust-1.79.0-stable') 1.891 1.891 error: failed to load bitcode of module "criterion-af9a3f7183f1573d.criterion.b69900c842eb33fa-cgu.08.rcgu.o": 1.891 1.991 warning: `torrust-tracker-contrib-bencode` (bench "bencode_benchmark") generated 1 warning 1.991 error: could not compile `torrust-tracker-contrib-bencode` (bench "bencode_benchmark") due to 1 previous error; 1 warning emitted 1.991 warning: build failed, waiting for other jobs to finish... 3.936 warning: `torrust-tracker-torrent-repository` (bench "repository_benchmark") generated 1 warning (1 duplicate) 3.936 error: could not compile `torrust-tracker-torrent-repository` (bench "repository_benchmark") due to 1 previous error; 1 warning emitted 56.80 error: command `/usr/local/rustup/toolchains/1.79.0-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --examples --tests --benches --all-targets --all-features --release` exited with code 101 ------ Containerfile:61 -------------------- 59 | WORKDIR /build/src 60 | COPY . /build/src 61 | >>> RUN cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release 62 | 63 | -------------------- ERROR: failed to solve: process "/bin/sh -c cargo nextest archive --tests --benches --examples --workspace --all-targets --all-features --archive-file /build/torrust-tracker.tar.zst --release" did not complete successfully: exit code: 101 ``` - Docker: version 25.0.2, build 29cf629 - Rust: nightly-x86_64-unknown-linux-gnu (default). rustc 1.81.0-nightly (d7f6ebace 2024-06-16) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10f0d488f..57e213599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ members = [ [profile.dev] debug = 1 -lto = "thin" +lto = "fat" opt-level = 1 [profile.release] From 73dec71419faea19f44eeebb7756dd149bf44e14 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 09:48:17 +0100 Subject: [PATCH 209/217] feat!: [#878] extract logging and core section in toml config files --- Containerfile | 4 +-- compose.yaml | 2 +- docs/benchmarking.md | 2 ++ docs/containers.md | 2 +- packages/configuration/src/v1/core.rs | 19 ++++++------- packages/configuration/src/v1/logging.rs | 27 +++++++++++++++++++ packages/configuration/src/v1/mod.rs | 21 +++++++++++++-- packages/test-helpers/src/configuration.rs | 2 +- share/container/entry_script_sh | 10 +++---- .../config/tracker.container.mysql.toml | 1 + .../config/tracker.container.sqlite3.toml | 1 + .../config/tracker.e2e.container.sqlite3.toml | 1 + .../config/tracker.udp.benchmarking.toml | 3 +++ src/bootstrap/app.rs | 2 +- src/core/mod.rs | 3 +++ src/lib.rs | 5 +++- 16 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 packages/configuration/src/v1/logging.rs diff --git a/Containerfile b/Containerfile index 79fae692f..cdd70e337 100644 --- a/Containerfile +++ b/Containerfile @@ -96,7 +96,7 @@ RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/ COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER="Sqlite3" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 @@ -104,7 +104,7 @@ ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} -ENV TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/compose.yaml b/compose.yaml index 1d425c743..a02302a26 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER:-MySQL} - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 1758e0de4..67b680fdc 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -29,6 +29,7 @@ cargo build --release -p aquatic_udp_load_test Run the tracker with UDP service enabled and other services disabled and set log level to `error`. ```toml +[logging] log_level = "error" [[udp_trackers]] @@ -163,6 +164,7 @@ Announce responses per info hash: Run the tracker with UDP service enabled and other services disabled and set log level to `error`. ```toml +[logging] log_level = "error" [[udp_trackers]] diff --git a/docs/containers.md b/docs/containers.md index a0ba59d4b..ff15cd7cc 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -149,7 +149,7 @@ The following environmental variables can be set: - `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). - `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. - `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 5d00c67ab..ae66f54fa 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -3,15 +3,11 @@ use std::net::{IpAddr, Ipv4Addr}; use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; -use crate::{AnnouncePolicy, LogLevel}; +use crate::AnnouncePolicy; #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct Core { - /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, - /// `Debug` and `Trace`. Default is `Info`. - #[serde(default = "Core::default_log_level")] - pub log_level: Option, /// Tracker mode. See [`TrackerMode`] for more information. #[serde(default = "Core::default_mode")] pub mode: TrackerMode, @@ -20,6 +16,7 @@ pub struct Core { /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. #[serde(default = "Core::default_db_driver")] pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. /// For `Sqlite3`, the format is `path/to/database.db`, for example: /// `./storage/tracker/lib/database/sqlite3.db`. @@ -35,11 +32,13 @@ pub struct Core { /// See [`AnnouncePolicy::interval_min`] #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header /// sent from the proxy will be used to get the client's IP address. #[serde(default = "Core::default_on_reverse_proxy")] pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a /// loopback IP address, this IP address will be used instead. If the peer /// is using a loopback IP address, the tracker assumes that the peer is @@ -47,6 +46,7 @@ pub struct Core { /// address instead. #[serde(default = "Core::default_external_ip")] pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -54,6 +54,7 @@ pub struct Core { /// information about the collected metrics. #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. /// That's how many times a torrent has been downloaded completely. #[serde(default = "Core::default_persistent_torrent_completed_stat")] @@ -65,10 +66,12 @@ pub struct Core { /// time, it will be removed from the torrent peer list. #[serde(default = "Core::default_max_peer_timeout")] pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. /// The clean up torrent job runs every `inactive_peer_cleanup_interval` /// seconds and it removes inactive peers. Eventually, the peer list of a @@ -83,7 +86,6 @@ impl Default for Core { let announce_policy = AnnouncePolicy::default(); Self { - log_level: Self::default_log_level(), mode: Self::default_mode(), db_driver: Self::default_db_driver(), db_path: Self::default_db_path(), @@ -101,11 +103,6 @@ impl Default for Core { } impl Core { - #[allow(clippy::unnecessary_wraps)] - fn default_log_level() -> Option { - Some(LogLevel::Info) - } - fn default_mode() -> TrackerMode { TrackerMode::Public } diff --git a/packages/configuration/src/v1/logging.rs b/packages/configuration/src/v1/logging.rs new file mode 100644 index 000000000..c85564a05 --- /dev/null +++ b/packages/configuration/src/v1/logging.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::LogLevel; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Logging { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + #[serde(default = "Logging::default_log_level")] + pub log_level: Option, +} + +impl Default for Logging { + fn default() -> Self { + Self { + log_level: Self::default_log_level(), + } + } +} + +impl Logging { + #[allow(clippy::unnecessary_wraps)] + fn default_log_level() -> Option { + Some(LogLevel::Info) + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 8d45270b8..809970506 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -193,7 +193,10 @@ //! The default configuration is: //! //! ```toml +//! [logging] //! log_level = "info" +//! +//! [core] //! mode = "public" //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" @@ -233,6 +236,7 @@ pub mod core; pub mod health_check_api; pub mod http_tracker; +pub mod logging; pub mod tracker_api; pub mod udp_tracker; @@ -241,6 +245,7 @@ use std::net::IpAddr; use figment::providers::{Env, Format, Serialized, Toml}; use figment::Figment; +use logging::Logging; use serde::{Deserialize, Serialize}; use self::core::Core; @@ -258,19 +263,25 @@ const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; /// Core configuration for the tracker. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { + /// Logging configuration + pub logging: Logging, + /// Core configuration. - #[serde(flatten)] pub core: Core, + /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker /// represents a HTTP server that the tracker is running and it has its own /// configuration. pub http_trackers: Vec, + /// The HTTP API configuration. pub http_api: HttpApi, + /// The Health Check API configuration. pub health_check_api: HealthCheckApi, } @@ -278,6 +289,7 @@ pub struct Configuration { impl Default for Configuration { fn default() -> Self { Self { + logging: Logging::default(), core: Core::default(), udp_trackers: vec![UdpTracker::default()], http_trackers: vec![HttpTracker::default()], @@ -365,7 +377,10 @@ mod tests { #[cfg(test)] fn default_config_toml() -> String { - let config = r#"log_level = "info" + let config = r#"[logging] + log_level = "info" + + [core] mode = "public" db_driver = "Sqlite3" db_path = "./storage/tracker/lib/database/sqlite3.db" @@ -475,6 +490,7 @@ mod tests { fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { let config_toml = r#" + [core] db_path = "OVERWRITTEN DEFAULT DB PATH" "# .to_string(); @@ -498,6 +514,7 @@ mod tests { jail.create_file( "tracker.toml", r#" + [core] db_path = "OVERWRITTEN DEFAULT DB PATH" "#, )?; diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 15ecd5280..c35d0a851 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -29,7 +29,7 @@ pub fn ephemeral() -> Configuration { let mut config = Configuration::default(); - config.core.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging + config.logging.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 8c704ea67..51df717c6 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,8 +26,8 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "Sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "Sqlite3"; then # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" @@ -35,7 +35,7 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" "MySQL"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "MySQL"; then # (no database file needed for MySQL) @@ -43,12 +43,12 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER" ]; then default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\"." echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_DB_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 7678327ab..617450562 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,3 +1,4 @@ +[core] db_driver = "MySQL" db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index da8259286..01ca655c3 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,3 +1,4 @@ +[core] db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[http_trackers]] diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 767b56116..60d7a798a 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,3 +1,4 @@ +[core] db_path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 00f62628b..cd193c40a 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -1,4 +1,7 @@ +[logging] log_level = "error" + +[core] remove_peerless_torrents = false tracker_usage_statistics = false diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index bdd51a58e..d82425578 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -42,7 +42,7 @@ fn map_to_tracing_level_filter(log_level: &LogLevel) -> LevelFilter { pub fn config() -> (Configuration, LevelFilter) { let config = initialize_configuration(); - let level: LevelFilter = match &config.core.log_level { + let level: LevelFilter = match &config.logging.log_level { None => LevelFilter::INFO, Some(level) => map_to_tracing_level_filter(level), }; diff --git a/src/core/mod.rs b/src/core/mod.rs index effabb558..6da58e236 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -312,7 +312,10 @@ //! You can control the behavior of this module with the module settings: //! //! ```toml +//! [logging] //! log_level = "debug" +//! +//! [core] //! mode = "public" //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" diff --git a/src/lib.rs b/src/lib.rs index 39d0b5b3d..2ed88a68b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,12 +167,15 @@ //! The default configuration is: //! //! ```toml +//! [logging] +//! log_level = "info" +//! +//! [core] //! announce_interval = 120 //! db_driver = "Sqlite3" //! db_path = "./storage/tracker/lib/database/sqlite3.db" //! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 -//! log_level = "info" //! max_peer_timeout = 900 //! min_announce_interval = 120 //! mode = "public" From 37a09aac53fa1855fc854b9360afddd19621b85e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:08:23 +0100 Subject: [PATCH 210/217] feat!: [#878] make log_level config value mandatory Althought, it has a default value `info` so you can omit it in the TOML config file. --- packages/configuration/src/v1/logging.rs | 7 +++---- packages/test-helpers/src/configuration.rs | 2 +- src/bootstrap/app.rs | 5 +---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/configuration/src/v1/logging.rs b/packages/configuration/src/v1/logging.rs index c85564a05..e33522db4 100644 --- a/packages/configuration/src/v1/logging.rs +++ b/packages/configuration/src/v1/logging.rs @@ -8,7 +8,7 @@ pub struct Logging { /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, /// `Debug` and `Trace`. Default is `Info`. #[serde(default = "Logging::default_log_level")] - pub log_level: Option, + pub log_level: LogLevel, } impl Default for Logging { @@ -20,8 +20,7 @@ impl Default for Logging { } impl Logging { - #[allow(clippy::unnecessary_wraps)] - fn default_log_level() -> Option { - Some(LogLevel::Info) + fn default_log_level() -> LogLevel { + LogLevel::Info } } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index c35d0a851..f70bebcf7 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -29,7 +29,7 @@ pub fn ephemeral() -> Configuration { let mut config = Configuration::default(); - config.logging.log_level = Some(LogLevel::Off); // Change to `debug` for tests debugging + config.logging.log_level = LogLevel::Off; // Change to `debug` for tests debugging // Ephemeral socket address for API let api_port = 0u16; diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index d82425578..59c57c5cd 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -42,10 +42,7 @@ fn map_to_tracing_level_filter(log_level: &LogLevel) -> LevelFilter { pub fn config() -> (Configuration, LevelFilter) { let config = initialize_configuration(); - let level: LevelFilter = match &config.logging.log_level { - None => LevelFilter::INFO, - Some(level) => map_to_tracing_level_filter(level), - }; + let level: LevelFilter = map_to_tracing_level_filter(&config.logging.log_level); (config, level) } From 4a39d65b96090b84f44a64a137a3c32622d67543 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:39:01 +0100 Subject: [PATCH 211/217] feat!: [#878] extract database section in core config section --- Containerfile | 4 +- compose.yaml | 2 +- docs/containers.md | 7 ++-- packages/configuration/src/v1/core.rs | 29 ++++---------- packages/configuration/src/v1/database.rs | 38 +++++++++++++++++++ packages/configuration/src/v1/mod.rs | 25 +++++++----- packages/test-helpers/src/configuration.rs | 2 +- share/container/entry_script_sh | 10 ++--- .../config/tracker.container.mysql.toml | 6 +-- .../config/tracker.container.sqlite3.toml | 4 +- .../config/tracker.e2e.container.sqlite3.toml | 4 +- src/console/ci/e2e/logs_parser.rs | 18 +-------- src/core/mod.rs | 8 ++-- src/lib.rs | 6 ++- 14 files changed, 92 insertions(+), 71 deletions(-) create mode 100644 packages/configuration/src/v1/database.rs diff --git a/Containerfile b/Containerfile index cdd70e337..d55d2f300 100644 --- a/Containerfile +++ b/Containerfile @@ -96,7 +96,7 @@ RUN ["/busybox/cp", "-sp", "/busybox/sh","/busybox/cat","/busybox/ls","/busybox/ COPY --from=gcc --chmod=0555 /usr/local/bin/su-exec /bin/su-exec ARG TORRUST_TRACKER_CONFIG_TOML_PATH="/etc/torrust/tracker/tracker.toml" -ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER="Sqlite3" +ARG TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER="Sqlite3" ARG USER_ID=1000 ARG UDP_PORT=6969 ARG HTTP_PORT=7070 @@ -104,7 +104,7 @@ ARG API_PORT=1212 ARG HEALTH_CHECK_API_PORT=1313 ENV TORRUST_TRACKER_CONFIG_TOML_PATH=${TORRUST_TRACKER_CONFIG_TOML_PATH} -ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER} +ENV TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER} ENV USER_ID=${USER_ID} ENV UDP_PORT=${UDP_PORT} ENV HTTP_PORT=${HTTP_PORT} diff --git a/compose.yaml b/compose.yaml index a02302a26..cab5c6d5e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: image: torrust-tracker:release tty: true environment: - - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER:-MySQL} + - TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER=${TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER:-MySQL} - TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN=${TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN:-MyAccessToken} networks: - server_side diff --git a/docs/containers.md b/docs/containers.md index ff15cd7cc..1a1ea2f0d 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -149,7 +149,7 @@ The following environmental variables can be set: - `TORRUST_TRACKER_CONFIG_TOML_PATH` - The in-container path to the tracker configuration file, (default: `"/etc/torrust/tracker/tracker.toml"`). - `TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN` - Override of the admin token. If set, this value overrides any value set in the config. -- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. +- `TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER` - The database type used for the container, (options: `Sqlite3`, `MySQL`, default `Sqlite3`). Please Note: This dose not override the database configuration within the `.toml` config file. - `TORRUST_TRACKER_CONFIG_TOML` - Load config from this environmental variable instead from a file, (i.e: `TORRUST_TRACKER_CONFIG_TOML=$(cat tracker-tracker.toml)`). - `USER_ID` - The user id for the runtime crated `torrust` user. Please Note: This user id should match the ownership of the host-mapped volumes, (default `1000`). - `UDP_PORT` - The port for the UDP tracker. This should match the port used in the configuration, (default `6969`). @@ -243,8 +243,9 @@ podman run -it \ The docker-compose configuration includes the MySQL service configuration. If you want to use MySQL instead of SQLite you should verify the `/etc/torrust/tracker/tracker.toml` (i.e `./storage/tracker/etc/tracker.toml`) configuration: ```toml -db_driver = "MySQL" -db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" +[core.database] +driver = "MySQL" +path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" ``` ### Build and Run: diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index ae66f54fa..17ac36ee0 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -1,8 +1,9 @@ use std::net::{IpAddr, Ipv4Addr}; use serde::{Deserialize, Serialize}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +use torrust_tracker_primitives::TrackerMode; +use crate::v1::database::Database; use crate::AnnouncePolicy; #[allow(clippy::struct_excessive_bools)] @@ -12,18 +13,9 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - // Database configuration - /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. - #[serde(default = "Core::default_db_driver")] - pub db_driver: DatabaseDriver, - - /// Database connection string. The format depends on the database driver. - /// For `Sqlite3`, the format is `path/to/database.db`, for example: - /// `./storage/tracker/lib/database/sqlite3.db`. - /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for - /// example: `root:password@localhost:3306/torrust`. - #[serde(default = "Core::default_db_path")] - pub db_path: String, + // Database configuration. + #[serde(default = "Core::default_database")] + pub database: Database, /// See [`AnnouncePolicy::interval`] #[serde(default = "AnnouncePolicy::default_interval")] @@ -87,8 +79,7 @@ impl Default for Core { Self { mode: Self::default_mode(), - db_driver: Self::default_db_driver(), - db_path: Self::default_db_path(), + database: Self::default_database(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), @@ -107,12 +98,8 @@ impl Core { TrackerMode::Public } - fn default_db_driver() -> DatabaseDriver { - DatabaseDriver::Sqlite3 - } - - fn default_db_path() -> String { - String::from("./storage/tracker/lib/database/sqlite3.db") + fn default_database() -> Database { + Database::default() } fn default_on_reverse_proxy() -> bool { diff --git a/packages/configuration/src/v1/database.rs b/packages/configuration/src/v1/database.rs new file mode 100644 index 000000000..b029175ce --- /dev/null +++ b/packages/configuration/src/v1/database.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::DatabaseDriver; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Database { + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + #[serde(default = "Database::default_driver")] + pub driver: DatabaseDriver, + + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + #[serde(default = "Database::default_path")] + pub path: String, +} + +impl Default for Database { + fn default() -> Self { + Self { + driver: Self::default_driver(), + path: Self::default_path(), + } + } +} + +impl Database { + fn default_driver() -> DatabaseDriver { + DatabaseDriver::Sqlite3 + } + + fn default_path() -> String { + String::from("./storage/tracker/lib/database/sqlite3.db") + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 809970506..5b3cad3ea 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -198,8 +198,6 @@ //! //! [core] //! mode = "public" -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! announce_interval = 120 //! min_announce_interval = 120 //! on_reverse_proxy = false @@ -210,6 +208,10 @@ //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! //! [[udp_trackers]] //! enabled = false //! bind_address = "0.0.0.0:6969" @@ -234,6 +236,7 @@ //! bind_address = "127.0.0.1:1313" //!``` pub mod core; +pub mod database; pub mod health_check_api; pub mod http_tracker; pub mod logging; @@ -382,8 +385,6 @@ mod tests { [core] mode = "public" - db_driver = "Sqlite3" - db_path = "./storage/tracker/lib/database/sqlite3.db" announce_interval = 120 min_announce_interval = 120 on_reverse_proxy = false @@ -394,6 +395,10 @@ mod tests { inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true + [core.database] + driver = "Sqlite3" + path = "./storage/tracker/lib/database/sqlite3.db" + [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" @@ -490,8 +495,8 @@ mod tests { fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() { figment::Jail::expect_with(|_jail| { let config_toml = r#" - [core] - db_path = "OVERWRITTEN DEFAULT DB PATH" + [core.database] + path = "OVERWRITTEN DEFAULT DB PATH" "# .to_string(); @@ -502,7 +507,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); @@ -514,8 +519,8 @@ mod tests { jail.create_file( "tracker.toml", r#" - [core] - db_path = "OVERWRITTEN DEFAULT DB PATH" + [core.database] + path = "OVERWRITTEN DEFAULT DB PATH" "#, )?; @@ -526,7 +531,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); - assert_eq!(configuration.core.db_path, "OVERWRITTEN DEFAULT DB PATH".to_string()); + assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string()); Ok(()) }); diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index f70bebcf7..fe05407d9 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -54,7 +54,7 @@ pub fn ephemeral() -> Configuration { let temp_directory = env::temp_dir(); let random_db_id = random::string(16); let temp_file = temp_directory.join(format!("data_{random_db_id}.db")); - temp_file.to_str().unwrap().clone_into(&mut config.core.db_path); + temp_file.to_str().unwrap().clone_into(&mut config.core.database.path); config } diff --git a/share/container/entry_script_sh b/share/container/entry_script_sh index 51df717c6..0668114fd 100644 --- a/share/container/entry_script_sh +++ b/share/container/entry_script_sh @@ -26,8 +26,8 @@ chmod -R 2770 /var/lib/torrust /var/log/torrust /etc/torrust # Install the database and config: -if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then - if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "Sqlite3"; then +if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" ]; then + if cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" "Sqlite3"; then # Select Sqlite3 empty database default_database="/usr/share/torrust/default/database/tracker.sqlite3.db" @@ -35,7 +35,7 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then # Select Sqlite3 default configuration default_config="/usr/share/torrust/default/config/tracker.container.sqlite3.toml" - elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" "MySQL"; then + elif cmp_lc "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER" "MySQL"; then # (no database file needed for MySQL) @@ -43,12 +43,12 @@ if [ -n "$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER" ]; then default_config="/usr/share/torrust/default/config/tracker.container.mysql.toml" else - echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\"." + echo "Error: Unsupported Database Type: \"$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER\"." echo "Please Note: Supported Database Types: \"Sqlite3\", \"MySQL\"." exit 1 fi else - echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DB_DRIVER\" was not set!"; exit 1; + echo "Error: \"\$TORRUST_TRACKER_CONFIG_OVERRIDE_CORE__DATABASE__DRIVER\" was not set!"; exit 1; fi install_config="/etc/torrust/tracker/tracker.toml" diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 617450562..75cc57b64 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -1,6 +1,6 @@ -[core] -db_driver = "MySQL" -db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" +[core.database] +driver = "MySQL" +path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" [[http_trackers]] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 01ca655c3..433e36127 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,5 +1,5 @@ -[core] -db_path = "/var/lib/torrust/tracker/database/sqlite3.db" +[core.database] +path = "/var/lib/torrust/tracker/database/sqlite3.db" [[http_trackers]] ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 60d7a798a..b8adedefb 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -1,5 +1,5 @@ -[core] -db_path = "/var/lib/torrust/tracker/database/sqlite3.db" +[core.database] +path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] enabled = true diff --git a/src/console/ci/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs index a4024f29d..4886786de 100644 --- a/src/console/ci/e2e/logs_parser.rs +++ b/src/console/ci/e2e/logs_parser.rs @@ -112,21 +112,7 @@ mod tests { #[test] fn it_should_parse_from_logs_with_valid_logs() { - let logs = r#" - Loading configuration from environment variable db_path = "/var/lib/torrust/tracker/database/sqlite3.db" - - [[udp_trackers]] - enabled = true - - [[http_trackers]] - enabled = true - ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" - ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - - [http_api] - ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" - ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - + let logs = r" Loading configuration from default configuration file: `./share/default/config/tracker.development.sqlite3.toml` ... 2024-06-10T16:07:39.989540Z INFO torrust_tracker::bootstrap::logging: logging initialized. 2024-06-10T16:07:39.990244Z INFO UDP TRACKER: Starting on: udp://0.0.0.0:6969 @@ -139,7 +125,7 @@ mod tests { 2024-06-10T16:07:39.990565Z INFO API: Started on http://127.0.0.1:1212 2024-06-10T16:07:39.990577Z INFO HEALTH CHECK API: Starting on: http://127.0.0.1:1313 2024-06-10T16:07:39.990638Z INFO HEALTH CHECK API: Started on: http://127.0.0.1:1313 - "#; + "; let running_services = RunningServices::parse_from_logs(logs); diff --git a/src/core/mod.rs b/src/core/mod.rs index 6da58e236..3e2903ac7 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -317,8 +317,6 @@ //! //! [core] //! mode = "public" -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! announce_interval = 120 //! min_announce_interval = 120 //! max_peer_timeout = 900 @@ -328,6 +326,10 @@ //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false +//! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" //! ``` //! //! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. @@ -550,7 +552,7 @@ impl Tracker { stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Arc::new(databases::driver::build(&config.db_driver, &config.db_path)?); + let database = Arc::new(databases::driver::build(&config.database.driver, &config.database.path)?); let mode = config.mode.clone(); diff --git a/src/lib.rs b/src/lib.rs index 2ed88a68b..b94da4717 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,8 +172,6 @@ //! //! [core] //! announce_interval = 120 -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" //! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 @@ -184,6 +182,10 @@ //! remove_peerless_torrents = true //! tracker_usage_statistics = true //! +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" //! enabled = false From 36c7ed1659bd002e32881381cc81130e9c6312c2 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 16:59:42 +0100 Subject: [PATCH 212/217] feat!: [#878] extract net section in core config section --- packages/configuration/src/v1/core.rs | 55 ++++++++-------------- packages/configuration/src/v1/mod.rs | 34 ++++++++----- packages/configuration/src/v1/network.rs | 41 ++++++++++++++++ packages/test-helpers/src/configuration.rs | 6 +-- src/core/mod.rs | 16 ++++--- src/lib.rs | 6 ++- src/servers/http/v1/services/announce.rs | 2 +- src/servers/udp/v0/handlers.rs | 2 +- 8 files changed, 99 insertions(+), 63 deletions(-) create mode 100644 packages/configuration/src/v1/network.rs diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 17ac36ee0..5d8ab0b33 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -1,8 +1,7 @@ -use std::net::{IpAddr, Ipv4Addr}; - use serde::{Deserialize, Serialize}; use torrust_tracker_primitives::TrackerMode; +use super::network::Network; use crate::v1::database::Database; use crate::AnnouncePolicy; @@ -13,10 +12,6 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - // Database configuration. - #[serde(default = "Core::default_database")] - pub database: Database, - /// See [`AnnouncePolicy::interval`] #[serde(default = "AnnouncePolicy::default_interval")] pub announce_interval: u32, @@ -25,20 +20,6 @@ pub struct Core { #[serde(default = "AnnouncePolicy::default_interval_min")] pub min_announce_interval: u32, - /// Weather the tracker is behind a reverse proxy or not. - /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header - /// sent from the proxy will be used to get the client's IP address. - #[serde(default = "Core::default_on_reverse_proxy")] - pub on_reverse_proxy: bool, - - /// The external IP address of the tracker. If the client is using a - /// loopback IP address, this IP address will be used instead. If the peer - /// is using a loopback IP address, the tracker assumes that the peer is - /// in the same network as the tracker and will use the tracker's IP - /// address instead. - #[serde(default = "Core::default_external_ip")] - pub external_ip: Option, - /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -71,6 +52,14 @@ pub struct Core { /// enabled. #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, + + // Database configuration. + #[serde(default = "Core::default_database")] + pub database: Database, + + // Network configuration. + #[serde(default = "Core::default_network")] + pub net: Network, } impl Default for Core { @@ -79,16 +68,15 @@ impl Default for Core { Self { mode: Self::default_mode(), - database: Self::default_database(), announce_interval: announce_policy.interval, min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), - on_reverse_proxy: Self::default_on_reverse_proxy(), - external_ip: Self::default_external_ip(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), remove_peerless_torrents: Self::default_remove_peerless_torrents(), + database: Self::default_database(), + net: Self::default_network(), } } } @@ -98,19 +86,6 @@ impl Core { TrackerMode::Public } - fn default_database() -> Database { - Database::default() - } - - fn default_on_reverse_proxy() -> bool { - false - } - - #[allow(clippy::unnecessary_wraps)] - fn default_external_ip() -> Option { - Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) - } - fn default_tracker_usage_statistics() -> bool { true } @@ -130,4 +105,12 @@ impl Core { fn default_remove_peerless_torrents() -> bool { true } + + fn default_database() -> Database { + Database::default() + } + + fn default_network() -> Network { + Network::default() + } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 5b3cad3ea..d96e1335c 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -200,17 +200,19 @@ //! mode = "public" //! announce_interval = 120 //! min_announce_interval = 120 -//! on_reverse_proxy = false -//! external_ip = "0.0.0.0" //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = false //! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false //! //! [[udp_trackers]] //! enabled = false @@ -240,6 +242,7 @@ pub mod database; pub mod health_check_api; pub mod http_tracker; pub mod logging; +pub mod network; pub mod tracker_api; pub mod udp_tracker; @@ -307,7 +310,7 @@ impl Configuration { /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { - self.core.external_ip.as_ref().map(|external_ip| *external_ip) + self.core.net.external_ip.as_ref().map(|external_ip| *external_ip) } /// Saves the default configuration at the given path. @@ -387,18 +390,20 @@ mod tests { mode = "public" announce_interval = 120 min_announce_interval = 120 - on_reverse_proxy = false - external_ip = "0.0.0.0" tracker_usage_statistics = true persistent_torrent_completed_stat = false max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true - [core.database] - driver = "Sqlite3" - path = "./storage/tracker/lib/database/sqlite3.db" - + [core.database] + driver = "Sqlite3" + path = "./storage/tracker/lib/database/sqlite3.db" + + [core.net] + external_ip = "0.0.0.0" + on_reverse_proxy = false + [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" @@ -443,7 +448,10 @@ mod tests { fn configuration_should_contain_the_external_ip() { let configuration = Configuration::default(); - assert_eq!(configuration.core.external_ip, Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))); + assert_eq!( + configuration.core.net.external_ip, + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + ); } #[test] diff --git a/packages/configuration/src/v1/network.rs b/packages/configuration/src/v1/network.rs new file mode 100644 index 000000000..8e53d419c --- /dev/null +++ b/packages/configuration/src/v1/network.rs @@ -0,0 +1,41 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use serde::{Deserialize, Serialize}; + +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Network { + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + #[serde(default = "Network::default_external_ip")] + pub external_ip: Option, + + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + #[serde(default = "Network::default_on_reverse_proxy")] + pub on_reverse_proxy: bool, +} + +impl Default for Network { + fn default() -> Self { + Self { + external_ip: Self::default_external_ip(), + on_reverse_proxy: Self::default_on_reverse_proxy(), + } + } +} + +impl Network { + #[allow(clippy::unnecessary_wraps)] + fn default_external_ip() -> Option { + Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) + } + + fn default_on_reverse_proxy() -> bool { + false + } +} diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index fe05407d9..9c6c0fe11 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -64,7 +64,7 @@ pub fn ephemeral() -> Configuration { pub fn ephemeral_with_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.core.on_reverse_proxy = true; + cfg.core.net.on_reverse_proxy = true; cfg } @@ -74,7 +74,7 @@ pub fn ephemeral_with_reverse_proxy() -> Configuration { pub fn ephemeral_without_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); - cfg.core.on_reverse_proxy = false; + cfg.core.net.on_reverse_proxy = false; cfg } @@ -124,7 +124,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); - cfg.core.external_ip = Some(ip); + cfg.core.net.external_ip = Some(ip); cfg } diff --git a/src/core/mod.rs b/src/core/mod.rs index 3e2903ac7..1e7083ddb 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -320,16 +320,18 @@ //! announce_interval = 120 //! min_announce_interval = 120 //! max_peer_timeout = 900 -//! on_reverse_proxy = false -//! external_ip = "2.137.87.41" //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! on_reverse_proxy = false +//! external_ip = "2.137.87.41" //! ``` //! //! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. @@ -566,13 +568,13 @@ impl Tracker { stats_event_sender, stats_repository, database, - external_ip: config.external_ip, + external_ip: config.net.external_ip, policy: TrackerPolicy::new( config.remove_peerless_torrents, config.max_peer_timeout, config.persistent_torrent_completed_stat, ), - on_reverse_proxy: config.on_reverse_proxy, + on_reverse_proxy: config.net.on_reverse_proxy, }) } diff --git a/src/lib.rs b/src/lib.rs index b94da4717..bf9257123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,12 +172,10 @@ //! //! [core] //! announce_interval = 120 -//! external_ip = "0.0.0.0" //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 //! min_announce_interval = 120 //! mode = "public" -//! on_reverse_proxy = false //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! tracker_usage_statistics = true @@ -186,6 +184,10 @@ //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" //! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false +//! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" //! enabled = false diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 253140fbc..eee5e4688 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -151,7 +151,7 @@ mod tests { fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.core.external_ip = Some(IpAddr::V6(Ipv6Addr::new( + configuration.core.net.external_ip = Some(IpAddr::V6(Ipv6Addr::new( 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, ))); diff --git a/src/servers/udp/v0/handlers.rs b/src/servers/udp/v0/handlers.rs index f1d16cb33..ad438967a 100644 --- a/src/servers/udp/v0/handlers.rs +++ b/src/servers/udp/v0/handlers.rs @@ -425,7 +425,7 @@ mod tests { } pub fn with_external_ip(mut self, external_ip: &str) -> Self { - self.configuration.core.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); + self.configuration.core.net.external_ip = Some(external_ip.to_owned().parse().expect("valid IP address")); self } From 063caf3c5d854ba90cf92d348c7ea4b60c295603 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 17:21:03 +0100 Subject: [PATCH 213/217] feat!: [#878] extract announce_policy section in core config section --- packages/configuration/src/lib.rs | 4 +++- packages/configuration/src/v1/core.rs | 21 +++++++++------------ packages/configuration/src/v1/mod.rs | 12 ++++++++---- src/core/mod.rs | 14 ++++++++------ src/lib.rs | 18 ++++++++++-------- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 83d02a79d..fe31b6848 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -96,7 +96,7 @@ impl Info { } /// Announce policy -#[derive(PartialEq, Eq, Debug, Clone, Copy, Constructor)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, Constructor)] pub struct AnnouncePolicy { /// Interval in seconds that the client should wait between sending regular /// announce requests to the tracker. @@ -109,6 +109,7 @@ pub struct AnnouncePolicy { /// client's initial request. It serves as a guideline for clients to know /// how often they should contact the tracker for updates on the peer list, /// while ensuring that the tracker is not overwhelmed with requests. + #[serde(default = "AnnouncePolicy::default_interval")] pub interval: u32, /// Minimum announce interval. Clients must not reannounce more frequently @@ -122,6 +123,7 @@ pub struct AnnouncePolicy { /// value to prevent sending too many requests in a short period, which /// could lead to excessive load on the tracker or even getting banned by /// the tracker for not adhering to the rules. + #[serde(default = "AnnouncePolicy::default_interval_min")] pub interval_min: u32, } diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 5d8ab0b33..266da21ed 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -12,14 +12,6 @@ pub struct Core { #[serde(default = "Core::default_mode")] pub mode: TrackerMode, - /// See [`AnnouncePolicy::interval`] - #[serde(default = "AnnouncePolicy::default_interval")] - pub announce_interval: u32, - - /// See [`AnnouncePolicy::interval_min`] - #[serde(default = "AnnouncePolicy::default_interval_min")] - pub min_announce_interval: u32, - /// Weather the tracker should collect statistics about tracker usage. /// If enabled, the tracker will collect statistics like the number of /// connections handled, the number of announce requests handled, etc. @@ -53,6 +45,10 @@ pub struct Core { #[serde(default = "Core::default_remove_peerless_torrents")] pub remove_peerless_torrents: bool, + // Announce policy configuration. + #[serde(default = "Core::default_announce_policy")] + pub announce_policy: AnnouncePolicy, + // Database configuration. #[serde(default = "Core::default_database")] pub database: Database, @@ -64,17 +60,14 @@ pub struct Core { impl Default for Core { fn default() -> Self { - let announce_policy = AnnouncePolicy::default(); - Self { mode: Self::default_mode(), - announce_interval: announce_policy.interval, - min_announce_interval: announce_policy.interval_min, max_peer_timeout: Self::default_max_peer_timeout(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), remove_peerless_torrents: Self::default_remove_peerless_torrents(), + announce_policy: Self::default_announce_policy(), database: Self::default_database(), net: Self::default_network(), } @@ -106,6 +99,10 @@ impl Core { true } + fn default_announce_policy() -> AnnouncePolicy { + AnnouncePolicy::default() + } + fn default_database() -> Database { Database::default() } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d96e1335c..d2f2a3012 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -198,14 +198,16 @@ //! //! [core] //! mode = "public" -//! announce_interval = 120 -//! min_announce_interval = 120 //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = false //! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = true //! +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 +//! //! [core.database] //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" @@ -388,14 +390,16 @@ mod tests { [core] mode = "public" - announce_interval = 120 - min_announce_interval = 120 tracker_usage_statistics = true persistent_torrent_completed_stat = false max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true + [core.announce_policy] + interval = 120 + interval_min = 120 + [core.database] driver = "Sqlite3" path = "./storage/tracker/lib/database/sqlite3.db" diff --git a/src/core/mod.rs b/src/core/mod.rs index 1e7083ddb..6b3c2421f 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -113,10 +113,10 @@ //! } //! //! // Core tracker configuration -//! pub struct Configuration { +//! pub struct AnnounceInterval { //! // ... -//! pub announce_interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker -//! pub min_announce_interval: u32, // Minimum announce interval. Clients must not reannounce more frequently than this +//! pub interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker +//! pub interval_min: u32, // Minimum announce interval. Clients must not reannounce more frequently than this //! // ... //! } //! ``` @@ -317,14 +317,16 @@ //! //! [core] //! mode = "public" -//! announce_interval = 120 -//! min_announce_interval = 120 //! max_peer_timeout = 900 //! tracker_usage_statistics = true //! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 //! remove_peerless_torrents = false //! +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 +//! //! [core.database] //! driver = "Sqlite3" //! path = "./storage/tracker/lib/database/sqlite3.db" @@ -560,7 +562,7 @@ impl Tracker { Ok(Tracker { //config, - announce_policy: AnnouncePolicy::new(config.announce_interval, config.min_announce_interval), + announce_policy: config.announce_policy, mode, keys: tokio::sync::RwLock::new(std::collections::HashMap::new()), whitelist: tokio::sync::RwLock::new(std::collections::HashSet::new()), diff --git a/src/lib.rs b/src/lib.rs index bf9257123..5c0fd4b56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,22 +171,24 @@ //! log_level = "info" //! //! [core] -//! announce_interval = 120 //! inactive_peer_cleanup_interval = 600 //! max_peer_timeout = 900 -//! min_announce_interval = 120 //! mode = "public" //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! tracker_usage_statistics = true //! -//! [core.database] -//! driver = "Sqlite3" -//! path = "./storage/tracker/lib/database/sqlite3.db" +//! [core.announce_policy] +//! interval = 120 +//! interval_min = 120 //! -//! [core.net] -//! external_ip = "0.0.0.0" -//! on_reverse_proxy = false +//! [core.database] +//! driver = "Sqlite3" +//! path = "./storage/tracker/lib/database/sqlite3.db" +//! +//! [core.net] +//! external_ip = "0.0.0.0" +//! on_reverse_proxy = false //! //! [[udp_trackers]] //! bind_address = "0.0.0.0:6969" From 2708f5d7d7616a27941d1e6040ac922a502db577 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 18:19:57 +0100 Subject: [PATCH 214/217] feat!: [#878] extract tracker_policy section in core config section --- packages/configuration/src/lib.rs | 44 ++++++++++++++++++- packages/configuration/src/v1/core.rs | 40 +++-------------- packages/configuration/src/v1/mod.rs | 12 +++-- .../torrent-repository/tests/entry/mod.rs | 8 ++-- .../tests/repository/mod.rs | 8 ++-- src/core/mod.rs | 16 +++---- src/lib.rs | 6 ++- 7 files changed, 76 insertions(+), 58 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index fe31b6848..18f365470 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -47,11 +47,51 @@ pub type HealthCheckApi = v1::health_check_api::HealthCheckApi; pub type AccessTokens = HashMap; -#[derive(Copy, Clone, Debug, PartialEq, Constructor)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Constructor)] pub struct TrackerPolicy { - pub remove_peerless_torrents: bool, + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + #[serde(default = "TrackerPolicy::default_max_peer_timeout")] pub max_peer_timeout: u32, + + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + #[serde(default = "TrackerPolicy::default_persistent_torrent_completed_stat")] pub persistent_torrent_completed_stat: bool, + + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + #[serde(default = "TrackerPolicy::default_remove_peerless_torrents")] + pub remove_peerless_torrents: bool, +} + +impl Default for TrackerPolicy { + fn default() -> Self { + Self { + max_peer_timeout: Self::default_max_peer_timeout(), + persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), + remove_peerless_torrents: Self::default_remove_peerless_torrents(), + } + } +} + +impl TrackerPolicy { + fn default_max_peer_timeout() -> u32 { + 900 + } + + fn default_persistent_torrent_completed_stat() -> bool { + false + } + + fn default_remove_peerless_torrents() -> bool { + true + } } /// Information required for loading config diff --git a/packages/configuration/src/v1/core.rs b/packages/configuration/src/v1/core.rs index 266da21ed..49fdf2a80 100644 --- a/packages/configuration/src/v1/core.rs +++ b/packages/configuration/src/v1/core.rs @@ -3,7 +3,7 @@ use torrust_tracker_primitives::TrackerMode; use super::network::Network; use crate::v1::database::Database; -use crate::AnnouncePolicy; +use crate::{AnnouncePolicy, TrackerPolicy}; #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] @@ -20,30 +20,14 @@ pub struct Core { #[serde(default = "Core::default_tracker_usage_statistics")] pub tracker_usage_statistics: bool, - /// If enabled the tracker will persist the number of completed downloads. - /// That's how many times a torrent has been downloaded completely. - #[serde(default = "Core::default_persistent_torrent_completed_stat")] - pub persistent_torrent_completed_stat: bool, - - // Cleanup job configuration - /// Maximum time in seconds that a peer can be inactive before being - /// considered an inactive peer. If a peer is inactive for more than this - /// time, it will be removed from the torrent peer list. - #[serde(default = "Core::default_max_peer_timeout")] - pub max_peer_timeout: u32, - /// Interval in seconds that the cleanup job will run to remove inactive /// peers from the torrent peer list. #[serde(default = "Core::default_inactive_peer_cleanup_interval")] pub inactive_peer_cleanup_interval: u64, - /// If enabled, the tracker will remove torrents that have no peers. - /// The clean up torrent job runs every `inactive_peer_cleanup_interval` - /// seconds and it removes inactive peers. Eventually, the peer list of a - /// torrent could be empty and the torrent will be removed if this option is - /// enabled. - #[serde(default = "Core::default_remove_peerless_torrents")] - pub remove_peerless_torrents: bool, + // Tracker policy configuration. + #[serde(default = "Core::default_tracker_policy")] + pub tracker_policy: TrackerPolicy, // Announce policy configuration. #[serde(default = "Core::default_announce_policy")] @@ -62,11 +46,9 @@ impl Default for Core { fn default() -> Self { Self { mode: Self::default_mode(), - max_peer_timeout: Self::default_max_peer_timeout(), tracker_usage_statistics: Self::default_tracker_usage_statistics(), - persistent_torrent_completed_stat: Self::default_persistent_torrent_completed_stat(), inactive_peer_cleanup_interval: Self::default_inactive_peer_cleanup_interval(), - remove_peerless_torrents: Self::default_remove_peerless_torrents(), + tracker_policy: Self::default_tracker_policy(), announce_policy: Self::default_announce_policy(), database: Self::default_database(), net: Self::default_network(), @@ -83,20 +65,12 @@ impl Core { true } - fn default_persistent_torrent_completed_stat() -> bool { - false - } - - fn default_max_peer_timeout() -> u32 { - 900 - } - fn default_inactive_peer_cleanup_interval() -> u64 { 600 } - fn default_remove_peerless_torrents() -> bool { - true + fn default_tracker_policy() -> TrackerPolicy { + TrackerPolicy::default() } fn default_announce_policy() -> AnnouncePolicy { diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index d2f2a3012..19499cd4a 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -199,9 +199,11 @@ //! [core] //! mode = "public" //! tracker_usage_statistics = true -//! persistent_torrent_completed_stat = false -//! max_peer_timeout = 900 //! inactive_peer_cleanup_interval = 600 +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 +//! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true //! //! [core.announce_policy] @@ -391,9 +393,11 @@ mod tests { [core] mode = "public" tracker_usage_statistics = true - persistent_torrent_completed_stat = false - max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 + + [core.tracker_policy] + max_peer_timeout = 900 + persistent_torrent_completed_stat = false remove_peerless_torrents = true [core.announce_policy] diff --git a/packages/torrent-repository/tests/entry/mod.rs b/packages/torrent-repository/tests/entry/mod.rs index 3b9f3e3ad..fdbe211b3 100644 --- a/packages/torrent-repository/tests/entry/mod.rs +++ b/packages/torrent-repository/tests/entry/mod.rs @@ -43,22 +43,22 @@ fn rw_lock_parking_lot() -> Torrent { #[fixture] fn policy_none() -> TrackerPolicy { - TrackerPolicy::new(false, 0, false) + TrackerPolicy::new(0, false, false) } #[fixture] fn policy_persist() -> TrackerPolicy { - TrackerPolicy::new(false, 0, true) + TrackerPolicy::new(0, true, false) } #[fixture] fn policy_remove() -> TrackerPolicy { - TrackerPolicy::new(true, 0, false) + TrackerPolicy::new(0, false, true) } #[fixture] fn policy_remove_persist() -> TrackerPolicy { - TrackerPolicy::new(true, 0, true) + TrackerPolicy::new(0, true, true) } pub enum Makes { diff --git a/packages/torrent-repository/tests/repository/mod.rs b/packages/torrent-repository/tests/repository/mod.rs index 53c19fcdb..9107259cf 100644 --- a/packages/torrent-repository/tests/repository/mod.rs +++ b/packages/torrent-repository/tests/repository/mod.rs @@ -220,22 +220,22 @@ fn paginated_limit_one_offset_one() -> Pagination { #[fixture] fn policy_none() -> TrackerPolicy { - TrackerPolicy::new(false, 0, false) + TrackerPolicy::new(0, false, false) } #[fixture] fn policy_persist() -> TrackerPolicy { - TrackerPolicy::new(false, 0, true) + TrackerPolicy::new(0, true, false) } #[fixture] fn policy_remove() -> TrackerPolicy { - TrackerPolicy::new(true, 0, false) + TrackerPolicy::new(0, false, true) } #[fixture] fn policy_remove_persist() -> TrackerPolicy { - TrackerPolicy::new(true, 0, true) + TrackerPolicy::new(0, true, true) } #[rstest] diff --git a/src/core/mod.rs b/src/core/mod.rs index 6b3c2421f..1fe8e382b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -317,11 +317,13 @@ //! //! [core] //! mode = "public" -//! max_peer_timeout = 900 //! tracker_usage_statistics = true -//! persistent_torrent_completed_stat = true //! inactive_peer_cleanup_interval = 600 -//! remove_peerless_torrents = false +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 +//! persistent_torrent_completed_stat = false +//! remove_peerless_torrents = true //! //! [core.announce_policy] //! interval = 120 @@ -571,11 +573,7 @@ impl Tracker { stats_repository, database, external_ip: config.net.external_ip, - policy: TrackerPolicy::new( - config.remove_peerless_torrents, - config.max_peer_timeout, - config.persistent_torrent_completed_stat, - ), + policy: config.tracker_policy.clone(), on_reverse_proxy: config.net.on_reverse_proxy, }) } @@ -1045,7 +1043,7 @@ mod tests { pub fn tracker_persisting_torrents_in_database() -> Tracker { let mut configuration = configuration::ephemeral(); - configuration.core.persistent_torrent_completed_stat = true; + configuration.core.tracker_policy.persistent_torrent_completed_stat = true; tracker_factory(&configuration) } diff --git a/src/lib.rs b/src/lib.rs index 5c0fd4b56..c059f6df7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,11 +172,13 @@ //! //! [core] //! inactive_peer_cleanup_interval = 600 -//! max_peer_timeout = 900 //! mode = "public" +//! tracker_usage_statistics = true +//! +//! [core.tracker_policy] +//! max_peer_timeout = 900 //! persistent_torrent_completed_stat = false //! remove_peerless_torrents = true -//! tracker_usage_statistics = true //! //! [core.announce_policy] //! interval = 120 From 6d07cf450f4eef141f15a41520fff7bac5884314 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 17 Jun 2024 19:34:56 +0100 Subject: [PATCH 215/217] feat: [#878] extract tsl_config in toml config TSL configuration for HTTP trackers and the Tracker API is still optional. However, when it's provided is enabled. The `ssl_enabled` field was removed. You can remove the whole `tsl_config` to disable TSL. If you want to kee a copy in the TOML file you can just comment the lines. ```toml [[http_trackers]] ... [http_trackers.tsl_config] ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" ssl_key_path = "./storage/tracker/lib/tls/localhost.key" [http_api] ... [http_api.tsl_config] ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" ssl_key_path = "./storage/tracker/lib/tls/localhost.key" ``` --- Cargo.toml | 2 +- docs/containers.md | 23 +++-- packages/configuration/Cargo.toml | 2 +- packages/configuration/src/lib.rs | 17 ++-- packages/configuration/src/v1/http_tracker.rs | 19 ++-- packages/configuration/src/v1/mod.rs | 20 ++--- packages/configuration/src/v1/tracker_api.rs | 19 ++-- .../config/tracker.container.mysql.toml | 8 +- .../config/tracker.container.sqlite3.toml | 8 -- .../config/tracker.e2e.container.sqlite3.toml | 6 -- src/bootstrap/jobs/http_tracker.rs | 11 ++- src/bootstrap/jobs/mod.rs | 86 ++++++------------- src/bootstrap/jobs/tracker_apis.rs | 11 ++- src/lib.rs | 9 -- src/servers/apis/mod.rs | 12 +-- src/servers/apis/server.rs | 11 ++- src/servers/http/launcher.rs | 17 ++-- tests/servers/api/environment.rs | 5 +- tests/servers/http/environment.rs | 5 +- 19 files changed, 119 insertions(+), 172 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57e213599..06c02fc4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ axum = { version = "0", features = ["macros"] } axum-client-ip = "0" axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } -camino = { version = "1.1.6", features = ["serde"] } +camino = { version = "1.1.6", features = ["serde", "serde1"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0.1" diff --git a/docs/containers.md b/docs/containers.md index 1a1ea2f0d..82c67c26e 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -330,24 +330,23 @@ The storage folder must contain your certificates: ```s storage/tracker/lib/tls - ├── localhost.crt - └── localhost.key + ├── localhost.crt + └── localhost.key +storage/http_api/lib/tls + ├── localhost.crt + └── localhost.key ``` You have not enabled it in your `tracker.toml` file: ```toml +[http_trackers.tsl_config] +ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +ssl_key_path = "./storage/tracker/lib/tls/localhost.key" -[[http_trackers]] -# ... -ssl_enabled = true -# ... - -[http_api] -# ... -ssl_enabled = true -# ... - +[http_api.tsl_config] +ssl_cert_path = "./storage/http_api/lib/tls/localhost.crt" +ssl_key_path = "./storage/http_api/lib/tls/localhost.key" ``` > NOTE: you can enable it independently for each HTTP tracker or the API. diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index bac2132d5..53e4e4cfa 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,7 +15,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -camino = { version = "1.1.6", features = ["serde"] } +camino = { version = "1.1.6", features = ["serde", "serde1"] } derive_more = "0" figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 18f365470..14a108d36 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -14,7 +14,7 @@ use std::time::Duration; use camino::Utf8PathBuf; use derive_more::Constructor; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; +use serde_with::serde_as; use thiserror::Error; use torrust_tracker_located_error::{DynError, LocatedError}; @@ -225,24 +225,23 @@ impl From for Error { #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] pub struct TslConfig { /// Path to the SSL certificate file. - #[serde_as(as = "NoneAsEmptyString")] #[serde(default = "TslConfig::default_ssl_cert_path")] - pub ssl_cert_path: Option, + pub ssl_cert_path: Utf8PathBuf, + /// Path to the SSL key file. - #[serde_as(as = "NoneAsEmptyString")] #[serde(default = "TslConfig::default_ssl_key_path")] - pub ssl_key_path: Option, + pub ssl_key_path: Utf8PathBuf, } impl TslConfig { #[allow(clippy::unnecessary_wraps)] - fn default_ssl_cert_path() -> Option { - Some(Utf8PathBuf::new()) + fn default_ssl_cert_path() -> Utf8PathBuf { + Utf8PathBuf::new() } #[allow(clippy::unnecessary_wraps)] - fn default_ssl_key_path() -> Option { - Some(Utf8PathBuf::new()) + fn default_ssl_key_path() -> Utf8PathBuf { + Utf8PathBuf::new() } } diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index b1fe1437b..fed2282a5 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -12,19 +12,17 @@ pub struct HttpTracker { /// Weather the HTTP tracker is enabled or not. #[serde(default = "HttpTracker::default_enabled")] pub enabled: bool, + /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. #[serde(default = "HttpTracker::default_bind_address")] pub bind_address: SocketAddr, - /// Weather the HTTP tracker will use SSL or not. - #[serde(default = "HttpTracker::default_ssl_enabled")] - pub ssl_enabled: bool, - /// TSL config. Only used if `ssl_enabled` is true. - #[serde(flatten)] - #[serde(default = "TslConfig::default")] - pub tsl_config: TslConfig, + + /// TSL config. + #[serde(default = "HttpTracker::default_tsl_config")] + pub tsl_config: Option, } impl Default for HttpTracker { @@ -32,8 +30,7 @@ impl Default for HttpTracker { Self { enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), - ssl_enabled: Self::default_ssl_enabled(), - tsl_config: TslConfig::default(), + tsl_config: Self::default_tsl_config(), } } } @@ -47,7 +44,7 @@ impl HttpTracker { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) } - fn default_ssl_enabled() -> bool { - false + fn default_tsl_config() -> Option { + None } } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 19499cd4a..603be85d2 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -176,14 +176,16 @@ //! //! ```s //! [[http_trackers]] -//! enabled = true //! ... +//! +//! [http_trackers.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! //! [http_api] -//! enabled = true //! ... +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! ``` @@ -225,16 +227,10 @@ //! [[http_trackers]] //! enabled = false //! bind_address = "0.0.0.0:7070" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! //! [http_api] //! enabled = true //! bind_address = "127.0.0.1:1212" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! //! [http_api.access_tokens] //! admin = "MyAccessToken" @@ -419,16 +415,10 @@ mod tests { [[http_trackers]] enabled = false bind_address = "0.0.0.0:7070" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" - + [http_api] enabled = true bind_address = "127.0.0.1:1212" - ssl_enabled = false - ssl_cert_path = "" - ssl_key_path = "" [http_api.access_tokens] admin = "MyAccessToken" diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index c2e3e5ee9..42794ad18 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -15,19 +15,18 @@ pub struct HttpApi { /// Weather the HTTP API is enabled or not. #[serde(default = "HttpApi::default_enabled")] pub enabled: bool, + /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating /// system to choose a random port, use port `0`. #[serde(default = "HttpApi::default_bind_address")] pub bind_address: SocketAddr, - /// Weather the HTTP API will use SSL or not. - #[serde(default = "HttpApi::default_ssl_enabled")] - pub ssl_enabled: bool, + /// TSL config. Only used if `ssl_enabled` is true. - #[serde(flatten)] - #[serde(default = "TslConfig::default")] - pub tsl_config: TslConfig, + #[serde(default = "HttpApi::default_tsl_config")] + pub tsl_config: Option, + /// Access tokens for the HTTP API. The key is a label identifying the /// token and the value is the token itself. The token is used to /// authenticate the user. All tokens are valid for all endpoints and have @@ -41,8 +40,7 @@ impl Default for HttpApi { Self { enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), - ssl_enabled: Self::default_ssl_enabled(), - tsl_config: TslConfig::default(), + tsl_config: Self::default_tsl_config(), access_tokens: Self::default_access_tokens(), } } @@ -57,8 +55,9 @@ impl HttpApi { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) } - fn default_ssl_enabled() -> bool { - false + #[allow(clippy::unnecessary_wraps)] + fn default_tsl_config() -> Option { + None } fn default_access_tokens() -> AccessTokens { diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 75cc57b64..70ee8b500 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -3,9 +3,5 @@ driver = "MySQL" path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" [[http_trackers]] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" +bind_address = "0.0.0.0:7070" +enabled = true diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 433e36127..f7bb6b8bb 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,10 +1,2 @@ [core.database] path = "/var/lib/torrust/tracker/database/sqlite3.db" - -[[http_trackers]] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index b8adedefb..744d267fd 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -6,12 +6,6 @@ enabled = true [[http_trackers]] enabled = true -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" - -[http_api] -ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" -ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" [health_check_api] # Must be bound to wildcard IP to be accessible from outside the container diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index b1193b7bf..491cdfb3c 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -45,9 +45,14 @@ pub async fn start_job( if config.enabled { let socket = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) - .await - .map(|tls| tls.expect("it should have a valid http tracker tls configuration")); + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker https configuration"), + ), + None => None, + }; match version { Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index 790b3d22a..397146eb0 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -51,52 +51,28 @@ impl From for Error { } #[instrument(ret)] -pub async fn make_rust_tls(enabled: bool, tsl_config: &TslConfig) -> Option> { - if let (Some(cert), Some(key)) = (tsl_config.ssl_cert_path.clone(), tsl_config.ssl_key_path.clone()) { - info!("Using https: cert path: {cert}."); - info!("Using https: key path: {key}."); - - Some( - RustlsConfig::from_pem_file(cert, key) - .await - .map_err(|err| Error::BadTlsConfig { - source: (Arc::new(err) as DynError).into(), - }), - ) - } else { - Some(Err(Error::MissingTlsConfig { - location: Location::caller(), - })) - } +pub async fn make_rust_tls(tsl_config: &TslConfig) -> Result { + let (cert, key) = (tsl_config.ssl_cert_path.clone(), tsl_config.ssl_key_path.clone()); + info!("Using https: cert path: {cert}."); + info!("Using https: key path: {key}."); + + RustlsConfig::from_pem_file(cert, key) + .await + .map_err(|err| Error::BadTlsConfig { + source: (Arc::new(err) as DynError).into(), + }) } #[instrument(ret)] -pub async fn make_rust_tls_from_path_buf( - enabled: bool, - cert: &Option, - key: &Option, -) -> Option> { - if !enabled { - info!("TLS not enabled"); - return None; - } +pub async fn make_rust_tls_from_path_buf(cert: &Utf8PathBuf, key: &Utf8PathBuf) -> Result { + info!("Using https: cert path: {cert}."); + info!("Using https: key path: {key}."); - if let (Some(cert), Some(key)) = (cert, key) { - info!("Using https: cert path: {cert}."); - info!("Using https: key path: {key}."); - - Some( - RustlsConfig::from_pem_file(cert, key) - .await - .map_err(|err| Error::BadTlsConfig { - source: (Arc::new(err) as DynError).into(), - }), - ) - } else { - Some(Err(Error::MissingTlsConfig { - location: Location::caller(), - })) - } + RustlsConfig::from_pem_file(cert, key) + .await + .map_err(|err| Error::BadTlsConfig { + source: (Arc::new(err) as DynError).into(), + }) } #[cfg(test)] @@ -109,31 +85,23 @@ mod tests { #[tokio::test] async fn it_should_error_on_bad_tls_config() { - let err = make_rust_tls( - true, - &TslConfig { - ssl_cert_path: Some(Utf8PathBuf::from("bad cert path")), - ssl_key_path: Some(Utf8PathBuf::from("bad key path")), - }, - ) + let err = make_rust_tls(&TslConfig { + ssl_cert_path: Utf8PathBuf::from("bad cert path"), + ssl_key_path: Utf8PathBuf::from("bad key path"), + }) .await - .expect("tls_was_enabled") .expect_err("bad_cert_and_key_files"); - assert!(matches!(err, Error::BadTlsConfig { source: _ })); + assert!(matches!(err, Error::MissingTlsConfig { location: _ })); } #[tokio::test] async fn it_should_error_on_missing_cert_or_key_paths() { - let err = make_rust_tls( - true, - &TslConfig { - ssl_cert_path: None, - ssl_key_path: None, - }, - ) + let err = make_rust_tls(&TslConfig { + ssl_cert_path: Utf8PathBuf::from(""), + ssl_key_path: Utf8PathBuf::from(""), + }) .await - .expect("tls_was_enabled") .expect_err("missing_config"); assert!(matches!(err, Error::MissingTlsConfig { location: _ })); diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 4aa3fd1f3..12d18a688 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -60,9 +60,14 @@ pub async fn start_job(config: &HttpApi, tracker: Arc, form: Form if config.enabled { let bind_to = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) - .await - .map(|tls| tls.expect("it should have a valid tracker api tls configuration")); + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker api tls configuration"), + ), + None => None, + }; let access_tokens = Arc::new(config.access_tokens.clone()); diff --git a/src/lib.rs b/src/lib.rs index c059f6df7..7f8c70a47 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,16 +199,10 @@ //! [[http_trackers]] //! bind_address = "0.0.0.0:7070" //! enabled = false -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" //! //! [http_api] //! bind_address = "127.0.0.1:1212" //! enabled = true -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" //! //! [http_api.access_tokens] //! admin = "MyAccessToken" @@ -261,9 +255,6 @@ //! [http_api] //! enabled = true //! bind_address = "127.0.0.1:1212" -//! ssl_enabled = false -//! ssl_cert_path = "" -//! ssl_key_path = "" //! ``` //! //! By default it's enabled on port `1212`. You also need to add access tokens in the configuration: diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 47d40c654..02b93efa6 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -27,7 +27,8 @@ //! [http_api] //! enabled = true //! bind_address = "0.0.0.0:1212" -//! ssl_enabled = false +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! @@ -106,16 +107,15 @@ //! //! # Setup SSL (optional) //! -//! The API server supports SSL. You can enable it by setting the -//! [`ssl_enabled`](torrust_tracker_configuration::HttpApi::ssl_enabled) option -//! to `true` in the configuration file -//! ([`http_api`](torrust_tracker_configuration::HttpApi) section). +//! The API server supports SSL. You can enable it by adding the `tsl_config` +//! section to the configuration. //! //! ```toml //! [http_api] //! enabled = true //! bind_address = "0.0.0.0:1212" -//! ssl_enabled = true +//! +//! [http_api.tsl_config] //! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" //! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" //! diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index d79d2b69d..8d05631cd 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -212,9 +212,14 @@ mod tests { let addr = config.bind_address; - let tls = make_rust_tls(config.ssl_enabled, &config.tsl_config) - .await - .map(|tls| tls.expect("tls config failed")); + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker api tls configuration"), + ), + None => None, + }; let access_tokens = Arc::new(config.access_tokens.clone()); diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs index ec72ccafd..05478fac3 100644 --- a/src/servers/http/launcher.rs +++ b/src/servers/http/launcher.rs @@ -84,7 +84,7 @@ mod tests { use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; use crate::bootstrap::app::tracker; - use crate::bootstrap::jobs::make_rust_tls_from_path_buf; + use crate::bootstrap::jobs::make_rust_tls; use crate::servers::http::launcher::Launcher; use crate::servers::{registar, service}; @@ -96,13 +96,14 @@ mod tests { let bind_to = config.bind_address; - let tls = make_rust_tls_from_path_buf( - config.ssl_enabled, - &config.tsl_config.ssl_cert_path, - &config.tsl_config.ssl_key_path, - ) - .await - .map(|tls| tls.expect("tls config failed")); + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker https configuration"), + ), + None => None, + }; let form = ®istar::Registar::default(); diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 76cfcfaf5..73ee52b23 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -39,7 +39,10 @@ impl Environment { let addr = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); + let tls = config + .tsl_config + .as_ref() + .map(|tls_config| block_on(make_rust_tls(tls_config)).expect("it should have a valid tracker api tls configuration")); let stopped = Service::new(ApiLauncher::new(tracker.clone(), access_tokens, addr, tls)); diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 9cacab04b..14612dcf0 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -38,7 +38,10 @@ impl Environment { let bind_to = config.bind_address; - let tls = block_on(make_rust_tls(config.ssl_enabled, &config.tsl_config)).map(|tls| tls.expect("tls config failed")); + let tls = config + .tsl_config + .as_ref() + .map(|tls_config| block_on(make_rust_tls(tls_config)).expect("it should have a valid tracker https configuration")); let server = Service::new(Launcher::new(tracker.clone(), bind_to, tls)); From 855bd4f1a1855b93122801434db37cd227ac783e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 18 Jun 2024 10:11:11 +0100 Subject: [PATCH 216/217] feat!: [#878] remove enabled fields in config By default all services are disabled. If the service section is missing in the TOML config file it means the service is disabled. From: ```toml [[udp_trackers]] enabled = false bind_address = "0.0.0.0:6969" ``` To: ```toml ``` The `http_api` section has been disabled by default becuase there is no way to override it to disable it, if it's enabled by default. You nned to explicitly enabled the API now. --- docs/benchmarking.md | 4 +- packages/configuration/src/v1/http_tracker.rs | 9 - packages/configuration/src/v1/mod.rs | 47 +--- packages/configuration/src/v1/tracker_api.rs | 9 - packages/configuration/src/v1/udp_tracker.rs | 8 - packages/test-helpers/src/configuration.rs | 39 ++- .../config/tracker.container.mysql.toml | 16 +- .../config/tracker.container.sqlite3.toml | 14 + .../config/tracker.development.sqlite3.toml | 10 +- .../config/tracker.e2e.container.sqlite3.toml | 10 +- .../config/tracker.udp.benchmarking.toml | 5 +- src/app.rs | 50 ++-- src/bootstrap/jobs/http_tracker.rs | 35 +-- src/bootstrap/jobs/tracker_apis.rs | 39 ++- src/bootstrap/jobs/udp_tracker.rs | 15 +- src/lib.rs | 21 +- src/servers/apis/mod.rs | 2 - src/servers/apis/server.rs | 2 +- src/servers/http/launcher.rs | 2 +- src/servers/http/server.rs | 258 ++++++++++++++++++ tests/common/udp/environment.rs | 4 +- tests/servers/api/environment.rs | 10 +- tests/servers/http/environment.rs | 7 +- 23 files changed, 417 insertions(+), 199 deletions(-) create mode 100644 src/servers/http/server.rs diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 67b680fdc..ce3b69057 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -33,7 +33,7 @@ Run the tracker with UDP service enabled and other services disabled and set log log_level = "error" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" ``` Build and run the tracker: @@ -168,7 +168,7 @@ Run the tracker with UDP service enabled and other services disabled and set log log_level = "error" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" ``` ```console diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs index fed2282a5..42ec02bf2 100644 --- a/packages/configuration/src/v1/http_tracker.rs +++ b/packages/configuration/src/v1/http_tracker.rs @@ -9,10 +9,6 @@ use crate::TslConfig; #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { - /// Weather the HTTP tracker is enabled or not. - #[serde(default = "HttpTracker::default_enabled")] - pub enabled: bool, - /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -28,7 +24,6 @@ pub struct HttpTracker { impl Default for HttpTracker { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), tsl_config: Self::default_tsl_config(), } @@ -36,10 +31,6 @@ impl Default for HttpTracker { } impl HttpTracker { - fn default_enabled() -> bool { - false - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 7070) } diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs index 603be85d2..546f55b6e 100644 --- a/packages/configuration/src/v1/mod.rs +++ b/packages/configuration/src/v1/mod.rs @@ -220,16 +220,7 @@ //! external_ip = "0.0.0.0" //! on_reverse_proxy = false //! -//! [[udp_trackers]] -//! enabled = false -//! bind_address = "0.0.0.0:6969" -//! -//! [[http_trackers]] -//! enabled = false -//! bind_address = "0.0.0.0:7070" -//! //! [http_api] -//! enabled = true //! bind_address = "127.0.0.1:1212" //! //! [http_api.access_tokens] @@ -267,7 +258,7 @@ const CONFIG_OVERRIDE_PREFIX: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_"; const CONFIG_OVERRIDE_SEPARATOR: &str = "__"; /// Core configuration for the tracker. -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default)] pub struct Configuration { /// Logging configuration pub logging: Logging, @@ -278,33 +269,20 @@ pub struct Configuration { /// The list of UDP trackers the tracker is running. Each UDP tracker /// represents a UDP server that the tracker is running and it has its own /// configuration. - pub udp_trackers: Vec, + pub udp_trackers: Option>, /// The list of HTTP trackers the tracker is running. Each HTTP tracker /// represents a HTTP server that the tracker is running and it has its own /// configuration. - pub http_trackers: Vec, + pub http_trackers: Option>, /// The HTTP API configuration. - pub http_api: HttpApi, + pub http_api: Option, /// The Health Check API configuration. pub health_check_api: HealthCheckApi, } -impl Default for Configuration { - fn default() -> Self { - Self { - logging: Logging::default(), - core: Core::default(), - udp_trackers: vec![UdpTracker::default()], - http_trackers: vec![HttpTracker::default()], - http_api: HttpApi::default(), - health_check_api: HealthCheckApi::default(), - } - } -} - impl Configuration { /// Returns the tracker public IP address id defined in the configuration, /// and `None` otherwise. @@ -408,21 +386,6 @@ mod tests { external_ip = "0.0.0.0" on_reverse_proxy = false - [[udp_trackers]] - enabled = false - bind_address = "0.0.0.0:6969" - - [[http_trackers]] - enabled = false - bind_address = "0.0.0.0:7070" - - [http_api] - enabled = true - bind_address = "127.0.0.1:1212" - - [http_api.access_tokens] - admin = "MyAccessToken" - [health_check_api] bind_address = "127.0.0.1:1313" "# @@ -556,7 +519,7 @@ mod tests { let configuration = Configuration::load(&info).expect("Could not load configuration from file"); assert_eq!( - configuration.http_api.access_tokens.get("admin"), + configuration.http_api.unwrap().access_tokens.get("admin"), Some("NewToken".to_owned()).as_ref() ); diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs index 42794ad18..302a4ee95 100644 --- a/packages/configuration/src/v1/tracker_api.rs +++ b/packages/configuration/src/v1/tracker_api.rs @@ -12,10 +12,6 @@ pub type AccessTokens = HashMap; #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpApi { - /// Weather the HTTP API is enabled or not. - #[serde(default = "HttpApi::default_enabled")] - pub enabled: bool, - /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -38,7 +34,6 @@ pub struct HttpApi { impl Default for HttpApi { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), tsl_config: Self::default_tsl_config(), access_tokens: Self::default_access_tokens(), @@ -47,10 +42,6 @@ impl Default for HttpApi { } impl HttpApi { - fn default_enabled() -> bool { - true - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1212) } diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs index f8387202e..b3d420d72 100644 --- a/packages/configuration/src/v1/udp_tracker.rs +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -4,9 +4,6 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { - /// Weather the UDP tracker is enabled or not. - #[serde(default = "UdpTracker::default_enabled")] - pub enabled: bool, /// The address the tracker will bind to. /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to /// listen to all interfaces, use `0.0.0.0`. If you want the operating @@ -17,17 +14,12 @@ pub struct UdpTracker { impl Default for UdpTracker { fn default() -> Self { Self { - enabled: Self::default_enabled(), bind_address: Self::default_bind_address(), } } } impl UdpTracker { - fn default_enabled() -> bool { - false - } - fn default_bind_address() -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6969) } diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 9c6c0fe11..646617b32 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -2,7 +2,7 @@ use std::env; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; -use torrust_tracker_configuration::{Configuration, LogLevel}; +use torrust_tracker_configuration::{Configuration, HttpApi, HttpTracker, LogLevel, UdpTracker}; use torrust_tracker_primitives::TrackerMode; use crate::random; @@ -33,8 +33,10 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for API let api_port = 0u16; - config.http_api.enabled = true; - config.http_api.bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port); + config.http_api = Some(HttpApi { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), api_port), + ..Default::default() + }); // Ephemeral socket address for Health Check API let health_check_api_port = 0u16; @@ -42,13 +44,16 @@ pub fn ephemeral() -> Configuration { // Ephemeral socket address for UDP tracker let udp_port = 0u16; - config.udp_trackers[0].enabled = true; - config.udp_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port); + config.udp_trackers = Some(vec![UdpTracker { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), udp_port), + }]); // Ephemeral socket address for HTTP tracker let http_port = 0u16; - config.http_trackers[0].enabled = true; - config.http_trackers[0].bind_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port); + config.http_trackers = Some(vec![HttpTracker { + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), http_port), + tsl_config: None, + }]); // Ephemeral sqlite database let temp_directory = env::temp_dir(); @@ -137,9 +142,17 @@ pub fn ephemeral_ipv6() -> Configuration { let ipv6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0); - cfg.http_api.bind_address.clone_from(&ipv6); - cfg.http_trackers[0].bind_address.clone_from(&ipv6); - cfg.udp_trackers[0].bind_address = ipv6; + if let Some(ref mut http_api) = cfg.http_api { + http_api.bind_address.clone_from(&ipv6); + }; + + if let Some(ref mut http_trackers) = cfg.http_trackers { + http_trackers[0].bind_address.clone_from(&ipv6); + } + + if let Some(ref mut udp_trackers) = cfg.udp_trackers { + udp_trackers[0].bind_address.clone_from(&ipv6); + } cfg } @@ -149,9 +162,9 @@ pub fn ephemeral_ipv6() -> Configuration { pub fn ephemeral_with_no_services() -> Configuration { let mut cfg = ephemeral(); - cfg.http_api.enabled = false; - cfg.http_trackers[0].enabled = false; - cfg.udp_trackers[0].enabled = false; + cfg.http_api = None; + cfg.http_trackers = None; + cfg.udp_trackers = None; cfg } diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index 70ee8b500..68cc8db8a 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -2,6 +2,16 @@ driver = "MySQL" path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker" -[[http_trackers]] -bind_address = "0.0.0.0:7070" -enabled = true +# Uncomment to enable services + +#[[udp_trackers]] +#bind_address = "0.0.0.0:6969" + +#[[http_trackers]] +#bind_address = "0.0.0.0:7070" + +#[http_api] +#bind_address = "0.0.0.0:1212" + +#[http_api.access_tokens] +#admin = "MyAccessToken" diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index f7bb6b8bb..63e169a70 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -1,2 +1,16 @@ [core.database] path = "/var/lib/torrust/tracker/database/sqlite3.db" + +# Uncomment to enable services + +#[[udp_trackers]] +#bind_address = "0.0.0.0:6969" + +#[[http_trackers]] +#bind_address = "0.0.0.0:7070" + +#[http_api] +#bind_address = "0.0.0.0:1212" + +#[http_api.access_tokens] +#admin = "MyAccessToken" diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index bf6478492..84754794e 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -1,5 +1,11 @@ [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" [[http_trackers]] -enabled = true +bind_address = "0.0.0.0:7070" + +[http_api] +bind_address = "0.0.0.0:1212" + +[http_api.access_tokens] +admin = "MyAccessToken" diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 744d267fd..fb33a8e32 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -2,10 +2,16 @@ path = "/var/lib/torrust/tracker/database/sqlite3.db" [[udp_trackers]] -enabled = true +bind_address = "0.0.0.0:6969" [[http_trackers]] -enabled = true +bind_address = "0.0.0.0:7070" + +[http_api] +bind_address = "0.0.0.0:1212" + +[http_api.access_tokens] +admin = "MyAccessToken" [health_check_api] # Must be bound to wildcard IP to be accessible from outside the container diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index cd193c40a..d9361cf10 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -6,7 +6,4 @@ remove_peerless_torrents = false tracker_usage_statistics = false [[udp_trackers]] -enabled = true - -[http_api] -enabled = false +bind_address = "0.0.0.0:6969" diff --git a/src/app.rs b/src/app.rs index 169805c84..c9054d1fe 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,36 +60,34 @@ pub async fn start(tracker_config: &Configuration, tracker: Arc) } // Start the UDP blocks - for config in &tracker_config.udp_trackers { - if tracker.is_private() { - warn!( - "Could not start UDP tracker on: {} while in {:?}. UDP is not safe for private trackers!", - config.bind_address, tracker_config.core.mode - ); - } else if let Some(job) = - udp_tracker::start_job(config, tracker.clone(), registar.form(), servers::udp::Version::V0).await - { - jobs.push(job); - }; - } + if let Some(udp_trackers) = &tracker_config.udp_trackers { + for config in udp_trackers { + if tracker.is_private() { + warn!( + "Could not start UDP tracker on: {} while in {:?}. UDP is not safe for private trackers!", + config.bind_address, tracker_config.core.mode + ); + } else if let Some(job) = + udp_tracker::start_job(config, tracker.clone(), registar.form(), servers::udp::Version::V0).await + { + jobs.push(job); + }; + } + }; // Start the HTTP blocks - for config in &tracker_config.http_trackers { - if let Some(job) = http_tracker::start_job(config, tracker.clone(), registar.form(), servers::http::Version::V1).await { - jobs.push(job); - }; - } + if let Some(http_trackers) = &tracker_config.http_trackers { + for config in http_trackers { + if let Some(job) = http_tracker::start_job(config, tracker.clone(), registar.form(), servers::http::Version::V1).await + { + jobs.push(job); + }; + } + }; // Start HTTP API - if tracker_config.http_api.enabled { - if let Some(job) = tracker_apis::start_job( - &tracker_config.http_api, - tracker.clone(), - registar.form(), - servers::apis::Version::V1, - ) - .await - { + if let Some(http_api) = &tracker_config.http_api { + if let Some(job) = tracker_apis::start_job(http_api, tracker.clone(), registar.form(), servers::apis::Version::V1).await { jobs.push(job); }; } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 491cdfb3c..62eef862c 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; -use tracing::{info, instrument}; +use tracing::instrument; use super::make_rust_tls; use crate::core; @@ -42,24 +42,19 @@ pub async fn start_job( form: Form, version: Version, ) -> Option> { - if config.enabled { - let socket = config.bind_address; - - let tls = match &config.tsl_config { - Some(tls_config) => Some( - make_rust_tls(tls_config) - .await - .expect("it should have a valid tracker https configuration"), - ), - None => None, - }; - - match version { - Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), - } - } else { - info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration."); - None + let socket = config.bind_address; + + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker https configuration"), + ), + None => None, + }; + + match version { + Version::V1 => Some(start_v1(socket, tls, tracker.clone(), form).await), } } @@ -94,7 +89,7 @@ mod tests { #[tokio::test] async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_trackers[0]; + let config = &cfg.http_trackers.as_ref().unwrap()[0]; let tracker = tracker(&cfg); let version = Version::V1; diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 12d18a688..bcf6e0cd2 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -26,7 +26,7 @@ use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; use tokio::task::JoinHandle; use torrust_tracker_configuration::{AccessTokens, HttpApi}; -use tracing::{info, instrument}; +use tracing::instrument; use super::make_rust_tls; use crate::core; @@ -57,26 +57,21 @@ pub struct ApiServerJobStarted(); #[allow(clippy::async_yields_async)] #[instrument(ret)] pub async fn start_job(config: &HttpApi, tracker: Arc, form: Form, version: Version) -> Option> { - if config.enabled { - let bind_to = config.bind_address; - - let tls = match &config.tsl_config { - Some(tls_config) => Some( - make_rust_tls(tls_config) - .await - .expect("it should have a valid tracker api tls configuration"), - ), - None => None, - }; - - let access_tokens = Arc::new(config.access_tokens.clone()); - - match version { - Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), form, access_tokens).await), - } - } else { - info!("Note: Not loading Http Tracker Service, Not Enabled in Configuration."); - None + let bind_to = config.bind_address; + + let tls = match &config.tsl_config { + Some(tls_config) => Some( + make_rust_tls(tls_config) + .await + .expect("it should have a valid tracker api tls configuration"), + ), + None => None, + }; + + let access_tokens = Arc::new(config.access_tokens.clone()); + + match version { + Version::V1 => Some(start_v1(bind_to, tls, tracker.clone(), form, access_tokens).await), } } @@ -117,7 +112,7 @@ mod tests { #[tokio::test] async fn it_should_start_http_tracker() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_api; + let config = &cfg.http_api.as_ref().unwrap(); let tracker = tracker(&cfg); let version = Version::V1; diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index 0bb5a7881..4a95451bb 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use tokio::task::JoinHandle; use torrust_tracker_configuration::UdpTracker; -use tracing::{info, instrument}; +use tracing::instrument; use crate::core; use crate::servers::registar::Form; @@ -33,15 +33,10 @@ use crate::servers::udp::Version; #[allow(clippy::async_yields_async)] #[instrument(ret)] pub async fn start_job(config: &UdpTracker, tracker: Arc, form: Form, version: Version) -> Option> { - if config.enabled { - let addr = config.bind_address; - - match version { - Version::V0 => Some(start_v0(addr, tracker.clone(), form).await), - } - } else { - info!("Note: Not loading Udp Tracker Service, Not Enabled in Configuration."); - None + let addr = config.bind_address; + + match version { + Version::V0 => Some(start_v0(addr, tracker.clone(), form).await), } } diff --git a/src/lib.rs b/src/lib.rs index 7f8c70a47..cf2834418 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -192,21 +192,6 @@ //! external_ip = "0.0.0.0" //! on_reverse_proxy = false //! -//! [[udp_trackers]] -//! bind_address = "0.0.0.0:6969" -//! enabled = false -//! -//! [[http_trackers]] -//! bind_address = "0.0.0.0:7070" -//! enabled = false -//! -//! [http_api] -//! bind_address = "127.0.0.1:1212" -//! enabled = true -//! -//! [http_api.access_tokens] -//! admin = "MyAccessToken" -//! //! [health_check_api] //! bind_address = "127.0.0.1:1313" //!``` @@ -253,8 +238,10 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "127.0.0.1:1212" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" //! ``` //! //! By default it's enabled on port `1212`. You also need to add access tokens in the configuration: @@ -310,7 +297,6 @@ //! //! ```toml //! [[http_trackers]] -//! enabled = true //! bind_address = "0.0.0.0:7070" //! ``` //! @@ -405,7 +391,6 @@ //! //! ```toml //! [[udp_trackers]] -//! enabled = true //! bind_address = "0.0.0.0:6969" //! ``` //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 02b93efa6..6dae66c2d 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -25,7 +25,6 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "0.0.0.0:1212" //! //! [http_api.tsl_config] @@ -112,7 +111,6 @@ //! //! ```toml //! [http_api] -//! enabled = true //! bind_address = "0.0.0.0:1212" //! //! [http_api.tsl_config] diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 8d05631cd..54c4e0594 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -206,7 +206,7 @@ mod tests { #[tokio::test] async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); - let config = &cfg.http_api; + let config = &cfg.http_api.clone().unwrap(); let tracker = tracker(&cfg); diff --git a/src/servers/http/launcher.rs b/src/servers/http/launcher.rs index 05478fac3..b9fa6ccd8 100644 --- a/src/servers/http/launcher.rs +++ b/src/servers/http/launcher.rs @@ -92,7 +92,7 @@ mod tests { async fn it_should_be_able_to_start_and_stop() { let cfg = Arc::new(ephemeral_mode_public()); let tracker = tracker(&cfg); - let config = &cfg.http_trackers[0]; + let config = &cfg.http_trackers.as_ref().unwrap()[0]; let bind_to = config.bind_address; diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs new file mode 100644 index 000000000..5798f7c10 --- /dev/null +++ b/src/servers/http/server.rs @@ -0,0 +1,258 @@ +//! Module to handle the HTTP server instances. +use std::net::SocketAddr; +use std::sync::Arc; + +use axum_server::tls_rustls::RustlsConfig; +use axum_server::Handle; +use derive_more::Constructor; +use futures::future::BoxFuture; +use tokio::sync::oneshot::{Receiver, Sender}; +use tracing::info; + +use super::v1::routes::router; +use crate::bootstrap::jobs::Started; +use crate::core::Tracker; +use crate::servers::custom_axum_server::{self, TimeoutAcceptor}; +use crate::servers::registar::{ServiceHealthCheckJob, ServiceRegistration, ServiceRegistrationForm}; +use crate::servers::signals::{graceful_shutdown, Halted}; + +/// Error that can occur when starting or stopping the HTTP server. +/// +/// Some errors triggered while starting the server are: +/// +/// - The spawned server cannot send its `SocketAddr` back to the main thread. +/// - The launcher cannot receive the `SocketAddr` from the spawned server. +/// +/// Some errors triggered while stopping the server are: +/// +/// - The channel to send the shutdown signal to the server is closed. +/// - The task to shutdown the server on the spawned server failed to execute to +/// completion. +#[derive(Debug)] +pub enum Error { + Error(String), +} + +#[derive(Constructor, Debug)] +pub struct Launcher { + pub bind_to: SocketAddr, + pub tls: Option, +} + +impl Launcher { + fn start(&self, tracker: Arc, tx_start: Sender, rx_halt: Receiver) -> BoxFuture<'static, ()> { + let socket = std::net::TcpListener::bind(self.bind_to).expect("Could not bind tcp_listener to address."); + let address = socket.local_addr().expect("Could not get local_addr from tcp_listener."); + + let handle = Handle::new(); + + tokio::task::spawn(graceful_shutdown( + handle.clone(), + rx_halt, + format!("Shutting down HTTP server on socket address: {address}"), + )); + + let tls = self.tls.clone(); + let protocol = if tls.is_some() { "https" } else { "http" }; + + info!(target: "HTTP TRACKER", "Starting on: {protocol}://{}", address); + + let app = router(tracker, address); + + let running = Box::pin(async { + match tls { + Some(tls) => custom_axum_server::from_tcp_rustls_with_timeouts(socket, tls) + .handle(handle) + .acceptor(TimeoutAcceptor) + .serve(app.into_make_service_with_connect_info::()) + .await + .expect("Axum server crashed."), + None => custom_axum_server::from_tcp_with_timeouts(socket) + .handle(handle) + .acceptor(TimeoutAcceptor) + .serve(app.into_make_service_with_connect_info::()) + .await + .expect("Axum server crashed."), + } + }); + + info!(target: "HTTP TRACKER", "Started on: {protocol}://{}", address); + + tx_start + .send(Started { address }) + .expect("the HTTP(s) Tracker service should not be dropped"); + + running + } +} + +/// A HTTP server instance controller with no HTTP instance running. +#[allow(clippy::module_name_repetitions)] +pub type StoppedHttpServer = HttpServer; + +/// A HTTP server instance controller with a running HTTP instance. +#[allow(clippy::module_name_repetitions)] +pub type RunningHttpServer = HttpServer; + +/// A HTTP server instance controller. +/// +/// It's responsible for: +/// +/// - Keeping the initial configuration of the server. +/// - Starting and stopping the server. +/// - Keeping the state of the server: `running` or `stopped`. +/// +/// It's an state machine. Configurations cannot be changed. This struct +/// represents concrete configuration and state. It allows to start and stop the +/// server but always keeping the same configuration. +/// +/// > **NOTICE**: if the configurations changes after running the server it will +/// > reset to the initial value after stopping the server. This struct is not +/// > intended to persist configurations between runs. +#[allow(clippy::module_name_repetitions)] +pub struct HttpServer { + /// The state of the server: `running` or `stopped`. + pub state: S, +} + +/// A stopped HTTP server state. +pub struct Stopped { + launcher: Launcher, +} + +/// A running HTTP server state. +pub struct Running { + /// The address where the server is bound. + pub binding: SocketAddr, + pub halt_task: tokio::sync::oneshot::Sender, + pub task: tokio::task::JoinHandle, +} + +impl HttpServer { + /// It creates a new `HttpServer` controller in `stopped` state. + #[must_use] + pub fn new(launcher: Launcher) -> Self { + Self { + state: Stopped { launcher }, + } + } + + /// It starts the server and returns a `HttpServer` controller in `running` + /// state. + /// + /// # Errors + /// + /// It would return an error if no `SocketAddr` is returned after launching the server. + /// + /// # Panics + /// + /// It would panic spawned HTTP server launcher cannot send the bound `SocketAddr` + /// back to the main thread. + pub async fn start(self, tracker: Arc, form: ServiceRegistrationForm) -> Result, Error> { + let (tx_start, rx_start) = tokio::sync::oneshot::channel::(); + let (tx_halt, rx_halt) = tokio::sync::oneshot::channel::(); + + let launcher = self.state.launcher; + + let task = tokio::spawn(async move { + let server = launcher.start(tracker, tx_start, rx_halt); + + server.await; + + launcher + }); + + let binding = rx_start.await.expect("it should be able to start the service").address; + + form.send(ServiceRegistration::new(binding, check_fn)) + .expect("it should be able to send service registration"); + + Ok(HttpServer { + state: Running { + binding, + halt_task: tx_halt, + task, + }, + }) + } +} + +impl HttpServer { + /// It stops the server and returns a `HttpServer` controller in `stopped` + /// state. + /// + /// # Errors + /// + /// It would return an error if the channel for the task killer signal was closed. + pub async fn stop(self) -> Result, Error> { + self.state + .halt_task + .send(Halted::Normal) + .map_err(|_| Error::Error("Task killer channel was closed.".to_string()))?; + + let launcher = self.state.task.await.map_err(|e| Error::Error(e.to_string()))?; + + Ok(HttpServer { + state: Stopped { launcher }, + }) + } +} + +/// Checks the Health by connecting to the HTTP tracker endpoint. +/// +/// # Errors +/// +/// This function will return an error if unable to connect. +/// Or if the request returns an error. +#[must_use] +pub fn check_fn(binding: &SocketAddr) -> ServiceHealthCheckJob { + let url = format!("http://{binding}/health_check"); // DevSkim: ignore DS137138 + + let info = format!("checking http tracker health check at: {url}"); + + let job = tokio::spawn(async move { + match reqwest::get(url).await { + Ok(response) => Ok(response.status().to_string()), + Err(err) => Err(err.to_string()), + } + }); + + ServiceHealthCheckJob::new(*binding, info, job) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use torrust_tracker_test_helpers::configuration::ephemeral_mode_public; + + use crate::bootstrap::app::initialize_with_configuration; + use crate::bootstrap::jobs::make_rust_tls; + use crate::servers::http::server::{HttpServer, Launcher}; + use crate::servers::registar::Registar; + + #[tokio::test] + async fn it_should_be_able_to_start_and_stop() { + let cfg = Arc::new(ephemeral_mode_public()); + let tracker = initialize_with_configuration(&cfg); + let http_trackers = cfg.http_trackers.clone().expect("missing HTTP trackers configuration"); + let config = &http_trackers[0]; + + let bind_to = config.bind_address; + + let tls = make_rust_tls(&config.tsl_config) + .await + .map(|tls| tls.expect("tls config failed")); + + let register = &Registar::default(); + + let stopped = HttpServer::new(Launcher::new(bind_to, tls)); + let started = stopped + .start(tracker, register.give_form()) + .await + .expect("it should start the server"); + let stopped = started.stop().await.expect("it should stop the server"); + + assert_eq!(stopped.state.launcher.bind_to, bind_to); + } +} diff --git a/tests/common/udp/environment.rs b/tests/common/udp/environment.rs index 710999182..af82b8029 100644 --- a/tests/common/udp/environment.rs +++ b/tests/common/udp/environment.rs @@ -33,7 +33,9 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = tracker(configuration); - let config = Arc::new(configuration.udp_trackers[0].clone()); + let udp_tracker = configuration.udp_trackers.clone().expect("missing UDP tracker configuration"); + + let config = Arc::new(udp_tracker[0].clone()); let addr = config.bind_address; diff --git a/tests/servers/api/environment.rs b/tests/servers/api/environment.rs index 73ee52b23..f33a4a20d 100644 --- a/tests/servers/api/environment.rs +++ b/tests/servers/api/environment.rs @@ -34,8 +34,7 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = tracker(configuration); - let config = Arc::new(configuration.http_api.clone()); - let access_tokens = Arc::new(config.access_tokens.clone()); + let config = Arc::new(configuration.http_api.clone().expect("missing API configuration")); let addr = config.bind_address; @@ -44,7 +43,12 @@ impl Environment { .as_ref() .map(|tls_config| block_on(make_rust_tls(tls_config)).expect("it should have a valid tracker api tls configuration")); - let stopped = Service::new(ApiLauncher::new(tracker.clone(), access_tokens, addr, tls)); + let stopped = Service::new(ApiLauncher::new( + tracker.clone(), + config.access_tokens.clone().into(), + addr, + tls, + )); Self { config, diff --git a/tests/servers/http/environment.rs b/tests/servers/http/environment.rs index 14612dcf0..bca85ec6e 100644 --- a/tests/servers/http/environment.rs +++ b/tests/servers/http/environment.rs @@ -34,7 +34,12 @@ impl Environment { pub fn new(configuration: &Arc) -> Self { let tracker = tracker(configuration); - let config = Arc::new(configuration.http_trackers[0].clone()); + let http_tracker = configuration + .http_trackers + .clone() + .expect("missing HTTP tracker configuration"); + + let config = Arc::new(http_tracker[0].clone()); let bind_to = config.bind_address; From 7e8f05b5b61b4ee06bec2daaf462d664cd671958 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Wed, 26 Jun 2024 11:12:45 +0200 Subject: [PATCH 217/217] dev: clients checker cleanup --- Cargo.lock | 1 + Cargo.toml | 2 +- src/console/clients/checker/app.rs | 2 +- src/console/clients/checker/checks/health.rs | 48 +++++--- src/console/clients/checker/checks/http.rs | 69 ++++++----- src/console/clients/checker/checks/udp.rs | 122 ++++++++----------- src/console/clients/checker/service.rs | 53 ++++---- src/console/clients/http/mod.rs | 10 +- src/console/clients/udp/mod.rs | 10 +- 9 files changed, 168 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 527c5bc57..996e8db5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3895,6 +3895,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 52bed28cd..ec4a431d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ tower = { version = "0.4.13", features = ["timeout"] } tower-http = { version = "0", features = ["compression-full", "cors", "propagate-header", "request-id", "trace"] } tracing = "0" tracing-subscriber = { version = "0", features = ["json"] } -url = "2" +url = { version = "2", features = ["serde"] } uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" diff --git a/src/console/clients/checker/app.rs b/src/console/clients/checker/app.rs index 8902524b0..37e1051d9 100644 --- a/src/console/clients/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -53,7 +53,7 @@ pub async fn run() -> Result> { console: console_printer, }; - service.run_checks().await + service.run_checks().await.context("it should run the check tasks") } fn setup_config(args: Args) -> Result { diff --git a/src/console/clients/checker/checks/health.rs b/src/console/clients/checker/checks/health.rs index d59dd538a..6d368583d 100644 --- a/src/console/clients/checker/checks/health.rs +++ b/src/console/clients/checker/checks/health.rs @@ -4,16 +4,12 @@ use std::time::Duration; use anyhow::Result; use hyper::StatusCode; use reqwest::{Client as HttpClient, Response}; +use serde::Serialize; use thiserror::Error; use url::Url; -use crate::console::clients::checker::{ - console::Console, - printer::Printer as _, - service::{CheckError, CheckResult}, -}; - -#[derive(Debug, Clone, Error)] +#[derive(Debug, Clone, Error, Serialize)] +#[serde(into = "String")] pub enum Error { #[error("Failed to Build a Http Client: {err:?}")] ClientBuildingError { err: Arc }, @@ -23,27 +19,39 @@ pub enum Error { UnsuccessfulResponse { code: StatusCode, response: Arc }, } -pub async fn run(health_checks: Vec, timeout: Duration, console: Console) -> Vec { - let mut check_results = Vec::default(); +impl From for String { + fn from(value: Error) -> Self { + value.to_string() + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Checks { + url: Url, + result: Result, +} + +pub async fn run(health_checks: Vec, timeout: Duration) -> Vec> { + let mut results = Vec::default(); - console.println("Health checks ..."); + tracing::debug!("Health checks ..."); for url in health_checks { - match run_health_check(url.clone(), &timeout).await { - Ok(response) => { - console.println(&format!("{} - Health API at {} is {}", "✓", url, response.status())); + let result = match run_health_check(url.clone(), &timeout).await { + Ok(response) => Ok(response.status().to_string()), + Err(err) => Err(err), + }; - check_results.push(Ok(())); - } - Err(err) => { - console.eprintln(&format!("{} - Health API at {} is failing: {}", "✗", url, err)); + let check = Checks { url, result }; - check_results.push(Err(CheckError::HealthCheckError { url, err })); - } + if check.result.is_err() { + results.push(Err(check)); + } else { + results.push(Ok(check)); } } - check_results + results } async fn run_health_check(url: Url, &timeout: &Duration) -> Result { diff --git a/src/console/clients/checker/checks/http.rs b/src/console/clients/checker/checks/http.rs index 56c51a857..dc1639365 100644 --- a/src/console/clients/checker/checks/http.rs +++ b/src/console/clients/checker/checks/http.rs @@ -1,46 +1,59 @@ use std::str::FromStr as _; use std::time::Duration; +use serde::Serialize; use torrust_tracker_primitives::info_hash::InfoHash; use url::Url; use crate::console::clients; -use crate::console::clients::checker::console::Console; -use crate::console::clients::checker::printer::Printer; -use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::console::clients::http::Error; use crate::shared::bit_torrent::tracker::http::client::responses; -pub async fn run(http_trackers: Vec, timeout: Duration, console: Console) -> Vec { - let mut check_results = Vec::default(); +#[derive(Debug, Clone, Serialize)] +pub struct Checks { + url: Url, + results: Vec<(Check, Result<(), Error>)>, +} + +#[derive(Debug, Clone, Serialize)] +pub enum Check { + Announce, + Scrape, +} - console.println("HTTP trackers ..."); +pub async fn run(http_trackers: Vec, timeout: Duration) -> Vec> { + let mut results = Vec::default(); + + tracing::debug!("HTTP trackers ..."); for ref url in http_trackers { - check_results.push(match check_http_announce(url, &timeout).await { - Ok(_) => { - console.println(&format!("{} - Announce at {} is OK", "✓", url)); - Ok(()) - } - Err(err) => { - console.println(&format!("{} - Announce at {} is failing", "✗", url)); - Err(CheckError::HttpCheckError { url: url.clone(), err }) - } - }); - - check_results.push(match check_http_scrape(url, &timeout).await { - Ok(_) => { - console.println(&format!("{} - Scrape at {} is OK", "✓", url)); - Ok(()) - } - Err(err) => { - console.println(&format!("{} - Scrape at {} is failing", "✗", url)); - Err(CheckError::HttpCheckError { url: url.clone(), err }) - } - }); + let mut checks = Checks { + url: url.clone(), + results: Vec::default(), + }; + + // Announce + { + let check = check_http_announce(url, &timeout).await.map(|_| ()); + + checks.results.push((Check::Announce, check)); + } + + // Scrape + { + let check = check_http_scrape(url, &timeout).await.map(|_| ()); + + checks.results.push((Check::Scrape, check)); + } + + if checks.results.iter().any(|f| f.1.is_err()) { + results.push(Err(checks)); + } else { + results.push(Ok(checks)); + } } - check_results + results } async fn check_http_announce(url: &Url, timeout: &Duration) -> Result { diff --git a/src/console/clients/checker/checks/udp.rs b/src/console/clients/checker/checks/udp.rs index bfa12de87..d58c4a00c 100644 --- a/src/console/clients/checker/checks/udp.rs +++ b/src/console/clients/checker/checks/udp.rs @@ -3,99 +3,75 @@ use std::num::NonZeroU16; use std::time::Duration; use hex_literal::hex; +use serde::Serialize; use torrust_tracker_primitives::info_hash::InfoHash; -use tracing::debug; -use crate::console::clients::checker::console::Console; -use crate::console::clients::checker::printer::Printer as _; -use crate::console::clients::checker::service::{CheckError, CheckResult}; use crate::console::clients::udp::checker::{self, Client}; use crate::console::clients::udp::Error; -pub async fn run(udp_trackers: Vec, timeout: Duration, console: Console) -> Vec { - let mut check_results = Vec::default(); +#[derive(Debug, Clone, Serialize)] +pub struct Checks { + addr: SocketAddr, + results: Vec<(Check, Result<(), Error>)>, +} + +#[derive(Debug, Clone, Serialize)] +pub enum Check { + Setup, + Announce, + Scrape, +} + +pub async fn run(udp_trackers: Vec, timeout: Duration) -> Vec> { + let mut results = Vec::default(); - console.println("UDP trackers ..."); + tracing::debug!("UDP trackers ..."); let info_hash = InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422")); // # DevSkim: ignore DS173237 for ref addr in udp_trackers { - debug!("UDP tracker: {:?}", addr); + let mut checks = Checks { + addr: *addr, + results: Vec::default(), + }; + + tracing::debug!("UDP tracker: {:?}", addr); // Setup Connection - let Ok((client, ctx)) = ({ - let res = setup_connection(addr, &timeout).await; - - check_results.push(match res { - Ok(_) => { - console.println(&format!("{} - Setup of {} is OK", "✓", addr)); - Ok(()) - } - Err(ref e) => { - console.println(&format!("{} - Setup of {} is failing", "✗", addr)); - Err(CheckError::UdpCheckError { - addr: *addr, - err: e.clone(), - }) - } - }); - - res - }) else { - break; + let (client, ctx) = match setup_connection(addr, &timeout).await { + Ok((client, ctx)) => { + checks.results.push((Check::Setup, Ok(()))); + (client, ctx) + } + Err(err) => { + checks.results.push((Check::Setup, Err(err))); + results.push(Err(checks)); + break; + } }; - // Do Announce - if { - let res = check_udp_announce(&client, &ctx, info_hash).await; - - check_results.push(match res { - Ok(_) => { - console.println(&format!("{} - Announce of {} is OK", "✓", addr)); - Ok(()) - } - Err(ref e) => { - console.println(&format!("{} - Announce of {} is failing", "✗", addr)); - Err(CheckError::UdpCheckError { - addr: *addr, - err: e.clone(), - }) - } - }); - - res - } - .is_err() + // Announce { - break; - }; + let check = check_udp_announce(&client, &ctx, info_hash).await.map(|_| ()); - // Do Scrape - if { - let res = check_udp_scrape(&client, &ctx, &[info_hash]).await; - - check_results.push(match res { - Ok(_) => { - console.println(&format!("{} - Announce of {} is OK", "✓", addr)); - Ok(()) - } - Err(ref e) => { - console.println(&format!("{} - Announce of {} is failing", "✗", addr)); - Err(CheckError::UdpCheckError { - addr: *addr, - err: e.clone(), - }) - } - }); - res + checks.results.push((Check::Announce, check)); } - .is_err() + + // Scrape { - break; - }; + let check = check_udp_scrape(&client, &ctx, &[info_hash]).await.map(|_| ()); + + checks.results.push((Check::Scrape, check)); + } + + if checks.results.iter().any(|f| f.1.is_err()) { + results.push(Err(checks)); + } else { + results.push(Ok(checks)); + } } - check_results + results } async fn setup_connection( diff --git a/src/console/clients/checker/service.rs b/src/console/clients/checker/service.rs index 03e8bd0b2..b68213d47 100644 --- a/src/console/clients/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -1,32 +1,24 @@ -use std::net::SocketAddr; use std::sync::Arc; -use anyhow::{Context, Result}; -use reqwest::Url; -use thiserror::Error; -use tokio::task::JoinSet; +use futures::FutureExt; +use serde::Serialize; +use tokio::task::{JoinError, JoinSet}; -use super::checks::{self}; +use super::checks::{health, http, udp}; use super::config::Configuration; use super::console::Console; use crate::console::clients::checker::printer::Printer; -use crate::console::clients::{http, udp}; pub struct Service { pub(crate) config: Arc, pub(crate) console: Console, } -pub type CheckResult = Result<(), CheckError>; - -#[derive(Debug, Clone, Error)] -pub enum CheckError { - #[error("Error In Udp: {addr:?}")] - UdpCheckError { addr: SocketAddr, err: udp::Error }, - #[error("Error In Http: url: {url:?}")] - HttpCheckError { url: Url, err: http::Error }, - #[error("Error In HeathCheck: url: {url:?}")] - HealthCheckError { url: Url, err: checks::health::Error }, +#[derive(Debug, Clone, Serialize)] +pub enum CheckResult { + Udp(Result), + Http(Result), + Health(Result), } impl Service { @@ -34,22 +26,35 @@ impl Service { /// /// It will return an error if some of the tests panic or otherwise fail to run. /// On success it will return a vector of `Ok(())` of [`CheckResult`]. - pub async fn run_checks(self) -> Result> { - self.console.println("Running checks for trackers ..."); + /// + /// # Panics + /// + /// It would panic if `serde_json` produces invalid json for the `to_string_pretty` function. + pub async fn run_checks(self) -> Result, JoinError> { + tracing::info!("Running checks for trackers ..."); - let mut check_results = Vec::::default(); + let mut check_results = Vec::default(); let timeout = self.config.client_timeout; let mut checks = JoinSet::new(); - checks.spawn(checks::udp::run(self.config.udp_trackers.clone(), timeout, self.console)); - checks.spawn(checks::http::run(self.config.http_trackers.clone(), timeout, self.console)); - checks.spawn(checks::health::run(self.config.health_checks.clone(), timeout, self.console)); + checks + .spawn(udp::run(self.config.udp_trackers.clone(), timeout).map(|mut f| f.drain(..).map(CheckResult::Udp).collect())); + checks.spawn( + http::run(self.config.http_trackers.clone(), timeout).map(|mut f| f.drain(..).map(CheckResult::Http).collect()), + ); + checks.spawn( + health::run(self.config.health_checks.clone(), timeout).map(|mut f| f.drain(..).map(CheckResult::Health).collect()), + ); while let Some(results) = checks.join_next().await { - check_results.append(&mut results.context("failed to join check")?); + check_results.append(&mut results?); } + let json_output = serde_json::json!(check_results); + self.console + .println(&serde_json::to_string_pretty(&json_output).expect("it should consume valid json")); + Ok(check_results) } } diff --git a/src/console/clients/http/mod.rs b/src/console/clients/http/mod.rs index 730b1decb..4474883e7 100644 --- a/src/console/clients/http/mod.rs +++ b/src/console/clients/http/mod.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use std::time::Duration; +use serde::Serialize; use thiserror::Error; use torrust_tracker_primitives::info_hash::InfoHash; use torrust_tracker_primitives::peer; @@ -11,7 +12,8 @@ use crate::shared::bit_torrent::tracker::http::client::{responses, Client}; pub mod app; -#[derive(Debug, Clone, Error)] +#[derive(Debug, Clone, Error, Serialize)] +#[serde(into = "String")] pub enum Error { #[error("Http request did not receive a response within the timeout: {err:?}")] HttpClientError { @@ -27,6 +29,12 @@ pub enum Error { }, } +impl From for String { + fn from(value: Error) -> Self { + value.to_string() + } +} + /// . /// /// # Errors diff --git a/src/console/clients/udp/mod.rs b/src/console/clients/udp/mod.rs index 52931d1d8..3bb5b97da 100644 --- a/src/console/clients/udp/mod.rs +++ b/src/console/clients/udp/mod.rs @@ -1,5 +1,6 @@ use std::net::SocketAddr; +use serde::Serialize; use thiserror::Error; use crate::shared::bit_torrent::tracker::udp; @@ -8,7 +9,8 @@ pub mod app; pub mod checker; pub mod responses; -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug, Clone, Serialize)] +#[serde(into = "String")] pub enum Error { #[error("Failed to Connect to: {addr}, with error: {err}")] UnableToBindAndConnect { addr: SocketAddr, err: udp::Error }, @@ -25,3 +27,9 @@ pub enum Error { #[error("Failed to get a successful connection response: {err}")] UnexpectedConnectionResponse { err: udp::Error }, } + +impl From for String { + fn from(value: Error) -> Self { + value.to_string() + } +}

o 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�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

o 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�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