Skip to content

fix: an abandoned launch keeps its place in the queue (#141, partly) - #143

Merged
glslang merged 2 commits into
mainfrom
fix/an-abandoned-launch-keeps-its-place
Sep 4, 2026
Merged

fix: an abandoned launch keeps its place in the queue (#141, partly)#143
glslang merged 2 commits into
mainfrom
fix/an-abandoned-launch-keeps-its-place

Conversation

@glslang

@glslang glslang commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Partial, deliberately, and the part it does not fix is structural. #141 stays open, narrowed.

The defect, reproduced

Dropping a launch guard before anything pumps does not un-queue its CreateProcessWide, and
forget removed the entry leaving nothing behind. That process still arrives, is new to the next
launch's snapshot because it did not exist when that snapshot was taken, and is claimed by it.

Measured before the fix — a launch of ping.exe behind an abandoned cmd.exe:

the second launch was given the abandoned launch's process ...
Claimed pid 0x2e0, whose row is:
.  0	id: 2e0	create	name: cmd.exe

The fix

The entry stays, marked abandoned. There is nothing to leave as an exclusion instead — which
process the queued create will produce is exactly what nobody knows yet — and being first in
registration order is what makes it work, since deliver offers in that order.

Only a launch, and only one given nothing. An abandoned attach is already covered by the
engine's attached_processes, which is what Pending::wants reads it for; keeping its entry would
leave presence answering Listed for an id nobody holds. An entry with a claim is removed and its
claim inherited, as before. Launched(None) is removed too — it can never claim anything.

What it does not fix, and cannot

The next launch still does not reliably get its own process. deliver offers in registration
order and the abandoned entry is first, so it takes whichever process arrives first — and one
WaitForEvent realises both queued creates
(measured: session 0 → 2 on a single pump), so which
one the event names is a coin flip. Identifying a launch by arrival order is the residual ambiguity
Arrival has always documented for two launches pending at once; #139 closed "both guards get the
same arrival", not "each guard gets its own". Closing the second needs something to match on
rather than an order.

What this does deliver: the abandoned create is accounted for. One entry absorbs one arrival, so
the next launch's wait() returns only once a second process has stopped — where before it could
return with its own process not yet created at all.

Two of my own tests passed for the wrong reason

Both instances of the same trap, and worth reading before adding a third:

  • Counting processes when wait() returns says nothing, because one pump realises both creates.
  • Asking whether the second program is listed says nothing either — membership is the weaker
    claim this module is built on not confusing with having stopped. It reported 0 short in 10 while
    the defect was live.

A third draft asserted the image name and failed ~50% of runs with the fix in place, which is
how the ordering limitation above was found rather than shipped. The test now reads the register and
asserts the property that actually holds: 8/8 with the fix, 4/4 failing without it, deterministic
both ways.

examples/abandoned_launch.rs is the measurement, kept as the record of what the public surface can
and cannot see. It also settles a contradiction between two recorded facts: a bad image fails at
launch_process_begin (0x80070002, 1.5 ms), not inside the wait — so deferred_arrival's arm
C description is wrong, worker.rs was right, and the "ghost that nothing retires" objection this
issue was deferred on does not exist.

Verification

  • cargo nextest run: 189 passed. cargo test --doc: 4. fmt and clippy clean.
  • Miri, full suite: 148 passed.
  • deferred_arrival: arm A 0 short in 40 under 24 spinners, both orderings — unmoved.
    session_fuzz: clean over seeds 1, 2, 7, 13.

Refs #141, #136, #133

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY

dbgscope#141, partly. Dropping a launch guard before anything pumps does
not un-queue its `CreateProcessWide`, and `forget` removed the entry with
nothing left behind -- so that process still arrived, was new to the *next*
launch's snapshot because it did not exist when that snapshot was taken,
and was claimed by it. Measured before the fix: a launch of `ping.exe`
behind an abandoned `cmd.exe` was delivered the `cmd.exe`.

The entry now stays, marked `abandoned`. There is nothing to leave as an
exclusion instead -- which process the queued create will produce is
exactly what nobody knows yet -- and being first in registration order is
what makes it work, since `deliver` offers in that order.

Only a launch, and only one given nothing. An abandoned *attach* is already
covered by the engine's `attached_processes`, which is what `Pending::wants`
reads it for, and keeping its entry would leave `presence` answering
`Listed` for an id nobody holds. An entry with a claim is removed and its
claim inherited, as before. `Launched(None)` is removed too: it can never
claim anything, so keeping it would leave an entry that does nothing.

**What this does not fix, and cannot.** The next launch still does not
reliably get its *own* process. `deliver` offers in registration order and
the abandoned entry is first, so it takes whichever process arrives first --
and one `WaitForEvent` realises *both* queued creates (measured: session
0 -> 2 on a single pump), so which one the event names is a coin flip.
Identifying a launch by arrival order is the residual ambiguity `Arrival`
has always documented for two launches pending at once; #139 closed "both
guards get the same arrival", not "each guard gets its own". What this does
deliver is that the abandoned create is *accounted for*: one entry absorbs
one arrival, so the next launch's wait() returns only once a second process
has stopped, where before it could return with its own process not yet
created at all. #141 stays open, narrowed to the identification.

Two of this change's own tests passed for the wrong reason before landing,
both worth knowing. Counting processes when wait() returns says nothing,
because one pump realises both creates. Asking whether the second program
is *listed* says nothing either -- membership is the weaker claim this
module is built on not confusing with having stopped -- and it reported "0
short in 10" while the defect was live. The test reads the register
instead, and asserts the property that holds: 8/8 with the fix, 4/4 failing
without it. `examples/abandoned_launch.rs` is the measurement, kept as the
record of what the public surface can and cannot see.

Refs #141, #136, #133

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 97cffbed-e561-4963-a95a-6d643b3ba087


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.

@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: 54cc34f125

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/dbgeng.rs
Comment on lines +677 to +681
if self.pending[index].claim == Claim::Waiting
&& matches!(self.pending[index].what, Arrival::Launched(Some(_)))
{
self.pending[index].abandoned = true;
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Retire launches that fail during their wait

When CreateProcessWide accepts a request but WaitForEvent later reports that the spawn failed—a behavior this module still explicitly documents in wait_for_live_targetPendingTarget::wait returns Err and then drops its Registered. This branch nevertheless marks that still-Waiting entry as abandoned, even though no process will arrive to satisfy it, so the next successful launch's stop is consumed by the stale entry and its own guard can time out. Distinguish a never-waited drop from a wait that ended with a definitive launch failure, and remove the registration on the latter path.

Useful? React with 👍 / 👎.

Review on #143, and a regression this branch introduced. `forget` keeps an
abandoned launch's entry so its queued create is accounted for -- but
`PendingTarget::wait` drops its `Registered` on the *failure* path too, so a
launch that pumped its whole bound and saw nothing was kept as abandoned and
would then take the next launch's stop. That is the exact failure keeping an
entry exists to prevent, reached from the other side.

`Arrivals::discard` forgets outright whatever the state, and
`wait_for_live_target` calls it beside `retire_deferred_attachment` on the
one ending that says nothing is coming: pumped the whole bound, still
absent. Not on the interrupted branch, which says nothing about whether the
process is still on its way, and not on a pump that failed with a live
session -- the same narrowing `retire_deferred_attachment` already carries
and for the same reason.

**The call site is not pinned and the comment says so.** The rule is
(`..._keeps_its_place_and_an_attach_does_not` gained a block that fails if
`discard` keeps the entry), but the wiring needs a launch whose process
never arrives inside `LIVE_WAIT_MS`, and a create that reaches the queue
produces a process -- every way a launch fails with nothing created lands on
`launch_process_begin`'s own `?`, measured at 1.5ms for a missing image.
Backing the call out leaves all 189 green; that was checked rather than
supposed, which is why it is written down instead of implied. The attach
side has a test for the equivalent wiring only because a pid that never
joins is constructible where a launch is not.

Refs #141, #143

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyRDk1yX5UqRkdpGgodrMY
@glslang

glslang commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Correct, and it is this branch's own regression — fixed in c10a2ed.

forget keeps an abandoned launch's entry so its queued create is accounted for. But
PendingTarget::wait drops its Registered on the failure path too, so a launch that pumped
its whole bound and saw nothing was kept as abandoned and would then take the next launch's stop —
the exact failure keeping an entry exists to prevent, reached from the other side.

Arrivals::discard forgets outright whatever the state, and wait_for_live_target calls it beside
retire_deferred_attachment on the one ending that says nothing is coming: pumped the whole bound,
still absent. Not on the interrupted branch, which says nothing about whether the process is
still on its way, and not on a pump that failed with a live session — the same narrowing
retire_deferred_attachment already carries, for the same reason.

One correction to the finding, which does not change the fix

It cites "a behavior this module still explicitly documents in wait_for_live_target" — a
CreateProcessWide accepted and the spawn failing later. That shape is not reachable: every way a
launch fails with nothing created lands on launch_process_begin's own ?
, measured this morning
at 0x80070002 in 1.5 ms for a missing image, before any guard exists. examples/abandoned_launch.rs
is that measurement, and it is what removed this issue's original deferral reason.

What is reachable is the ending you describe arriving by a different route — the bound expiring —
and that is enough to make the finding right. Same fix either way.

What is not pinned, said rather than implied

The rule is: ..._keeps_its_place_and_an_attach_does_not gained a block that fails if discard
keeps the entry. The call site is not. Pinning it needs a launch whose process never arrives
inside LIVE_WAIT_MS, and a create that reaches the queue produces a process — so backing the call
out leaves all 189 tests green. I checked that rather than assuming it, and the doc comment on
discard_registration records it. The attach side has
test_the_openers_prune_clears_a_claim_on_a_departed_process for the equivalent wiring only because
a pid that never joins is constructible where a launch is not.

Verification

cargo nextest run 189, cargo test --doc 4, fmt and clippy clean.

@glslang
glslang merged commit b259566 into main Sep 4, 2026
7 checks passed
@glslang
glslang deleted the fix/an-abandoned-launch-keeps-its-place branch September 4, 2026 15:31
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