Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions e2e/tasks/test_task_failure_hang
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
# https://github.com/jdx/mise/discussions/6391

cat <<EOF >mise.toml
[tasks.fails]
run = '''
sleep 1;
echo "An error occurred!"
exit 1;
'''

[tasks.deponfails]
depends = ["fails"]
run = 'echo "This will not run because the dependency fails."'

[tasks.grouped]
run = [
{ task = "deponfails" }
]
EOF

# Test that task failure with dependencies does not hang
timeout 5s mise run grouped 2>&1 && exit_code=0 || exit_code=$?

# Check if it was a timeout (exit code 124)
if [ "$exit_code" -eq 124 ]; then
echo "FAIL: Task hung after dependency failure (timeout reached)"
exit 1
fi

# The command should fail with exit code 1
if [ "$exit_code" -ne 1 ]; then
echo "Expected exit code 1, got $exit_code"
exit 1
fi

echo "Test passed: task with failing dependency did not hang"
38 changes: 34 additions & 4 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ impl Run {
let sub_deps = Arc::new(Mutex::new(sub_deps));

// Pump subgraph into scheduler and signal completion via oneshot when done
let (done_tx, done_rx) = oneshot::channel::<()>();
let (done_tx, mut done_rx) = oneshot::channel::<()>();
let task_env_directives: Vec<EnvDirective> =
task_env.iter().cloned().map(Into::into).collect();
{
Expand Down Expand Up @@ -887,10 +887,40 @@ impl Run {
});
}

// Wait for completion
done_rx.await.map_err(|e| eyre!(e))?;
// Wait for completion with a check for early stopping
loop {
// Check if we should stop early due to failure
if self.is_stopping() && !self.continue_on_error {
trace!("inject_and_wait: stopping early due to failure");
// Clean up the dependency graph to ensure completion
let mut deps = sub_deps.lock().await;
let tasks_to_remove: Vec<Task> = deps.all().cloned().collect();
for task in tasks_to_remove {
deps.remove(&task);
}
drop(deps);
Comment on lines +897 to +901
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

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

This cleanup code holds the lock while collecting all tasks and then iterates through them. Consider clearing the dependency graph more efficiently with a single operation like deps.clear() if available, or release the lock between collection and removal to reduce lock contention.

Suggested change
let tasks_to_remove: Vec<Task> = deps.all().cloned().collect();
for task in tasks_to_remove {
deps.remove(&task);
}
drop(deps);
deps.clear();
// If deps.clear() is not available, fallback to the original method:
// let tasks_to_remove: Vec<Task> = deps.all().cloned().collect();
// drop(deps);
// let mut deps = sub_deps.lock().await;
// for task in tasks_to_remove {
// deps.remove(&task);
// }
// drop(deps);

Copilot uses AI. Check for mistakes.
// Give a short time for the spawned task to finish cleanly
let _ = tokio::time::timeout(Duration::from_millis(100), done_rx).await;
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

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

The timeout duration (100ms) is hardcoded and appears twice in this function. Consider extracting this as a named constant to improve maintainability and consistency.

Copilot uses AI. Check for mistakes.
return Err(eyre!("task sequence aborted due to failure"));
}

// Try to receive the done signal with a short timeout
match tokio::time::timeout(Duration::from_millis(100), &mut done_rx).await {
Ok(Ok(())) => {
trace!("inject_and_wait: received done signal");
break;
}
Ok(Err(e)) => {
return Err(eyre!(e));
}
Err(_) => {
// Timeout, check again if we should stop
continue;
}
}
}

// Check if we failed during the execution
// Final check if we failed during the execution
if self.is_stopping() && !self.continue_on_error {
return Err(eyre!("task sequence aborted due to failure"));
}
Expand Down
Loading