Skip to content

test(parallel): pin cancel-and-drain against the join_all panic - #11532

Merged
jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:test/parallel-cancel-drain
Jul 31, 2026
Merged

jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:test/parallel-cancel-drain

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Test-only. src/parallel.rs aborts its JoinSet when a task fails and then drains with join_next(), with a comment saying exactly why:

jset.abort_all();
// Drain remaining tasks - don't use join_all() as it panics on cancelled tasks
while jset.join_next().await.is_some() {}

That is the fix from #7280. Nothing pins it. The file's only test is a happy path where no task fails, so anyone who "simplifies" the drain back to join_all() reintroduces the task N was cancelled panic — reported three separate times in #5263, #5369 and #5414 — and CI stays green.

The tests

a_failure_aborts_unfinished_siblings_without_panicking — two inputs. Item 0 fails; item 1 parks for 10s, far longer than the test takes, so it is unfinished when the drain reaches it. JoinSet::join_all() panics on any task that was cancelled rather than completed, so that is the whole precondition — the sibling does not have to have been polled. Asserts: Err returned rather than a panic, and the sibling did not run to completion, i.e. it was cancelled.

results_follow_input_order_not_completion_order — parallel pre-fills Vec<Option<U>> and writes by the enumerate index, and src/backend/conda.rs:423 zips its input vector against this function's output vector, which is correct only while that holds. The existing test can't detect a regression: x * 2 over 1..=5 is monotonic, so completion order and input order coincide. Here item 1 signals completion and item 0 does not return until it observes that, so item 0 finishes last and the output is still in input order.

Why neither test depends on jobs

A permit is taken before spawn, so a task that waits unconditionally on a sibling stops that sibling from ever being spawned. That matters because jobs == 1 is reachable — Settings::try_get forces it when raw = true:

if settings.raw {
    settings.jobs = 1;
}

So: the failing task is item 0, spawned first, never starved of a permit by the sibling; and both handshakes go through a yield_until helper that gives up after a bounded number of turns. At jobs == 1 the first task completes, releases its permit, the second is spawned, and abort_all() cancels it — the same drain path, no hang. At any larger budget both are in flight and the handshake decides the interleaving. Giving up early only weakens the interleaving; it never changes what is asserted.

Shared state is static AtomicBool rather than a captured handle because F: Fn(T) -> Fut + Send + Copy + 'static — Copy rules out a closure holding an Arc, which is how the rest of the repo sequences async tests.

What I can and cannot claim

I can't demonstrate the test failing against the pre-#7280 code, since that would mean reverting the fix. The argument is structural: join_all() panics on a cancelled task, and the test guarantees an unfinished-then-cancelled task exists at drain time.

Known gap, deliberately left out: the Err(e) => e.into() JoinError arm, which needs a panicking task. There is prior art in src/cli/plugins/mod.rs, but it dumps a panic backtrace into test output for a branch no bug report has ever pointed at. Happy to add it if you'd rather have the coverage.

No local cargo run — rustfmt --edition 2024 only. lint, linux cargo test, unit-macos and windows-unit cover compile and behaviour on all three platforms.

Summary by CodeRabbit

  • Bug Fixes
    • Parallel task execution now cancels remaining sibling tasks when one task fails, without causing a panic.
    • Successful parallel results consistently preserve the original input order.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds asynchronous tests for parallel, covering sibling-task cancellation after failure and preservation of input order when completion order differs.

Changes

Parallel task behavior

Layer / File(s) Summary
Cancellation and result-order tests
src/parallel.rs
Adds timing and atomic-state test support, verifies failed tasks cancel running siblings without panicking, and confirms results retain input order.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit with tests in my hat,
Checking tasks where they’re at.
One fails, friends stop,
Results stay in the right hop—
Parallel paths now behave just so!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary cancellation-and-drain regression test added for parallel task handling.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds regression tests covering cancellation of unfinished parallel tasks after an error and preservation of input order in parallel results.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/parallel.rs Adds test-only synchronization helpers and regression coverage for sibling cancellation and input-ordered results.

Reviews (2): Last reviewed commit: "test(parallel): pin cancel-and-drain aga..." | Re-trigger Greptile

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 31, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/parallel.rs`:
- Around line 67-89: Ensure the tests around
a_failure_aborts_running_siblings_without_panicking and the related ordering
test run with a concurrency budget of at least two, rather than relying on the
global Settings::get().jobs value. Configure the test settings or inject the
limit so both sibling tasks can be spawned concurrently while preserving the
existing abort and ordering assertions.
- Around line 111-115: Update the completion-order setup in the parallel test
around the parallel invocation to use an explicit synchronization handshake,
such as an atomic flag or notification: have task 1 signal that it has started
or reached the required point, then let task 0 proceed only after observing that
signal. Remove reliance on the fixed 50 ms sleep while preserving the intended
order where item-1 completes before item-0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 78d85c04-4bc9-4a59-aedf-43ad955c8965

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5d0e6 and 6a56c9b.

📒 Files selected for processing (1)
  • src/parallel.rs

Comment thread src/parallel.rs Outdated
Comment thread src/parallel.rs
`parallel()` aborts its `JoinSet` when a task fails and then drains with
`join_next()`, because `join_all()` panics on any task that was cancelled
rather than completed (jdx#7280). Nothing pinned that behaviour: the only test was
a happy path where no task fails, so restoring `join_all()` would keep CI green
while reintroducing the `task N was cancelled` panic reported in jdx#5263, jdx#5369
and jdx#5414.

Two tests, no behaviour change:

- `a_failure_aborts_unfinished_siblings_without_panicking` — asserts an `Err` is
  returned rather than a panic, and that the sibling was cancelled rather than
  allowed to finish.
- `results_follow_input_order_not_completion_order` — results are indexed by
  input position, not by completion order. `src/backend/conda.rs:423` zips its
  input vector against this function's output and is only correct while that
  holds; the existing test cannot catch a regression there, since `x * 2` over
  a sorted input looks the same either way.

Neither test depends on the `jobs` setting, which can legitimately be 1:
`raw = true` forces exactly that in `Settings::try_get`. Because a permit is
taken before `spawn`, a task that waits unconditionally on a sibling would stop
that sibling from ever being spawned, so the waits are bounded and the failing
task is spawned first. A sibling cancelled before its first poll is still a
cancelled task at drain time, which is the condition `join_all()` panics on, so
the property holds at any budget.

Shared state lives in statics because `F` is bound by `Copy`, which a closure
capturing an `Arc` would not satisfy.
@JamBalaya56562
JamBalaya56562 force-pushed the test/parallel-cancel-drain branch from 6a56c9b to 3714dec Compare July 31, 2026 00:34
@greptile-apps
greptile-apps Bot dismissed their stale review July 31, 2026 00:34

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@jdx
jdx enabled auto-merge (squash) July 31, 2026 00:51
@jdx
jdx merged commit 09321ab into jdx:main Jul 31, 2026
29 checks passed
@JamBalaya56562
JamBalaya56562 deleted the test/parallel-cancel-drain branch July 31, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants