fix(peer): drain cancelled download workers

Keep initial peer transfers, retry attempts, and chunk receivers structurally owned until they quiesce. Cancellation now stops opening new streams, flushes accepted file writes, and drains active work before ownership rollback can begin.

Leave operation admission owned by the running download task when liveness detects that every source disappeared, and emit the peers-gone notification only once.

Test Plan:
- just fmt (Rust and configured formatters completed; 39 pre-existing rumdl findings remain)
- just clippy
- just test
- git diff --cached --check
This commit is contained in:
ddidderr committed 2026-08-09 20:05:42 +02:00
1 parent 7a77d3ffd1
commit 5bb4a8b611
6 files changed
+417 -127

No files matched your search

+45 -45
View File
@@ -212,27 +212,34 @@ async fn handle_active_downloads_without_peers(
return;
}
let mut changed = false;
for id in active_ids {
if peers_still_have_game(peer_game_db, &id).await {
continue;
}
changed |= active_operations.write().await.remove(&id).is_some();
let Some(cancel_token) = active_downloads.write().await.remove(&id) else {
continue;
let cancelled = {
// An exclusive guard makes the check-and-cancel transition one-shot even when
// concurrent liveness checks remove the last peers at the same time.
let active_downloads = active_downloads.write().await;
let Some(cancel_token) = active_downloads.get(&id) else {
continue;
};
if cancel_token.is_cancelled() {
false
} else {
cancel_token.cancel();
true
}
};
cancel_token.cancel();
if !cancelled {
continue;
}
events::send(
tx_notify_ui,
PeerEvent::DownloadGameFilesAllPeersGone { id: id.clone() },
);
}
if changed {
events::emit_active_operations(active_operations, tx_notify_ui).await;
}
}
async fn peers_still_have_game(peer_game_db: &Arc<RwLock<PeerGameDB>>, game_id: &str) -> bool {
@@ -248,16 +255,10 @@ mod tests {
use tokio_util::sync::CancellationToken;
use super::handle_active_downloads_without_peers;
use crate::{
ActiveOperation,
ActiveOperationKind,
PeerEvent,
context::OperationKind,
peer_db::PeerGameDB,
};
use crate::{PeerEvent, context::OperationKind, peer_db::PeerGameDB};
#[tokio::test]
async fn all_peers_gone_cancels_download_and_emits_peers_gone_then_active_snapshot() {
async fn all_peers_gone_cancels_once_and_leaves_cleanup_to_download_owner() {
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let active_operations = Arc::new(RwLock::new(HashMap::from([(
"game".to_string(),
@@ -279,30 +280,38 @@ mod tests {
.await;
assert!(cancel.is_cancelled());
assert!(!active_operations.read().await.contains_key("game"));
assert!(!active_downloads.read().await.contains_key("game"));
assert_eq!(
active_operations.read().await.get("game"),
Some(&OperationKind::Downloading)
);
assert!(active_downloads.read().await.contains_key("game"));
let event = rx.recv().await.expect("peers-gone event should be emitted");
assert!(matches!(
event,
PeerEvent::DownloadGameFilesAllPeersGone { id } if id == "game"
));
let event = rx
.recv()
.await
.expect("active operation snapshot should be emitted");
assert!(matches!(
event,
PeerEvent::ActiveOperationsChanged { active_operations } if active_operations.is_empty()
));
assert!(
rx.try_recv().is_err(),
"peers-gone cancellation must not emit extra events"
"cancellation must not emit a premature active-operation snapshot"
);
handle_active_downloads_without_peers(
&peer_game_db,
&active_operations,
&active_downloads,
&tx,
)
.await;
assert!(
rx.try_recv().is_err(),
"an already-cancelled download must not emit peers-gone twice"
);
}
#[tokio::test]
async fn all_peers_gone_cancels_multiple_downloads_without_stuck_entries() {
async fn all_peers_gone_cancels_multiple_downloads_without_releasing_admission() {
let peer_game_db = Arc::new(RwLock::new(PeerGameDB::new()));
let first_cancel = CancellationToken::new();
let second_cancel = CancellationToken::new();
@@ -328,14 +337,17 @@ mod tests {
assert!(first_cancel.is_cancelled());
assert!(second_cancel.is_cancelled());
let operations = active_operations.read().await;
assert!(!operations.contains_key("first"));
assert!(!operations.contains_key("second"));
assert_eq!(operations.get("first"), Some(&OperationKind::Downloading));
assert_eq!(operations.get("second"), Some(&OperationKind::Downloading));
assert_eq!(
operations.get("installing"),
Some(&OperationKind::Installing)
);
drop(operations);
assert!(active_downloads.read().await.is_empty());
let downloads = active_downloads.read().await;
assert!(downloads.contains_key("first"));
assert!(downloads.contains_key("second"));
drop(downloads);
let mut cancelled_ids = Vec::new();
for _ in 0..2 {
@@ -347,21 +359,9 @@ mod tests {
}
cancelled_ids.sort();
assert_eq!(cancelled_ids, vec!["first", "second"]);
let event = rx
.recv()
.await
.expect("active operation snapshot should be emitted");
assert!(matches!(
event,
PeerEvent::ActiveOperationsChanged { active_operations }
if active_operations == vec![ActiveOperation {
id: "installing".to_string(),
operation: ActiveOperationKind::Installing,
}]
));
assert!(
rx.try_recv().is_err(),
"multiple peers-gone cancellations must not emit extra events"
"multiple cancellations must not emit an active-operation snapshot"
);
}
}