Skip to content

vp run kills in-flight sibling tasks when one dependsOn task fails #3323

Closed as not planned

Description

@n0mer

vp run kills in-flight sibling tasks when one dependsOn task fails. There is no documented flag or task option to run every sibling to completion.

Version

vp v1.4.0 (local vite-plus@1.4.0): vite v8.3.5, rolldown v1.2.15, vitest v5.0.5, oxfmt v0.74.0, oxlint v1.89.0.

vp run --help on this build lists --ignore-depends-on, --parallel, --no-cache, and --concurrency-limit. It does not list --no-bail, --continue, or any other "run the rest" switch. docs/config/run.md documents command, dependsOn, and cache. dependsOn is described as tasks that must complete successfully before the parent starts. Nothing there says an in-flight sibling is allowed to finish after a peer fails.

Fixture

vite.config.ts:

import { defineConfig } from "vite-plus";

export default defineConfig({
  run: {
    enablePrePostScripts: false,
    cache: { scripts: false, tasks: false },
    tasks: {
      "child:fail": {
        command: "sh -c 'echo FAIL_START; exit 1'",
        cache: false,
      },
      "child:sentinel": {
        command: "sh -c 'echo SENTINEL_START; echo start > sentinel-start.txt; sleep 3; echo SENTINEL_DONE; echo done > sentinel-done.txt'",
        cache: false,
      },
      "gate:probe": {
        command: "echo parent-ran",
        dependsOn: ["child:fail", "child:sentinel"],
        cache: false,
      },
    },
  },
});

Command: vp run gate:probe --no-cache

Observed

  • Process exit code 1 (0/2 successful).
  • child:fail exited 1.
  • child:sentinel exited 137.
  • sentinel-start.txt was written. sentinel-done.txt was not.
  • Wall time was about 147ms, not the sibling's 3 second sleep.
  • The parent command did not run.

So a failing dependency does not only skip the parent. It SIGKILLs the sibling that had already started. A gate that fans out to many checks then hides every result that was in flight or not yet scheduled.

Expected

Either document and honor an option that runs every sibling to completion and still exits non-zero if any failed, or make that the default for dependsOn peers. Killing an in-flight sibling (exit 137) should not be the only behavior.

This matches an earlier measurement on 1.3.0 (vp run gate:lint stopped scheduling after the first red and killed in-flight tasks, exit 137). 1.4.0 still does it.

