Initial impl of up/down ratelimits
This commit is contained in:
parent
616498f500
commit
25b309867b
8 changed files with 121 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2593,6 +2593,7 @@ dependencies = [
|
||||||
"hex 0.4.3",
|
"hex 0.4.3",
|
||||||
"http",
|
"http",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"leaky-bucket",
|
||||||
"librqbit-bencode",
|
"librqbit-bencode",
|
||||||
"librqbit-buffers",
|
"librqbit-buffers",
|
||||||
"librqbit-clone-to-owned",
|
"librqbit-clone-to-owned",
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ tokio = { version = "1", features = [
|
||||||
"fs",
|
"fs",
|
||||||
"io-util",
|
"io-util",
|
||||||
] }
|
] }
|
||||||
|
leaky-bucket = "1.1"
|
||||||
console-subscriber = { version = "0.4", optional = true }
|
console-subscriber = { version = "0.4", optional = true }
|
||||||
axum = { version = "0.7", optional = true }
|
axum = { version = "0.7", optional = true }
|
||||||
tower-http = { version = "0.5", features = ["cors", "trace"], optional = true }
|
tower-http = { version = "0.5", features = ["cors", "trace"], optional = true }
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ mod file_ops;
|
||||||
pub mod http_api;
|
pub mod http_api;
|
||||||
#[cfg(feature = "http-api")]
|
#[cfg(feature = "http-api")]
|
||||||
pub mod http_api_client;
|
pub mod http_api_client;
|
||||||
|
mod limits;
|
||||||
mod merge_streams;
|
mod merge_streams;
|
||||||
mod peer_connection;
|
mod peer_connection;
|
||||||
mod peer_info_reader;
|
mod peer_info_reader;
|
||||||
|
|
|
||||||
48
crates/librqbit/src/limits.rs
Normal file
48
crates/librqbit/src/limits.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use leaky_bucket::RateLimiter;
|
||||||
|
use peer_binary_protocol::PIECE_MESSAGE_DEFAULT_LEN;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct LimitsConfig {
|
||||||
|
pub upload_bps: Option<usize>,
|
||||||
|
pub download_bps: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Limits {
|
||||||
|
down: Option<leaky_bucket::RateLimiter>,
|
||||||
|
up: Option<leaky_bucket::RateLimiter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limits {
|
||||||
|
pub fn new(config: LimitsConfig) -> Self {
|
||||||
|
let new = |bps: usize| -> RateLimiter {
|
||||||
|
let b_per_100_ms = bps.div_ceil(10);
|
||||||
|
RateLimiter::builder()
|
||||||
|
.interval(Duration::from_millis(100))
|
||||||
|
.refill(b_per_100_ms)
|
||||||
|
// whatever the limit is, we need to be able to download / upload a chunk
|
||||||
|
.max(PIECE_MESSAGE_DEFAULT_LEN.max(bps))
|
||||||
|
.build()
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
down: config.download_bps.map(new),
|
||||||
|
up: config.upload_bps.map(new),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_for_upload(&self, len: usize) {
|
||||||
|
if let Some(rl) = self.up.as_ref() {
|
||||||
|
rl.acquire(len).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_for_download(&self, len: usize) {
|
||||||
|
if let Some(rl) = self.down.as_ref() {
|
||||||
|
rl.acquire(len).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
bitv_factory::{BitVFactory, NonPersistentBitVFactory},
|
bitv_factory::{BitVFactory, NonPersistentBitVFactory},
|
||||||
dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult},
|
dht_utils::{read_metainfo_from_peer_receiver, ReadMetainfoResult},
|
||||||
file_info::FileInfo,
|
file_info::FileInfo,
|
||||||
|
limits::{Limits, LimitsConfig},
|
||||||
merge_streams::merge_streams,
|
merge_streams::merge_streams,
|
||||||
peer_connection::PeerConnectionOptions,
|
peer_connection::PeerConnectionOptions,
|
||||||
read_buf::ReadBuf,
|
read_buf::ReadBuf,
|
||||||
|
|
@ -122,6 +123,8 @@ pub struct Session {
|
||||||
|
|
||||||
root_span: Option<Span>,
|
root_span: Option<Span>,
|
||||||
|
|
||||||
|
pub(crate) ratelimits: Limits,
|
||||||
|
|
||||||
pub(crate) stats: SessionStats,
|
pub(crate) stats: SessionStats,
|
||||||
|
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
|
|
@ -264,6 +267,9 @@ pub struct AddTorrentOptions {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub disable_trackers: bool,
|
pub disable_trackers: bool,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub ratelimits: LimitsConfig,
|
||||||
|
|
||||||
/// Initial peers to start of with.
|
/// Initial peers to start of with.
|
||||||
pub initial_peers: Option<Vec<SocketAddr>>,
|
pub initial_peers: Option<Vec<SocketAddr>>,
|
||||||
|
|
||||||
|
|
@ -421,6 +427,8 @@ pub struct SessionOptions {
|
||||||
// the root span to use. If not set will be None.
|
// the root span to use. If not set will be None.
|
||||||
pub root_span: Option<Span>,
|
pub root_span: Option<Span>,
|
||||||
|
|
||||||
|
pub ratelimits: LimitsConfig,
|
||||||
|
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
pub disable_upload: bool,
|
pub disable_upload: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -636,6 +644,7 @@ impl Session {
|
||||||
concurrent_initialize_semaphore: Arc::new(tokio::sync::Semaphore::new(
|
concurrent_initialize_semaphore: Arc::new(tokio::sync::Semaphore::new(
|
||||||
opts.concurrent_init_limit.unwrap_or(3),
|
opts.concurrent_init_limit.unwrap_or(3),
|
||||||
)),
|
)),
|
||||||
|
ratelimits: Limits::new(opts.ratelimits),
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
_disable_upload: opts.disable_upload,
|
_disable_upload: opts.disable_upload,
|
||||||
});
|
});
|
||||||
|
|
@ -1179,6 +1188,7 @@ impl Session {
|
||||||
allow_overwrite: opts.overwrite,
|
allow_overwrite: opts.overwrite,
|
||||||
output_folder,
|
output_folder,
|
||||||
disk_write_queue: self.disk_write_tx.clone(),
|
disk_write_queue: self.disk_write_tx.clone(),
|
||||||
|
ratelimits: opts.ratelimits,
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
_disable_upload: self._disable_upload,
|
_disable_upload: self._disable_upload,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ use tracing::{debug, error, error_span, info, trace, warn, Instrument};
|
||||||
use crate::{
|
use crate::{
|
||||||
chunk_tracker::{ChunkMarkingResult, ChunkTracker, HaveNeededSelected},
|
chunk_tracker::{ChunkMarkingResult, ChunkTracker, HaveNeededSelected},
|
||||||
file_ops::FileOps,
|
file_ops::FileOps,
|
||||||
|
limits::Limits,
|
||||||
peer_connection::{
|
peer_connection::{
|
||||||
PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest,
|
PeerConnection, PeerConnectionHandler, PeerConnectionOptions, WriterRequest,
|
||||||
},
|
},
|
||||||
|
|
@ -201,6 +202,12 @@ pub struct TorrentStateLive {
|
||||||
|
|
||||||
pub(crate) streams: Arc<TorrentStreams>,
|
pub(crate) streams: Arc<TorrentStreams>,
|
||||||
have_broadcast_tx: tokio::sync::broadcast::Sender<ValidPieceIndex>,
|
have_broadcast_tx: tokio::sync::broadcast::Sender<ValidPieceIndex>,
|
||||||
|
|
||||||
|
ratelimit_upload_tx: tokio::sync::mpsc::UnboundedSender<(
|
||||||
|
tokio::sync::mpsc::UnboundedSender<WriterRequest>,
|
||||||
|
ChunkInfo,
|
||||||
|
)>,
|
||||||
|
ratelimits: Limits,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TorrentStateLive {
|
impl TorrentStateLive {
|
||||||
|
|
@ -238,6 +245,12 @@ impl TorrentStateLive {
|
||||||
|
|
||||||
let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
|
let (have_broadcast_tx, _) = tokio::sync::broadcast::channel(128);
|
||||||
|
|
||||||
|
let (ratelimit_upload_tx, ratelimit_upload_rx) = tokio::sync::mpsc::unbounded_channel::<(
|
||||||
|
tokio::sync::mpsc::UnboundedSender<WriterRequest>,
|
||||||
|
ChunkInfo,
|
||||||
|
)>();
|
||||||
|
let ratelimits = Limits::new(paused.shared.options.ratelimits);
|
||||||
|
|
||||||
let state = Arc::new(TorrentStateLive {
|
let state = Arc::new(TorrentStateLive {
|
||||||
torrent: paused.shared.clone(),
|
torrent: paused.shared.clone(),
|
||||||
peers: PeerStates {
|
peers: PeerStates {
|
||||||
|
|
@ -272,6 +285,8 @@ impl TorrentStateLive {
|
||||||
per_piece_locks: (0..lengths.total_pieces())
|
per_piece_locks: (0..lengths.total_pieces())
|
||||||
.map(|_| RwLock::new(()))
|
.map(|_| RwLock::new(()))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
ratelimit_upload_tx,
|
||||||
|
ratelimits,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.spawn(
|
state.spawn(
|
||||||
|
|
@ -304,6 +319,11 @@ impl TorrentStateLive {
|
||||||
error_span!(parent: state.torrent.span.clone(), "peer_adder"),
|
error_span!(parent: state.torrent.span.clone(), "peer_adder"),
|
||||||
state.clone().task_peer_adder(peer_queue_rx),
|
state.clone().task_peer_adder(peer_queue_rx),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
state.spawn(
|
||||||
|
error_span!(parent: state.torrent.span.clone(), "upload_scheduler"),
|
||||||
|
state.clone().task_upload_scheduler(ratelimit_upload_rx),
|
||||||
|
);
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,6 +408,26 @@ impl TorrentStateLive {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn task_upload_scheduler(
|
||||||
|
self: Arc<Self>,
|
||||||
|
mut rx: tokio::sync::mpsc::UnboundedReceiver<(
|
||||||
|
tokio::sync::mpsc::UnboundedSender<WriterRequest>,
|
||||||
|
ChunkInfo,
|
||||||
|
)>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
while let Some((tx, ci)) = rx.recv().await {
|
||||||
|
self.ratelimits.prepare_for_upload(ci.size as usize).await;
|
||||||
|
if let Some(session) = self.torrent.session.upgrade() {
|
||||||
|
session
|
||||||
|
.ratelimits
|
||||||
|
.prepare_for_upload(ci.size as usize)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let _ = tx.send(WriterRequest::ReadChunkRequest(ci));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn task_manage_incoming_peer(
|
async fn task_manage_incoming_peer(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
checked_peer: CheckedIncomingConnection,
|
checked_peer: CheckedIncomingConnection,
|
||||||
|
|
@ -1227,12 +1267,10 @@ impl PeerHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is not super efficient as it does copying multiple times.
|
self.state
|
||||||
// Theoretically, this could be done in the sending code, so that it reads straight into
|
.ratelimit_upload_tx
|
||||||
// the send buffer.
|
.send((self.tx.clone(), chunk_info))?;
|
||||||
let request = WriterRequest::ReadChunkRequest(chunk_info);
|
Ok(())
|
||||||
trace!("sending {:?}", &request);
|
|
||||||
Ok::<_, anyhow::Error>(self.tx.send(request)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_have(&self, have: u32) {
|
fn on_have(&self, have: u32) {
|
||||||
|
|
@ -1406,6 +1444,18 @@ impl PeerHandler {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.state
|
||||||
|
.ratelimits
|
||||||
|
.prepare_for_download(request.length as usize)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(session) = self.state.torrent().session.upgrade() {
|
||||||
|
session
|
||||||
|
.ratelimits
|
||||||
|
.prepare_for_download(request.length as usize)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match aframe!(tokio::time::timeout(
|
match aframe!(tokio::time::timeout(
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ use tracing::trace;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::chunk_tracker::ChunkTracker;
|
use crate::chunk_tracker::ChunkTracker;
|
||||||
|
use crate::limits::LimitsConfig;
|
||||||
use crate::session::TorrentId;
|
use crate::session::TorrentId;
|
||||||
use crate::spawn_utils::BlockingSpawner;
|
use crate::spawn_utils::BlockingSpawner;
|
||||||
use crate::storage::BoxStorageFactory;
|
use crate::storage::BoxStorageFactory;
|
||||||
|
|
@ -103,6 +104,7 @@ pub(crate) struct ManagedTorrentOptions {
|
||||||
pub allow_overwrite: bool,
|
pub allow_overwrite: bool,
|
||||||
pub output_folder: PathBuf,
|
pub output_folder: PathBuf,
|
||||||
pub disk_write_queue: Option<DiskWorkQueueSender>,
|
pub disk_write_queue: Option<DiskWorkQueueSender>,
|
||||||
|
pub ratelimits: LimitsConfig,
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
pub _disable_upload: bool,
|
pub _disable_upload: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,8 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
cancellation_token: Some(cancel.clone()),
|
cancellation_token: Some(cancel.clone()),
|
||||||
#[cfg(feature = "disable-upload")]
|
#[cfg(feature = "disable-upload")]
|
||||||
disable_upload: opts.disable_upload,
|
disable_upload: opts.disable_upload,
|
||||||
|
// TODO: expose
|
||||||
|
ratelimits: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stats_printer = |session: Arc<Session>| async move {
|
let stats_printer = |session: Arc<Session>| async move {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue