Uh oh!
There was an error while loading. Please reload this page.
fix(devx): reserve the port sdui_pick_free_port hands out, instead of probing and letting go - #10217
Merged
Merged
Conversation
… probing and letting go `sdui_pick_free_port` bound a probe socket, closed it, and only then reported the port free; the caller bound it afterwards. The scan is deterministic from `base` upward, so concurrent callers did not diverge — they were handed the same port, and the first one every time. Measured before this change, eight concurrent callers scanning from 5180: DISTINCT_PORTS=1 of 8, and six of eight lost the follow-up bind with `EADDRINUSE 127.0.0.1:5180` — the signature that dequeued a PR from the merge queue. A port is now claimed in a host-shared registry (`O_EXCL`, swept of claims whose owner process is gone) before it is probed, and the claim outlives the function. Two cooperating callers can no longer be handed one port. The probe stays for processes outside the registry, and a lost probe hands the claim back rather than hoarding it. The collision test gains the vacuity guard it was missing — BUSY_HELD, so an occupier that lost its own bind can no longer read as a picker that ignores busy ports — plus the concurrency and foreign-steal assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…dying on it Both raw listeners in the harness bound with no `error` handler, so a lost bind killed node with an unhandled `error` event and took the harness's exit code with it. That is why the merge-queue report was a stack trace (`EADDRINUSE 127.0.0.1:5180` under `runHarness`) rather than a named precondition — reproduced here on the pre-fix script before this change. They now exit, so the vacuity guards (BUSY_HELD, STEAL_HELD) get to speak. STEAL_CLAIM_ON_PICK joins STEAL_CLAIM_RELEASED as its positive control: "no claim file for that port" is also what a registry that does not exist looks like, so the release assertion alone was measured green against the pre-fix script it was written to fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
os-zhuang
marked this pull request as ready for review
August 20, 2026 14:23
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang pushed a commit
that referenced
this pull request
Aug 20, 2026
…f probing and letting go `smoke_pick_free_port` bound a probe socket, closed it, and only then reported the port free; `objectstack dev` bound it afterwards. The scan walks `base` upward deterministically, so concurrent callers were handed the same port — the first one, every time. Ports are now CLAIMED before they are probed, in a host-shared registry, and the claim outlives the function. Ports the same design PR #10217 landed for `sdui_pick_free_port` (#10167), keeping this script's wildcard probe, which matches serve.ts isPortAvailable(). Part of #10212 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#10167
What was wrong, re-derived rather than taken from the card
sdui_pick_free_portbound a probe socket, closed it, and only then reported theport free; the caller bound it afterwards. The scan walks
baseupwarddeterministically, so concurrent callers did not diverge — they were handed the same
port, and the first one every time. Check-then-use, with the "use" in another process.
Measured on this tree before the change, eight concurrent callers from base
5180:After the change, the same construction:
One correction to the card's reading. The card explains the CI trace as the picker
handing two callers
5180. The trace also carriedEADDRINUSE 127.0.0.1:5180from anunhandled
errorevent, and reproducing it here pins a more specific chain: thecollision test's own occupier lost its bind to a concurrent scanner and died, so the
following pick returned the occupied number legitimately — the port really was free
by then. The read-out (
BUSY_PORT == PICKED_WITH_BUSY) then accused the picker of notskipping a busy port, which is not what happened. Same root cause, different victim:
the harness was one of the TOCTOU's casualties, not its witness. Both halves are fixed
below.
The fix
A port is claimed before it is probed, in a registry every caller on the host
shares (
${TMPDIR:-/tmp}/sdui-port-reservations, overridable viaSDUI_PORT_RESERVATION_DIR), and the claim outlives the function — it is releasedwhen the claiming process dies, not when the function returns.
O_EXCLfile creation is the mutual exclusion: exactly one caller wins a port, andthe losers are told so there instead of discovering it at bind time in another
process.
written moments ago) and a ceiling (a backstop for a dead owner whose pid number was
reused).
flockwraps the scan where available. It is not what makes a claim exclusive —O_EXCLalready is — it makes the sweep safe, the one step that unlinks a fileanother scanner may be creating. Missing
flockdegrades to "sound, minus thesweep's tie-break", never to a hard failure.
When the port is stolen anyway
Named plainly, because the reservation binds only callers that share the registry:
and it is every collision anyone has actually measured here.
the picker hands the claim back and advances. This retry is asserted, not
argued (
STEAL_*below), because a hoarded claim would cost a port on every laterrun in the container. Measured: foreign listener on
5180⇒PICKED=5181,LEFTOVER_CLAIM_ON_5180=no,CLAIM_ON_PICKED=yes.that, and this PR does not change what happens:
--strictPortplussdui_wait_for_own_serverstill turn it into a loud failure whose remedy is to pina free port with
SDUI_DUMP_PORT. Converting that refusal into a vite respawn isa change to a contract the collision test deliberately pins, and belongs to its own
decision rather than to this one.
Tests
gen-sdui-manifest-collision.test.tsgains the vacuity guard it was missing, plus therace as an executed assertion:
BUSY_HELD— the occupier must really hold the port before "the picker skipped it"means anything. Its absence is why this file accused the wrong function.
CONCURRENT_TOTAL/CONCURRENT_DISTINCT— eight subshells, one base, at once.BUSY_PORT/NPORT/OPORTdistinct — two picks in one run cannot return one port.STEAL_HELD/STEAL_PICK/STEAL_CLAIM_RELEASED, withSTEAL_CLAIM_ON_PICKasits positive control — "no claim file for that port" is also what a registry that
does not exist looks like, and the release assertion alone was measured green
against the pre-fix script it was written to fail.
Both raw listeners in the harness also stopped throwing on a lost bind. An unhandled
errorevent killed the harness mid-measurement and buried the result under a stacktrace — which is exactly what the CI report was.
Ablation (fix reverted on disk, tests kept)
Mutation confirmed on disk before running, by grepping the text it removes and the text
it restores:
sdui_scan_and_reserve_port=0,ADVISORY ONLY. It reserves nothing=1.No rebuild is involved — these tests read
scripts/gen-sdui-manifest.shfrom theworktree at run time, so there is no
dist/to go stale. Restoration confirmed the sameway (
=3/=0, cleangit statusagainst HEAD).The fourth red was not planted.
gen-sdui-manifest-write-target.test.tsdrives the realscript, and on the unfixed picker it lost
5180to the collision test running besideit — the cross-file contention the card predicted, reproduced live rather than argued.
An earlier ablation round, before the harness was hardened, reproduced the merge-queue
signature verbatim:
Error: Command failed: bash /tmp/sdui-collision-*/harness.sh/Error: listen EADDRINUSE: address already in use 127.0.0.1:5180underrunHarness.Note that
PICKED_WITH_BUSYcame back 5181 in every ablation round — the originalassertion stayed green on the broken picker. That is the card's point about a green run
not being the acceptance criterion, measured.
Green, at
7d865c1eAll three files together (
--maxWorkers=3, so the contention is real and not staged):Gates at
7d865c1eUnion derived by
node scripts/pm/dispatch-gates.mjswith no path args (2 paths, mergebase
da891e0e). All green:check:cross-package-test-inputs·check-cross-package-test-inputs.mjs·spec check:empty-state·spec check:liveness·check:merge-driver·check:slot-lookup·spec check:strictness-ledger·check:type-source-resolution·spec check:variant-docs·docs-audit/check-affected-docs.mjs·check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:type-check-coverage·check:nul-bytesDeclared narrowing — two gates NOT MEASURED, and that is not a green.
check-dev-prereqs.mjsand thecheck:type-check-debt --re-measureratchet both refuseon an unbuilt worktree, and
check-dev-prereqshere reports67 of 67 workspace packagesmissing their
dist/entry point — a property of the worktree, not of a diff whose twofiles are a root shell script and one test file that imports nothing from the workspace.
Building the full closure would have held the container's shared verify lock against
three concurrently active agents (queue waits were already 5m21s during this run), which
is the same shared-resource harm this card is about. The risk the ratchet covers for this
diff is that the new TS does not typecheck or grows the debt, and that was measured
directly instead:
CI runs the whole farm regardless; this is the cheap half, not a substitute for it.
No changeset
Root
scripts/plus onepackages/spec/scripts/*.test.ts. Nothing published changesbehaviour, so this carries
skip-changeset.Not addressed here
smoke_pick_free_portinscripts/publish-smoke.shis the same defect, byte for bytein shape, drawing from base
3210. It sits outside this card's declared file surface(as does its own test file), so it is filed separately rather than fixed as a rider.
Filed as issue 10212 (referenced without a closing keyword on purpose).
Generated by Claude Code