Skip to content
14 changes: 11 additions & 3 deletions crates/pm/src/service/install.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::util::cli_enum::ScriptPolicy;
use anyhow::Context;
use anyhow::Result;
use futures::stream::{FuturesUnordered, StreamExt};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::time::Instant;
Expand Down Expand Up @@ -83,7 +84,14 @@ pub async fn install_packages(
depths.sort_unstable();

for depth in depths.iter() {
let mut clone_tasks: Vec<tokio::task::JoinHandle<Result<()>>> = Vec::new();
// Stream task completion within a layer instead of joining serially.
// The previous `for task in clone_tasks { task.await?? }` walks tasks
// in spawn order — slow tasks block the pop of already-finished
// ones, leaving idle latency between layers on deep trees. With
// FuturesUnordered we drain in completion order, propagating the
// first error early.
let mut clone_tasks: FuturesUnordered<tokio::task::JoinHandle<Result<()>>> =
FuturesUnordered::new();
Comment on lines +93 to +94

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using tokio::task::JoinSet instead of FuturesUnordered<tokio::task::JoinHandle<_>>.

While FuturesUnordered allows for processing completions in any order, it has a significant drawback in this context: when the FuturesUnordered collection is dropped (e.g., when an error is propagated via ? in the drain loop), the contained JoinHandles are dropped, which detaches the tasks rather than cancelling them. This means background installation tasks will continue to run even after an error has been returned to the caller.

tokio::task::JoinSet is more efficient for this pattern and, crucially, aborts all remaining tasks when dropped. This ensures that if one package fails, all other pending clones in the same layer are immediately cancelled, saving resources and avoiding orphaned background work.

Note: Implementing this would also involve replacing tokio::spawn(...) and clone_tasks.push(task) with clone_tasks.spawn(...).

References
  1. Resource management and task lifecycle: prefer JoinSet over FuturesUnordered of JoinHandles when cancellation on error is desired, as JoinHandles detach on drop.


if let Some(packages) = groups.get(depth) {
for (path, package) in packages.iter() {
Expand Down Expand Up @@ -169,8 +177,8 @@ pub async fn install_packages(
}
}

for task in clone_tasks {
task.await??;
while let Some(joined) = clone_tasks.next().await {
joined??;
}
}

Expand Down
Loading