From 8e7e81277b4819e91365ede25cb3ffb16074267c Mon Sep 17 00:00:00 2001 From: NimBold Date: Sat, 15 Aug 2026 22:56:36 +0330 Subject: [PATCH] fix(downloads): recover stale aria2 gids on resume - validate retained GIDs before resume shortcuts, including WaitingToSeed - invalidate stale lifecycle mappings and queue ownership before re-enqueue - preserve conservative transport handling and cover missing-GID classification --- src-tauri/src/lib.rs | 66 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d865856..ed68525 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5377,7 +5377,55 @@ async fn resume_download( state.queue_manager.release_registered_id(&id).await; return Ok(false); }; - if state.queue_manager.is_waiting_to_seed(&id) { + let waiting_to_seed = state.queue_manager.is_waiting_to_seed(&id); + let status = match aria2_download_status( + state.aria2_port.load(std::sync::atomic::Ordering::Relaxed), + &state.aria2_secret, + &gid, + ) + .await + { + Ok(status) => status, + Err(error) if aria2_gid_not_found(&error) => { + // Aria2 GIDs are daemon-scoped. A daemon restart or an external + // removal can leave Firelink's mapping pointing at a GID that no + // longer exists. Treat only that verified absence as an + // unresumable lifecycle so the frontend can re-enqueue the saved + // payload; transport/RPC failures must remain errors because + // re-enqueueing without knowing the daemon state could duplicate + // an existing transfer. + log::warn!( + "aria2 resume [stage=status id={} gid={} result=stale_gid action=reenqueue]", + id, + gid + ); + state.queue_manager.next_aria2_control_epoch(&id).await; + state.queue_manager.cancel_aria2_retries(&id).await; + state.queue_manager.clear_aria2_retry_state(&id).await; + state.queue_manager.forget_aria2_gid(&id).await; + state.queue_manager.release_permit(&id).await; + state.queue_manager.release_registered_id(&id).await; + return Ok(false); + } + Err(error) if waiting_to_seed => { + // WaitingToSeed normally has no active download permit, so a + // temporary RPC outage must keep using the seed scheduler rather + // than turning a recoverable wake-up into a failed resume. The + // verified missing-GID case above still retires the stale + // lifecycle and permits a fresh enqueue. + log::warn!( + "aria2 resume [stage=status id={} gid={} result=unverified_waiting_seed_status error_class={}]", + id, + gid, + crate::retry::network_error_class(&error) + ); + state.queue_manager.wake_seed_waiters(); + drop(control_guard); + return Ok(true); + } + Err(error) => return Err(error), + }; + if waiting_to_seed { // WaitingToSeed is an intentional paused GID with no download // permit. A resume request only wakes the Firelink seed scheduler; // it must not bypass the seed-slot admission gate. @@ -5385,12 +5433,6 @@ async fn resume_download( drop(control_guard); return Ok(true); } - let status = aria2_download_status( - state.aria2_port.load(std::sync::atomic::Ordering::Relaxed), - &state.aria2_secret, - &gid, - ) - .await?; match status.as_str() { "paused" => { let control_epoch = state.queue_manager.next_aria2_control_epoch(&id).await; @@ -11551,6 +11593,7 @@ mod tests { parse_media_playlist_metadata, normalize_media_connections, validate_enqueue_url, validate_enqueue_uris, validate_keychain_grant_request_id, + aria2_gid_not_found, retained_torrent_id_from_persisted_record, retained_torrent_info_hash_from_persisted_record, merge_durable_torrent_telemetry, torrent_identity_magnet, torrent_move_path_pair, @@ -11570,6 +11613,15 @@ mod tests { assert!(validate_keychain_grant_request_id(&"x".repeat(129)).is_err()); } + #[test] + fn aria2_missing_gid_detection_is_distinct_from_transport_and_resource_errors() { + assert!(aria2_gid_not_found( + r#"{"code": 1, "message": "GID#abc is not found"}"# + )); + assert!(!aria2_gid_not_found("error trying to connect: connection refused")); + assert!(!aria2_gid_not_found("aria2 error code 3: Resource not found")); + } + #[test] fn renderer_download_snapshots_cannot_lower_native_torrent_totals() { let existing = vec![