mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-24 09:46:29 +00:00
fix(downloads): harden aria2 resume lifecycle
This commit is contained in:
+73
-18
@@ -3399,7 +3399,7 @@ async fn resume_download(
|
|||||||
drop(control_guard);
|
drop(control_guard);
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let acquired = queue_manager.ensure_aria2_permit(&id_clone).await;
|
let acquired = queue_manager.ensure_aria2_permit(&id_clone).await;
|
||||||
if !acquired {
|
if !acquired && !queue_manager.has_active_permit(&id_clone).await {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let _control_guard = queue_manager.acquire_aria2_control(&id_clone).await;
|
let _control_guard = queue_manager.acquire_aria2_control(&id_clone).await;
|
||||||
@@ -3414,6 +3414,18 @@ async fn resume_download(
|
|||||||
queue_manager.release_permit(&id_clone).await;
|
queue_manager.release_permit(&id_clone).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if !queue_manager
|
||||||
|
.rebind_aria2_gid_epoch(&id_clone, &gid_clone, control_epoch)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::warn!(
|
||||||
|
"aria2 resume [{}]: gid {} disappeared before lifecycle rebind",
|
||||||
|
id_clone,
|
||||||
|
gid_clone
|
||||||
|
);
|
||||||
|
queue_manager.release_permit(&id_clone).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _ = app_handle_clone.emit(
|
let _ = app_handle_clone.emit(
|
||||||
"download-state",
|
"download-state",
|
||||||
crate::ipc::DownloadStateEvent::new(
|
crate::ipc::DownloadStateEvent::new(
|
||||||
@@ -3421,7 +3433,7 @@ async fn resume_download(
|
|||||||
crate::ipc::DownloadStatus::Downloading,
|
crate::ipc::DownloadStatus::Downloading,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let result = match rpc_call(
|
let unpause_error = match rpc_call(
|
||||||
aria2_port,
|
aria2_port,
|
||||||
&aria2_secret,
|
&aria2_secret,
|
||||||
"aria2.unpause",
|
"aria2.unpause",
|
||||||
@@ -3429,23 +3441,66 @@ async fn resume_download(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => result,
|
Ok(result) => ensure_aria2_gid_result("unpause", &gid_clone, &result)
|
||||||
Err(error) => {
|
.err()
|
||||||
queue_manager.release_permit(&id_clone).await;
|
.map(|error| error.to_string()),
|
||||||
log::error!("failed to resume aria2 gid {}: {}", gid_clone, error);
|
Err(error) => Some(format!("failed to resume aria2 gid {gid_clone}: {error}")),
|
||||||
let _ = app_handle_clone.emit(
|
|
||||||
"download-state",
|
|
||||||
crate::ipc::DownloadStateEvent::new(
|
|
||||||
&id_clone,
|
|
||||||
crate::ipc::DownloadStatus::Failed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if let Err(error) = ensure_aria2_gid_result("unpause", &gid_clone, &result) {
|
if let Some(unpause_error) = unpause_error {
|
||||||
queue_manager.release_permit(&id_clone).await;
|
match aria2_download_status(aria2_port, &aria2_secret, &gid_clone).await {
|
||||||
log::error!("failed to resume aria2 gid {}: {}", gid_clone, error);
|
Ok(status) if matches!(status.as_str(), "active" | "waiting") => {
|
||||||
|
log::warn!(
|
||||||
|
"aria2 resume [{}]: {} but daemon reports gid {} as {}; retaining permit",
|
||||||
|
id_clone,
|
||||||
|
unpause_error,
|
||||||
|
gid_clone,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(status) if status == "complete" => {
|
||||||
|
log::info!(
|
||||||
|
"aria2 resume [{}]: {} but daemon reports gid {} complete; reconciling completion",
|
||||||
|
id_clone,
|
||||||
|
unpause_error,
|
||||||
|
gid_clone
|
||||||
|
);
|
||||||
|
queue_manager
|
||||||
|
.apply_completion_locked(
|
||||||
|
&id_clone,
|
||||||
|
crate::queue::PendingOutcome::Complete,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(status) => {
|
||||||
|
queue_manager.release_permit(&id_clone).await;
|
||||||
|
log::error!(
|
||||||
|
"aria2 resume [{}]: {}; daemon reports gid {} as {}",
|
||||||
|
id_clone,
|
||||||
|
unpause_error,
|
||||||
|
gid_clone,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(status_error) => {
|
||||||
|
log::error!(
|
||||||
|
"aria2 resume [{}]: {}; could not verify gid {} after the RPC failure: {}; retaining permit",
|
||||||
|
id_clone,
|
||||||
|
unpause_error,
|
||||||
|
gid_clone,
|
||||||
|
status_error
|
||||||
|
);
|
||||||
|
let _ = app_handle_clone.emit(
|
||||||
|
"download-state",
|
||||||
|
crate::ipc::DownloadStateEvent::new(
|
||||||
|
&id_clone,
|
||||||
|
crate::ipc::DownloadStatus::Failed,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
let _ = app_handle_clone.emit(
|
let _ = app_handle_clone.emit(
|
||||||
"download-state",
|
"download-state",
|
||||||
crate::ipc::DownloadStateEvent::new(
|
crate::ipc::DownloadStateEvent::new(
|
||||||
|
|||||||
@@ -498,6 +498,10 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn has_active_permit(&self, id: &str) -> bool {
|
||||||
|
self.active_permits.lock().await.contains_key(id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear all permits belonging to aria2. Useful when aria2 WS connection drops.
|
/// Clear all permits belonging to aria2. Useful when aria2 WS connection drops.
|
||||||
pub async fn clear_aria2_permits(&self) {
|
pub async fn clear_aria2_permits(&self) {
|
||||||
let ids_to_fail: Vec<String> = {
|
let ids_to_fail: Vec<String> = {
|
||||||
@@ -768,6 +772,22 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
|||||||
buffered_outcome
|
buffered_outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rebind an existing paused GID to the new lifecycle created by resume.
|
||||||
|
/// The GID remains stable across aria2.pause/unpause, but its previous
|
||||||
|
/// epoch must not be reused after a pause invalidated that lifecycle.
|
||||||
|
pub async fn rebind_aria2_gid_epoch(&self, id: &str, gid: &str, epoch: u64) -> bool {
|
||||||
|
let _gid_state = self.aria2_gid_state.lock().await;
|
||||||
|
let mut gids = self.aria2_gids.write().unwrap();
|
||||||
|
let Some(mapping) = gids.get_mut(gid) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if mapping.id != id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mapping.epoch = epoch;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply an aria2 completion outcome: release permit + emit state.
|
/// Apply an aria2 completion outcome: release permit + emit state.
|
||||||
pub async fn apply_completion(&self, id: &str, outcome: PendingOutcome) {
|
pub async fn apply_completion(&self, id: &str, outcome: PendingOutcome) {
|
||||||
let _control_guard = self.acquire_aria2_control(id).await;
|
let _control_guard = self.acquire_aria2_control(id).await;
|
||||||
|
|||||||
@@ -273,6 +273,40 @@ async fn stale_terminal_event_cannot_complete_a_newer_control_epoch() {
|
|||||||
manager.release_registered_id("stale-event").await;
|
manager.release_registered_id("stale-event").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn resumed_gid_rebinds_to_the_new_control_epoch() {
|
||||||
|
use firelink_lib::queue::PendingOutcome;
|
||||||
|
|
||||||
|
let (mgr, _spawner) = make_manager(1);
|
||||||
|
let manager = Arc::new(mgr);
|
||||||
|
manager.push(aria2_task("resumed-gid")).await.unwrap();
|
||||||
|
let permit = manager.acquire_permit().await.expect("permit");
|
||||||
|
manager.park_permit("resumed-gid", permit).await;
|
||||||
|
manager
|
||||||
|
.remember_gid("resumed-gid".to_string(), "gid-resumed".to_string())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let resume_epoch = manager.next_aria2_control_epoch("resumed-gid").await;
|
||||||
|
manager
|
||||||
|
.handle_aria2_event("gid-resumed", PendingOutcome::Complete)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
manager.available_permits(),
|
||||||
|
0,
|
||||||
|
"the old GID epoch must not complete the resumed lifecycle"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(manager
|
||||||
|
.rebind_aria2_gid_epoch("resumed-gid", "gid-resumed", resume_epoch)
|
||||||
|
.await);
|
||||||
|
manager
|
||||||
|
.handle_aria2_event("gid-resumed", PendingOutcome::Complete)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(manager.available_permits(), 1);
|
||||||
|
assert!(manager.aria2_gid_for_download("resumed-gid").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn forgetting_aria2_gid_clears_mapping_without_releasing_twice() {
|
async fn forgetting_aria2_gid_clears_mapping_without_releasing_twice() {
|
||||||
let (mgr, _spawner) = make_manager(1);
|
let (mgr, _spawner) = make_manager(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user