Fix torad hanging on add of torrent

This commit is contained in:
Alexander
2026-07-21 17:47:54 +02:00
parent 6309b87323
commit e1d35f81bc
+83 -39
View File
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -22,6 +22,7 @@ use crate::db::{self, TorrentRow, TorrentState};
use crate::source::SourceResolver;
const POLL_INTERVAL: Duration = Duration::from_secs(2);
const ADD_TIMEOUT: Duration = Duration::from_secs(60);
const BYTES_PER_MIB: f64 = 1_048_576.0;
pub enum ResolveError {
@@ -36,6 +37,7 @@ pub struct TorrentManager {
default_output_dir: PathBuf,
source: SourceResolver,
tracked: Mutex<HashMap<Uuid, Arc<ManagedTorrent>>>,
adding: Mutex<HashSet<Uuid>>,
}
impl TorrentManager {
@@ -56,6 +58,7 @@ impl TorrentManager {
default_output_dir: download_dir,
source,
tracked: Mutex::new(HashMap::new()),
adding: Mutex::new(HashSet::new()),
}))
}
@@ -170,13 +173,13 @@ impl TorrentManager {
let mut interval = tokio::time::interval(POLL_INTERVAL);
loop {
interval.tick().await;
self.pick_up_pending().await;
Arc::clone(&self).pick_up_pending().await;
self.report_progress().await;
}
});
}
async fn pick_up_pending(&self) {
async fn pick_up_pending(self: Arc<Self>) {
let pending = match db::list_pending(&self.pool).await {
Ok(rows) => rows,
Err(err) => {
@@ -188,46 +191,74 @@ impl TorrentManager {
return;
}
let mut tracked = self.tracked.lock().await;
for row in pending {
if tracked.contains_key(&row.id) {
continue;
// Mark in-flight adds under brief locks so the poller doesn't re-spawn
// the same torrent on the next tick. The actual `add_torrent` call runs
// in a detached task WITHOUT holding either lock; otherwise a slow add
// (e.g. a magnet with no peers) would block every RPC that touches
// `tracked` indefinitely.
let to_start: Vec<TorrentRow> = {
let tracked = self.tracked.lock().await;
let mut adding = self.adding.lock().await;
let selected = pending
.into_iter()
.filter(|row| !tracked.contains_key(&row.id) && !adding.contains(&row.id))
.collect::<Vec<_>>();
for row in &selected {
adding.insert(row.id);
}
let options = AddTorrentOptions {
output_folder: Some(row.output_path.clone()),
overwrite: true,
..Default::default()
};
let add = match AddTorrent::from_cli_argument(&row.source) {
Ok(add) => add,
Err(err) => {
error!(id = %row.id, source = %row.source, error = %err, "failed to parse torrent source");
continue;
}
};
match self.session.add_torrent(add, Some(options)).await {
Ok(response) => match response.into_handle() {
Some(handle) => {
info!(id = %row.id, info_hash = %row.info_hash, "torrent added to session");
tracked.insert(row.id, handle);
selected
};
for row in to_start {
let manager = Arc::clone(&self);
tokio::spawn(async move {
let options = AddTorrentOptions {
output_folder: Some(row.output_path.clone()),
overwrite: true,
..Default::default()
};
let add = match AddTorrent::from_cli_argument(&row.source) {
Ok(add) => add,
Err(err) => {
error!(id = %row.id, source = %row.source, error = %err, "failed to parse torrent source");
let message = err.to_string();
persist_error(&manager.pool, row.id, &message).await;
manager.adding.lock().await.remove(&row.id);
return;
}
None => warn!(id = %row.id, "add_torrent returned no handle"),
},
Err(err) => {
error!(id = %row.id, error = %err, "failed to add torrent to session");
let message = err.to_string();
let progress = db::Progress {
name: None,
total_bytes: 0,
downloaded_bytes: 0,
state: TorrentState::Error,
error_message: Some(message.as_str()),
};
if let Err(err) = db::update_progress(&self.pool, row.id, progress).await {
error!(error = %err, "failed to persist torrent add error");
};
let add_result = tokio::time::timeout(
ADD_TIMEOUT,
manager.session.add_torrent(add, Some(options)),
)
.await;
match add_result {
Ok(Ok(response)) => match response.into_handle() {
Some(handle) => {
info!(id = %row.id, info_hash = %row.info_hash, "torrent added to session");
let mut tracked = manager.tracked.lock().await;
tracked.insert(row.id, handle);
}
None => warn!(id = %row.id, "add_torrent returned no handle"),
},
Ok(Err(err)) => {
error!(id = %row.id, error = %err, "failed to add torrent to session");
let message = err.to_string();
persist_error(&manager.pool, row.id, &message).await;
}
Err(_) => {
warn!(id = %row.id, timeout = ?ADD_TIMEOUT, "timed out adding torrent to session");
persist_error(
&manager.pool,
row.id,
"metadata fetch timed out (no peers or unreachable trackers)",
)
.await;
}
}
}
manager.adding.lock().await.remove(&row.id);
});
}
}
@@ -270,6 +301,19 @@ struct LiveTorrentStats {
seeds: u32,
}
async fn persist_error(pool: &PgPool, id: Uuid, message: &str) {
let progress = db::Progress {
name: None,
total_bytes: 0,
downloaded_bytes: 0,
state: TorrentState::Error,
error_message: Some(message),
};
if let Err(err) = db::update_progress(pool, id, progress).await {
error!(id = %id, error = %err, "failed to persist torrent add error");
}
}
fn extract_live_stats(tracked: &HashMap<Uuid, Arc<ManagedTorrent>>, id: &Uuid) -> LiveTorrentStats {
let Some(handle) = tracked.get(id) else {
return LiveTorrentStats::default();