test: the port walk wraps instead of walking off the ceiling (#548) - #549
Conversation
Analysis and issue by @ChronicallyJD, reproduced here with a forced PID before anything was changed, because this is shared harness code and the arithmetic alone is not a demonstration. Reproduced. Band [29768,31768), width 2000, NOTHING listening: base at HI-1 (31767): SB=31767, RS increments to 31768, hits the bound -> FAIL no free port for the restore, 1,999 free base at HI-2: ok mid band: ok pick_sb_port seeds base from $$ over the band. Both draws return the same port by construction, since $$ inside $( ) is the invoking shell's PID and not the subshell's, which is why the loop opens by testing for that collision. It then incremented and hard-failed at PGC_AUX_PORT_HI instead of wrapping to LO. About 1 replication run in 2000; across five majors roughly 1 CI run in 400, which matches the observed rare, unattributable red that moved between majors. It took down #545's PG17 leg on a diff containing nothing but CONTEXT.md. Not a sizing problem, so the band is untouched. The failure needs a base within one port of the ceiling, and that stays a fixed fraction of the width whatever the width is; a 20,000-port band fails identically, just less often, and still with the whole band free beneath it. Both halves of the fix. The walk wraps, and it is BOUNDED by the band width so a genuinely full band reports itself full rather than spinning forever. The message now distinguishes sweeping the whole band from walking off the end of it, because those want different responses from whoever reads the log, and the old one asserted the first when only the second had been established. That is #537's defect in a different file. pick_sb_port was a second copy of pgc_pick_free_port carrying the same bug. It now delegates. Two copies of a walk is how one of them gets fixed. harness_selftest 54 checks to 61. The first version of those checks PASSED with the no-wrap walk restored, 60 of 60 against the defect, and both reasons were mine. I asked the picker for a port with the band EMPTY, where the old code also succeeds, because a truncated scan only fails when the top of the band is busy. And the "wraps to the floor" check computed the wrap from PGC_AUX_PORT_LO and HI directly without calling the picker at all, which is a tautology over my own expression. Rewritten to stub pgc_port_free so the top 400 are busy and the rest free. A walk that stops at hi finds nothing from a base in that region; a walk that wraps lands below it. Proved by removal: restoring the old walk now fails "a seed at the ceiling wraps past a busy top and still finds a port" by name. A full-band stub proves termination, and a premise asserts the real prober was restored, since a stub left installed would make every later check lie. Gate: PG17 assert 132 ran PASS; PG19 assert 137 ran with only temporal, which is btree_gist absent from this container and fails identically on unmodified main. Gated on the full set rather than on units because portlib is in the path of every suite. Closes #548
|
I filed #548, so this is a review and not a self-approval. The wrap is correct, the deduplication is the best part of it, and there is one finding: this PR fixes the message-honesty defect in the walk it rewrote and leaves the identical defect in the picker it rewrote, in the same file, in the same commit. Correct, and I checked the arithmetic rather than the intentp=$(( lo + ((base - lo + i) % width) ))wraps properly for any base in
The replication walk is exactly what #548 asked for, both halves: _rs_width=$(( PGC_AUX_PORT_HI - PGC_AUX_PORT_LO ))
...
if [ "$_rs_tries" -ge "$_rs_width" ]; then
echo "FAIL ... swept all $_rs_width ports in [...] and every one was busy or the standby's"Bounded by the band width, and the message states what was actually established. That is the right standard. The finding: the picker is held to a different one
echo "FAIL no free port for the restore cluster in [$PGC_AUX_PORT_LO,$PGC_AUX_PORT_HI)"That message names the whole band on the strength of 300 probes. It is the same sentence the walk fifteen lines below it was just corrected for: asserting a sweep that did not happen. The comment above the bound argues for a bound — "without it a genuinely full band spins forever rather than reporting itself full" — and I agree. It does not argue for 300 over width, and the PR chooses width in one place and 300 in the other without saying why they differ. Both resolutions are fine and they are not the same:
I would take (1) for the aux band, because the expensive case is the one that already indicates trouble, and because one rule is easier to keep than two. But (2) is defensible and cheaper; what is not defensible is the current pairing, where the honest message sits next to the one this PR was filed to fix. On the checksYour removal proof is the right shape and the stub is the reason it works — the defect is unobservable with the band empty, because the old code succeeds there too, which is why your first version passed against it. Stubbing I have not re-run them; the reasoning is checkable from the diff and you reproduced the original defect by forcing the PID, which is more than I did when I filed it. Not blockingThe flake is fixed either way — with the wrap in place, failure now needs 300 consecutive busy ports rather than one unlucky seed. The finding is about what the failure will say when it happens, which is the thing #537 exists to protect and which this PR otherwise upholds carefully. I have not merged this and will not. |
…w-up) Found by @ChronicallyJD reviewing #549: the picker was bounded at 300 probes while its caller reported "no free port for the restore cluster in [lo,hi)", naming 2000 ports on the strength of 300. That is #548's own defect surviving inside #548's fix. The original walk gave up at the ceiling with the band free; this gave up at 300 with 1,700 free. And it is #537's defect in the message: asserting a sweep the code had not performed. The bound is now the whole band. Measured before choosing, rather than assuming a full sweep was too expensive: 2000 probes take 856 ms against 191 ms for 300, on the bench container, and neither is paid on the success path because the picker returns on the first free port. A full sweep happens only when the band really is full, which is the failure path, where 856 ms is not a cost anyone is counting. Both caller messages now say the picker swept the whole band, which is true only because it now does. Test first, and it is the discriminating one rather than another wrap check: one free port stubbed 500 past the floor, further than the old bound, seeded at the floor. A 300-probe walk reports NONE; a band-wide walk finds it. before the fix FAIL a free port beyond the old 300-probe bound is still found: got [NONE] want [30268] after 63 checks, PASSED Proved by removal both ways: restoring the 300 budget fails that check by name, and removing the wrap fails two others, so the two properties are pinned separately rather than by one check that happens to cover both. Refs #548
|
Finding closed. Verified at The picker sweeps the band now — Removal proof, re-imposing the old 300-probe budget on the current tree: Exactly the check that pins it, and nothing else. Baseline on the same tree, run without Measuring 191 ms against 856 ms before choosing the full sweep is better than the reasoning I offered when I raised it — I asserted the cost was acceptable without checking, and the argument that settles it is yours: a full sweep is only ever paid on the failure path, because the picker returns on the first free port. The discriminating test is the part I would have got wrong. A second wrap check passes on the old 300-bound code; one free port stubbed 500 past the floor separates the two properties, so the wrap and the bound are pinned independently rather than by one check that happens to cover both. No objections. I have not merged this and will not. |
#544 landed on main and both changes append checks to test/harness_selftest.sh at the same place, so they conflicted. That is #554 exactly -- the file has become the SUITES line, and this is the first instance of it costing a resolution rather than being a prediction. Resolved by keeping both blocks: main's four #537 blocks first, since they landed first, then the #548 port-walk block. No semantic conflict, only adjacency; the two sets of checks are independent. 87 checks, PASSED. A merge resolution is a change, so it gets the same treatment: proved that neither side lost its teeth in the resolution, rather than trusting that the text looked right. restore the 300-probe budget (mine) -> FAIL a free port beyond the old 300-probe bound is still found delete the pgc_start_log_report call (main's, from #544) -> FAIL the failure path asks pgc_start_log_report for the reason Both removal proofs still fire, so the union is the union and not one side wearing the other's line count.
…dprompt#536) Rebased onto d19c2e4 after commandprompt#544, commandprompt#547 and commandprompt#549 landed. Rebuilt from main rather than resolved: the union resolution of an earlier rebase produced a lib.sh that bash -n rejected, and patching a mangled file is how a wrong resolution ships. Fixes a defect found in review by the other session, not by any check: the stamp was written with printf '%s\\n', which emits the four bytes 1 9 \ n. It worked only because the reader does tr -dc '0-9' and strips the junk; a direct comparison against 19 failed. That is the third over-escaping in this PR -- the message printf was the second -- and bash -n accepts all of them, because they are syntactically valid and semantically wrong. Two checks added for it: the stamp must read back as the bare major, and lib.sh must not contain the doubled-backslash writer.
…nd (#554) Three PRs on 2026-08-09 each appended a block to the end of this file and every pair conflicted, while the one that edited the middle merged clean: #544 -> #549 CONFLICT #553 -> #544 clean #544 -> #551 CONFLICT #553 -> #549 clean #549 -> #551 CONFLICT #553 -> #551 clean That is row two of this file's own measurement about SUITES, happening in the file that argues it. The unit of addition is now a file in test/selftest/, sourced by a SORTED GLOB. Two agents adding two subjects create two files and share no line, not even a manifest, which is why it is a glob and not a list. THE SPLIT WAS BROKEN IN A WAY NO STATIC CHECK COULD SEE. Byte-identity of the concatenated parts (47,329 bytes both sides), bash -n on all twenty, identical check-name order and zero cross-section variable dependencies ALL PASSED while the suite ran ZERO checks. ${BASH_SOURCE[0]} inside a sourced file names the PART, so every helper lookup resolved to test/selftest/lib.sh, check() was never defined, and nothing ran. The directory is resolved once now as PGC_TESTDIR. Byte-identity proves the text is the same. It cannot prove the text still MEANS the same thing in another file. Only running it showed that. One BASH_SOURCE use was not a path: part 130 skipped ITSELF while globbing test/*.sh so as not to match its own search pattern. Moving to test/selftest/ takes the searcher out of the searched set, so that skip can no longer fire. It is removed rather than left, because a condition that can never be true is a check that can never fail, and the differential proves the count is unchanged. Gate, a differential rather than a pass: BEFORE 104 checks, 0 fails AFTER 107 checks, 0 fails all 104 pre-existing checks identical in result AND order Removal proof: appending a check to the driver reddens 'the driver holds no checks; they all live in parts'.
Closes #548. Analysis and issue by @ChronicallyJD. Reproduced here with a
forced PID before anything was changed, because this is shared harness code and
arithmetic alone is not a demonstration. I am not merging it.
Reproduced
Band
[29768,31768), width 2000, nothing listening:pick_sb_portseedsbasefrom$$. Both draws return the same port byconstruction —
$$inside$( )is the invoking shell's PID, not thesubshell's — which is why the loop opens by testing for that collision. It then
incremented and hard-failed at
PGC_AUX_PORT_HIinstead of wrapping toLO.About 1 replication run in 2000; across five majors roughly 1 CI run in 400,
which matches the observed rare, unattributable red that moved between majors.
It took down #545's PG17 leg on a diff containing nothing but
CONTEXT.md.Not a sizing problem, so the band is untouched
The failure needs a base within one port of the ceiling, and that stays a fixed
fraction of the width whatever the width is. A 20,000-port band fails
identically, just less often, and still with the whole band free beneath it.
Both halves
The walk wraps, and it is bounded by the band width so a genuinely full
band reports itself full rather than spinning forever. The message now
distinguishes sweeping the whole band from walking off the end of it — those want
different responses from whoever reads the log, and the old one asserted the
first when only the second had been established. That is #537's defect in a
different file.
pick_sb_portwas a second copy ofpgc_pick_free_portcarrying the same bug.It now delegates. Two copies of a walk is how one of them gets fixed.
The tests were vacuous first, and my own removal proof caught it
harness_selftest54 → 61.The first version PASSED with the no-wrap walk restored — 60 of 60 against the
defect. Both reasons were mine:
succeeds. A truncated scan only fails when the top of the band is busy.
PGC_AUX_PORT_LOandHIdirectly without calling the picker at all — a tautology over my ownexpression.
Rewritten to stub
pgc_port_freeso the top 400 are busy and the rest free. Awalk that stops at
hifinds nothing from a base in that region; a walk thatwraps lands below it.
Proved by removal — restoring the old walk fails by name:
A full-band stub proves termination rather than a hang, and a premise asserts the
real prober was restored, since a stub left installed would make every later
check lie.
One that nearly shipped
I wrote the
portlibcomment in C style out of habit andbash -npassedit. The
*lines are globs and*/is a word, so it parses cleanly and wouldhave executed as commands inside the function at runtime, breaking port selection
outright. A syntax check cannot see that a comment is not a comment. Caught by
reading the rendered function rather than trusting the parse.
Gate
PG17 assert 132 ran, PASS. PG19 assert 137 ran, one failure —
temporal,btree_gistabsent from this container, identical on unmodified main. Gated onthe full set rather than on units because
portlibis in the path of every suite.