Skip to content

fix(axl): don't orphan the bazel client when a spawn fails partway - #1381

Open
cristifalcas wants to merge 1 commit into
mainfrom
cf/fix-bazel-child-leak
Open

fix(axl): don't orphan the bazel client when a spawn fails partway#1381
cristifalcas wants to merge 1 commit into
mainfrom
cf/fix-bazel-child-leak

Conversation

@cristifalcas

@cristifalcas cristifalcas commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Prevent AXL from leaving Bazel clients running when build setup fails or a Build is abandoned without calling wait():

  • Own newly spawned clients until a Build is fully constructed, terminating and reaping them if setup returns an error.
  • Terminate and unregister unfinished clients when the Starlark build object is dropped, avoiding stale Bazel server locks and PID-reuse hazards.
  • Cover abandoned, defused, and already-reaped client lifecycles with unit tests.

visible to end-users: yes
docs: no
breaking change: no
release notes: yes

@cristifalcas
cristifalcas requested a review from thesayyn August 8, 2026 14:36
@cristifalcas
cristifalcas marked this pull request as ready for review August 8, 2026 14:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de6060d82d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/axl-runtime/src/engine/bazel/build.rs
cristifalcas added a commit that referenced this pull request Aug 8, 2026
`Drop for Build` reaped the child but left the `LiveBazelGuard` to fall out in
field order — after `build_event_stream`, `workspace_event_stream` and
`execlog_stream`. Those join their reader threads, so for that whole window a
reaped and therefore reusable PID stayed in `live_pids()`, and a CI cancel
landing there would signal whatever now owned it.

That is the hazard the `live_guard` field is a `RefCell<Option<_>>` for, and
`wait()` / `try_wait()` both take it the moment they observe the exit. Do the
same on the drop path. Applied to `SpawnedClient` too — its window is a few
instructions rather than three stream joins, but one rule beats reasoning about
which window is small enough.

Reported by the Codex reviewer on #1381.

Not covered by a test: what matters is ordering *within* the drop sequence, and
observing that from outside needs instrumentation out of proportion to the fix.

Also trims the comments this PR added to what cannot be inferred from the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Build::spawn` creates the client with `cmd.spawn()` and then does fallible
work after it — `BuildEventStream::spawn(...)?`, binding iterator handles,
configuring sinks. Every one of those `?` returns dropped a bare
`std::process::Child`, which by design does not kill on drop, so the client
kept running and was reparented to init.

The `LiveBazelGuard` dropped on the same path, which makes it worse rather than
better: the PID leaves the live registry, so the OS-signal handler that exists
precisely to stop clients outliving aspect-cli can no longer reach it.
`live.rs` documents this as its own reason for existing — an orphaned client
holds the JVM-server lock and the next invocation on that runner hangs at
"Running Bazel server needs to be killed".

The reachable trigger is a `build_events` list carrying an already-bound
`iterator()` handle: `iter.bind(...)` fails after the spawn. That is
`iterator_handle_rejects_reuse`, which leaked one `basil` per run — found while
running the suite for unrelated work, confirmed on two worktrees.

`SpawnedClient` owns the child between `cmd.spawn()` and a constructed `Build`,
terminating it on drop; `defuse()` hands the child and guard over on success.
`Drop for Build` covers the same defect one level up, where AXL starts a build
and then returns or fails without calling `wait()`. Note it fires at Starlark
heap teardown, which spans the whole AXL run — enough to keep a client from
outliving aspect-cli, but it does not shorten the in-run server-lock window.

Both route through one helper: SIGINT so bazel can release the server lock, a
200ms grace, then SIGKILL. The trailing `wait()` is what reaps; killing alone
leaves a zombie. The grace is skipped when the signal never went out — a failed
`kill(2)`, or the non-unix stub that can never send one.

Both drop paths take the `LiveBazelGuard` the moment the child is reaped rather
than letting it fall out in field order. `live_guard` is declared after the
three streams, so field order would leave a reaped and therefore reusable PID
in `live_pids()` while they join their reader threads, and a CI cancel landing
there would signal whatever now owned it. `wait()` and `try_wait()` already
take the guard on observing the exit for exactly this reason.

Tests assert the guard rather than the AXL path: snapshotting the process-wide
`live_pids()` would race every other test in the binary. `is_pid_running` uses
`kill(pid, 0)`, which still succeeds on an unreaped zombie, so the reap
assertion fails if the trailing `wait()` is removed. The unregister ordering is
not covered — what matters is sequencing within the drop, and observing that
from outside needs instrumentation out of proportion to the fix.

axl-runtime 380 passed / 0 failed, aspect-cli 74 passed / 0 failed, and a full
run now leaves no orphaned basil.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cristifalcas
cristifalcas force-pushed the cf/fix-bazel-child-leak branch from 378ffd4 to 7fc9c84 Compare August 8, 2026 15:08
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.

1 participant