-
Notifications
You must be signed in to change notification settings - Fork 7
fix(runner): abort parallel siblings on step failure #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use forge::runner::collect_parallel_results; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Arc, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| atomic::{AtomicUsize, Ordering}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::time::Duration; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use tokio::task::JoinHandle; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type StepResult = Result<(), Box<dyn std::error::Error + Send + Sync>>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[tokio::test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async fn collect_parallel_results_succeeds_when_all_tasks_ok() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let handles: Vec<JoinHandle<StepResult>> = vec![ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokio::spawn(async { Ok(()) }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokio::spawn(async { Ok(()) }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collect_parallel_results(handles).await.expect("all tasks should succeed"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #[tokio::test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async fn collect_parallel_results_aborts_remaining_tasks_on_failure() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let started = Arc::new(AtomicUsize::new(0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let finished = Arc::new(AtomicUsize::new(0)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let started_fail = Arc::clone(&started); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let handles: Vec<JoinHandle<StepResult>> = vec![ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokio::spawn(async move { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| started_fail.fetch_add(1, Ordering::SeqCst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Box::new(std::io::Error::other("step failed")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| as Box<dyn std::error::Error + Send + Sync>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokio::spawn({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let started = Arc::clone(&started); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let finished = Arc::clone(&finished); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async move { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| started.fetch_add(1, Ordering::SeqCst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokio::time::sleep(Duration::from_millis(500)).await; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finished.fetch_add(1, Ordering::SeqCst); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason this test passed even with the sequential implementation is that the failing task was placed first in the To make the test robust and guarantee true concurrent fail-fast behavior, we should place the slow task first in the vector.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let err = collect_parallel_results(handles) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .await | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .expect_err("first failing task should fail the stage"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert!(err.to_string().contains("step failed")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!(started.load(Ordering::SeqCst), 2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finished.load(Ordering::SeqCst), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "slow sibling must be aborted before completion" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awaiting the
JoinHandles sequentially in aforloop blocks on each task in the order they were spawned. If a later task (e.g., the second one) fails early, the loop will remain blocked awaiting the first task to complete before it can detect the failure and abort the others. This defeats the intended fail-fast behavior.To achieve true concurrent monitoring and immediate fail-fast, you can use
futures_util::future::select_allto await the first completed task.