Activity

  1. voidzero-bot commented on Nov 3, 2026

    @voidzero-bot
    Contributor
    • Reproduced: Yes
    • Exploration: Yes
    • Unit Test: No — triage did not add or modify tests; the existing upstream vite-task snapshot tests already codify the current fast-fail behavior.
    • Priority: Priority P3: Minor Bug. The reported behavior is the documented/intended fast-fail design of the upstream vite-task runner, not a crash or data-loss bug. It mainly affects fan-out/gate-style task graphs that would benefit from a non-bailing option, but there is currently no such option.

    vp run cancels the fast_fail_token as soon as any leaf task exits non-zero. That token is wired into spawn.rs so every child process waits on it; when it fires, the runner calls child.start_kill(), which sends SIGKILL and produces the observed exit 137 for the still-running sibling. The parent task is then skipped because the scheduler closes the semaphore on cancellation.

    I wasn't able to find a fix, but I identified some areas that may be relevant. The fast-fail logic lives in the upstream vite-task git dependency (rev 40cb9cac46bddbf8aa5ce5373df1a6a1980acc38), specifically crates/vt/src/session/execute/scheduler.rs, crates/vt/src/session/execute/spawn.rs, crates/vt/src/session/mod.rs, and the CLI flags in crates/vt/src/cli/mod.rs. A proper fix would be to add a --continue / --no-bail flag, thread it through PlanOptions/ExecutionContext, and only cancel fast_fail_token when that mode is enabled, while still exiting non-zero if any task failed. The Cargo.toml rev for vite-task would then need to be bumped in vite-plus.

    Full Triage Report

    Triage Report: Issue #3323

    Summary

    vp run SIGKILLs an in-flight sibling task when a peer dependsOn task fails, instead of letting siblings run to completion and reporting all failures.

    Environment

    • OS: Ubuntu 24.04.1 LTS (7.0.0-1017-azure)
    • vp version: v1.4.0
    • Local vite-plus: v1.4.0
    • Node.js: v24.22.1
    • pnpm: v12.11.2
    • Branch under test: triagebot/fix-3323

    Reproduction

    Created /tmp/vite-plus-triage/gh-3323/repro with the following vite.config.ts:

    import { defineConfig } from "vite-plus";
    
    export default defineConfig({
      run: {
        enablePrePostScripts: false,
        cache: { scripts: false, tasks: false },
        tasks: {
          "child:fail": {
            command: "sh -c 'echo FAIL_START; exit 1'",
            cache: false,
          },
          "child:sentinel": {
            command: "sh -c 'echo SENTINEL_START; echo start > sentinel-start.txt; sleep 3; echo SENTINEL_DONE; echo done > sentinel-done.txt'",
            cache: false,
          },
          "gate:probe": {
            command: "echo parent-ran",
            dependsOn: ["child:fail", "child:sentinel"],
            cache: false,
          },
        },
      },
    });

    Command:

    vp run gate:probe --no-cache

    Result

    Reproduced on current main/triagebot/fix-3323.

    Output:

    $ sh -c 'echo FAIL_START; exit 1' ⊘ cache disabled
    $ sh -c 'echo SENTINEL_START; echo start > sentinel-start.txt; sleep 3; echo SENTINEL_DONE; echo done > sentinel-done.txt' ⊘ cache disabled
    FAIL_START
    
    
    ---
    vp run: 0/2 successful. (Run `vp run --last-details` for full details)
    ---
    

    Exit code: 1

    vp run --last-details:

    Task Details:
      [1] triage-3323-repro#child:fail: $ sh -c 'echo FAIL_START; exit 1' ✗ (exit code: 1)
          → Cache disabled in task configuration
      [2] triage-3323-repro#child:sentinel: $ sh -c 'echo SENTINEL_START; echo start > sentinel-start.txt; sleep 3; echo SENTINEL_DONE; echo done > sentinel-done.txt' ✗ (exit code: 137)
          → Cache disabled in task configuration
    

    Observations:

    • child:sentinel was killed with exit code 137 (SIGKILL).
    • Wall time was ~2.1 seconds, far below the 3 second sleep, confirming the sibling was terminated early.
    • Neither sentinel-start.txt nor sentinel-done.txt was present after the run.
    • The parent task (gate:probe) did not run, which is expected because dependencies failed.

    Control case

    Changed child:fail to exit 0 and re-ran vp run gate:probe --no-cache:

    • Exit code: 0
    • 3/3 successful
    • parent-ran was printed.
    • Both sentinel-start.txt and sentinel-done.txt were written.
    • Wall time was ~3.1 seconds, matching the sleep 3.

    This confirms the task configuration and commands work when no dependency fails, and the early termination of siblings is caused by the peer dependency failure.

    Conclusion

    The bug is reproducible. vp run aggressively terminates in-flight sibling tasks when any peer dependsOn task fails, which prevents gate/fan-out tasks from collecting full results. The task runner lacks an option (or default behavior) that runs all siblings to completion while still exiting non-zero for failures.


    Diagnosis

    Affected code path

    vp run is handled by the Vite Task (vt) git dependency, not by code in the local vite-plus repository. Routing in this repo sends the command into vt here:

    • packages/cli/src/bin.ts: dispatches to the NAPI run() function.
    • packages/cli/binding/src/lib.rs: run() spawns the Rust worker and calls crate::cli::main(...).
    • packages/cli/binding/src/cli/mod.rs: parses CLIArgs::ViteTask(command) and calls execute_vite_task_command(...), which calls session.main(command).await.

    The actual scheduling/spawning logic lives in the cargo git checkout of vt:

    • ~/.cargo/git/checkouts/vite-task-*/<rev>/crates/vt/src/session/execute/scheduler.rs
    • ~/.cargo/git/checkouts/vite-task-*/<rev>/crates/vt/src/session/execute/spawn.rs
    • ~/.cargo/git/checkouts/vite-task-*/<rev>/crates/vt/src/session/mod.rs (token creation)
    • ~/.cargo/git/checkouts/vite-task-*/<rev>/crates/vt/src/cli/mod.rs (RunFlags / PlanOptions)

    Root cause

    The task scheduler implements a hard-coded “fast-fail” strategy. Key points:

    1. In crates/vt/src/session/mod.rs around lines 421–422 and 744–745, the session creates two cancellation tokens:

      • fast_fail_token — cancelled when any task fails.
      • cancel_token — a child of fast_fail_token, cancelled on Ctrl-C (and inherited via the child relationship on fast-fail).
    2. In crates/vt/src/session/execute/scheduler.rs (execute_leaf, execute_expanded_graph):

      • When a leaf exits non-zero, execute_leaf calls self.fast_fail_token.cancel().
      • The scheduler detects cancelled() (the child token) in the FuturesUnordered processing loop, closes the semaphore, and stops scheduling new tasks.
    3. In crates/vt/src/session/execute/spawn.rs (spawn_tokio), the wait future is wrapped as:

      let exit_status = tokio::select! {
          status = child.wait() => status?,
          () = cancellation_token.cancelled() => {
              child.start_kill()?;
              #[cfg(windows)]
              job.terminate();
              child.wait().await?
          }
      };

      The cancellation_token passed to spawn() is fast_fail_token. When it fires, child.start_kill() sends SIGKILL on Unix, producing exit code 137 (128 + SIGKILL 9). This is exactly what child:sentinel reports.

    There is no CLI flag, config option, or environment variable that disables fast-fail. RunFlags exposes --ignore-depends-on, --parallel, --no-cache, --cache, --concurrency-limit, and --log, but nothing like --no-bail / --continue.

    Why this matches the observed behavior

    • The failing task cancels fast_fail_token.
    • Because cancel_token is a child of fast_fail_token, the whole graph becomes “cancelled”.
    • The scheduler stops scheduling new dependent tasks (parent never runs) and kills still-running siblings via start_kill().
    • Exit code 137 + 0/2 success count + short wall time are the expected symptoms of this code path.

    Suggested fix approach

    Because the failing code is in the external vite-task repository, the fix must be made there and the Cargo.toml rev bumped in this repo.

    Proposed change:

    1. Add a RunFlags option such as --continue / --no-bail (or make non-bailing the default for dependsOn peers).
    2. Pass it through ResolvedRunCommand::into_query_plan_request into a new PlanOptions field, e.g. bail: bool.
    3. Thread the flag into Session::execute_graph and ExecutionContext.
    4. In execute_leaf, only cancel fast_fail_token when bail is true. If bail is false, record the failure but let the scheduler continue scheduling ready dependents and let spawn.rs keep waiting on in-flight children.
    5. Still exit non-zero if any task failed, so CI and scripts catch failures.
    6. Update vp run --help output (Rust RunFlags doc strings; JS help is generated/derived from the same clap definitions or mirrored in packages/cli/src/help.ts).
    7. Also document the new option in docs/config/run.md if it is exposed as config, or in the CLI help / user docs if CLI-only.

    Edge cases to consider:

    • Interactive dev servers or long-lived tasks: even with --no-bail, Ctrl-C should still propagate through cancel_token and kill children.
    • Nested vp run invocations inside task scripts: the flag should propagate via PlanOptions to nested levels.
    • Mix of failed and cached tasks: cache hits should still report success; only actual failures should contribute to the final non-zero exit.
    • Multiple independent siblings failing: all failures should be collected and reported in the summary.
    • Windows job objects: fast-fail currently uses job.terminate(); with --no-bail we should avoid that path but still allow normal cleanup.

    Regression / version note

    The issue was observed on vp v1.4.0 and was already present on v1.3.0. The current triagebot/fix-3323 checkout (vite-plus 1.4.0) uses the same hard-coded fast-fail code, so release history is consistent with this root cause.


    Verification

    Claim

    Reporter observed: when one dependsOn peer fails, vp run SIGKILLs the in-flight sibling (child:sentinel exit 137) and reports 0/2 successful. The reporter expects either a documented option to let every sibling finish (and exit non-zero), or that behavior as the default.

    Evidence

    1. Product documentation. docs/config/run.md describes dependsOn as "tasks that must complete successfully before this one starts" but does not say what happens to concurrent siblings when one fails. The guide at docs/guide/run.md also does not mention fast-fail behavior.

    2. Vite Task cancellation documentation (vite-task git checkout docs/cancellation.md, upstream) explicitly documents the current behavior:

      "When any task exits with non-zero status: All other running child processes are killed immediately (SIGKILL on Unix, TerminateJobObject on Windows). No new tasks are scheduled."

      This confirms the child:sentinel SIGKILL is the intended fast-fail mechanism.

    3. Source code comments. In the vt scheduler.rs:

      "Token cancelled when a task fails. Kills in-flight child processes (via start_kill in spawn.rs)."
      "Fast-fail: if any task fails, execute_leaf cancels the fast_fail_token (killing in-flight child processes)."

      And spawn.rs spawn_tokio() races child.wait() against the cancellation token and calls child.start_kill() when it fires.

    4. Existing tests / CLI snapshots in the vt repo codify fast-fail:

      • crates/vt_bin/tests/e2e_snapshots/fixtures/concurrent_execution/snapshots/failure_kills_concurrent_tasks.md: explicitly asserts "When one concurrent task fails, the sibling running under inherited stdio must be cancelled."
      • crates/vt_bin/tests/e2e_snapshots/fixtures/exit_codes/snapshots/task_failure_fast_fails_remaining_tasks.md: asserts a failing task under -r fast-fails the run and skips remaining packages.
      • crates/vt_bin/tests/e2e_snapshots/fixtures/exit_codes/snapshots/dependency_failure_fast_fails_dependents.md: asserts dependents are skipped when a dependency fails.
    5. Prior release / change history. Git commit 45b667ce in vite-task ("fix(cache): stop remote cache requests on Ctrl-C and fast-fail") intentionally kept fast-fail semantics and shortened the comment to point to spawn.rs for killing. Git commit history shows fast-fail has been the intended design, not an accidental regression.

    6. GitHub search. Searching the voidzero-dev/vite-plus repo for no-bail, continue, fast fail, and exit 137 found no earlier proposals for a non-bailing mode; issue vp run kills in-flight sibling tasks when one dependsOn task fails #3323 is the only relevant one.

    Verdict

    This is intended behavior. The task runner is explicitly designed to fast-fail on the first non-zero task exit, killing in-flight siblings and skipping not-yet-scheduled dependents. The reporter's request is effectively a feature request for a --no-bail / --continue-style option or a change to the default fast-fail policy, rather than a bug in the implemented behavior.

    Confidence

    High. Intent is documented in upstream docs/cancellation.md, enforced by snapshot tests, and explicit in both scheduler comments and commit history.

    This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue.

  2. n0mer commented on Nov 3, 2026

    @n0mer
    Author

    Closing this. It was opened in error. We are not asking voidzero to track it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions