Skip to content

test(credentials): stub creation and child spawning stop overlapping - #176

Merged
tobert merged 1 commit into
mainfrom
test/credentials-etxtbsy
Sep 3, 2026
Merged

test(credentials): stub creation and child spawning stop overlapping#176
tobert merged 1 commit into
mainfrom
test/credentials-etxtbsy

Conversation

@tobert

@tobert tobert commented Sep 3, 2026

Copy link
Copy Markdown
Owner

The credentials suite failed CI on #174 and had been flaking for weeks. Amy's call was to
fix it ahead of the cosmetic pre-release items — "a merge gate that fails a fifth of the
time is one we will start ignoring"
— so this goes first in the pre-release queue.

Measure before asserting

Our own note called this a rare race. It is not:

parallel serial
the #174 branch 6 / 20 failed 0 / 10
main @ f741a84, unmodified 3 / 15 failed

A fifth to a third of runs. That number is what moved this from "known flake, re-run it"
to "fix it now."

What it actually is

ETXTBSY. Writing a stub holds a write fd on it, and fork hands every open fd to the
child; a sibling test forking in that window gives its child a duplicate, which it holds
until it reaches exec. An exec of that stub in the meantime is "Text file busy".
O_CLOEXEC does not save it — the fd closes at exec, and the entire window is before
it. Nothing to do with the credential code: the harness owns this one.

Three decisions

Fix the race, not the symptom. Retrying on ETXTBSY would go green while leaving a
real fork/exec race in the tree, and a retry loop around a race teaches the next reader
that the race is acceptable. This removes the overlap instead.

A read/write lock, not a mutex — the two operations are not symmetric. Creation takes
the exclusive side (brief, and it must exclude every spawn); spawning takes the shared
side, so resolves still run concurrently with each other. That matters for one test
specifically: a_blocking_key_resolve_does_not_stall_a_sibling_task exists to pin a
concurrency property, and a coarse mutex would have quietly serialized it into a
tautology that passes without testing anything.

Every resolve goes through the helper, including the ones whose command is not a stub
— a spawn that fails at exec has already forked, so it is in the race too.

Poisoning is stepped over deliberately: otherwise one test's panicking assertion fails
every later test with a lock error instead of its own message.

The number, and the control that makes it mean something

0 of 60 parallel runs fail with the fix.

A zero is only worth as much as its control, so: remove only the two lock-acquisition
lines, keep the helper and every other change, and it goes back to 11 of 20 failing.
The lock is doing the work, and the instrument still detects the race it claims to have
closed.

Full suite 1328 passed / 0 failed. cargo clippy --all-targets clean.

No changelog entry

The test harness is not a user-facing surface, and the house rule is that the git log is
the record for an internal fix.

🤖 Generated with Claude Code

The credentials suite failed CI on #174 and had been flaking for weeks. Amy's
call was to fix it before the cosmetic pre-release items, on the argument that a
merge gate failing a fifth of the time is one we will start ignoring.

So the first decision was to measure rather than assert. The prior note called
this a rare race; it is not — 6 of 20 parallel runs on the branch, 3 of 15 on
unmodified `main`, 0 of 10 serial.

The race is `ETXTBSY`. Writing a stub holds a write fd on it, and `fork` hands
every open fd to the child; a sibling test forking in that window gives its child
a duplicate, held until the child reaches `exec`. An `exec` of that stub meanwhile
is "Text file busy". `O_CLOEXEC` does not save it — the fd closes *at* exec, and
the whole window is before it. Nothing to do with the credential code; the
harness owns this one.

Second decision: fix the race, not the symptom. Retrying on `ETXTBSY` would go
green while leaving a real fork/exec race in the tree, and a loop around a race
teaches the next reader that the race is acceptable. So the fix removes the
overlap instead.

Third, a read/write lock rather than a mutex, because the two operations are not
symmetric. Creation takes the exclusive side; spawning takes the shared side, so
resolves still run concurrently and `a_blocking_key_resolve_does_not_stall_a_sibling_task`
keeps testing the concurrency property it was written for rather than being
quietly serialized into a tautology. Every resolve in the file goes through the
helper, including the ones whose command is not a stub — a spawn that fails at
`exec` has already forked, so it is in the race too.

Poisoning is stepped over on purpose: otherwise one test's panicking assertion
fails every later test with a lock error instead of its own message.

Measured after: 0 of 60 parallel runs fail. The control that makes that number
mean something — remove only the two lock lines and keep everything else — puts
it back to 11 of 20.

No changelog entry: the harness is not a user-facing surface, and the git log is
the record for an internal fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tobert
tobert merged commit 1d00857 into main Sep 3, 2026
1 check passed
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