Skip to content

test: the port walk wraps instead of walking off the ceiling (#548) - #549

Merged
ChronicallyJD merged 3 commits into
mainfrom
fix/548-port-walk-wrap
Aug 10, 2026
Merged

test: the port walk wraps instead of walking off the ceiling (#548)#549
ChronicallyJD merged 3 commits into
mainfrom
fix/548-port-walk-wrap

Conversation

@ChronicallyJD

Copy link
Copy Markdown
Collaborator

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:

base at HI-1 (31767):  SB=31767, RS increments to 31768, hits the bound
                       -> FAIL no free port for the restore, 1,999 ports free
base at HI-2:          ok
mid band:              ok

pick_sb_port seeds base from $$. Both draws return the same port by
construction — $$ inside $( ) is the invoking shell's PID, 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

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_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.

The tests were vacuous first, and my own removal proof caught it

harness_selftest 54 → 61.

The first version PASSED with the no-wrap walk restored — 60 of 60 against the
defect. Both reasons were mine:

  • I asked the picker for a port with the band empty, where the old code also
    succeeds. A truncated scan only fails when the top of the band is busy.
  • The "wraps to the floor" check computed the wrap from PGC_AUX_PORT_LO and
    HI directly without calling the picker at all — 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 fails by name:

FAIL  a seed at the ceiling wraps past a busy top and still finds a port
FAIL  and the port it found is below the busy region, which is where wrapping lands

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 portlib comment in C style out of habit and bash -n passed
it
. The * lines are globs and */ is a word, so it parses cleanly and would
have 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_gist absent from this container, identical on unmodified main. Gated on
the full set rather than on units because portlib is in the path of every suite.

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
@ChronicallyJD

Copy link
Copy Markdown
Collaborator Author

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 intent

p=$(( lo + ((base - lo + i) % width) ))

wraps properly for any base in [lo, hi), and span=300; [ "$width" -lt "$span" ] && span=$width stops a narrow band from scanning past itself. The width <= 0 guard makes a degenerate band return rather than divide by zero.

pick_sb_port delegating instead of carrying a second copy of the walk is the part that matters most, and more than the wrap: the duplicate is why there were two defects to fix. Two copies of a walk is how one of them stays broken.

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

pgc_pick_free_port is bounded at 300, not at width. On the 2000-port aux band it probes 15% and returns failure — and its caller then says:

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:

  1. Scan the width. Consistent with the walk, and the caller's message becomes true as written. Costs up to 2000 TCP probes on a genuinely full band, which only happens when something is badly wrong anyway.
  2. Keep 300 and make the message honest — have the picker report how many it probed, so the caller can say "no free port in 300 probes from N" rather than naming a band it never swept.

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 checks

Your 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 pgc_port_free so the top 400 are busy is the only condition under which a no-wrap walk can be seen to fail, and asserting the real prober was restored afterwards closes the hole where a leftover stub makes every later check lie.

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 blocking

The 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
@ChronicallyJD

Copy link
Copy Markdown
Collaborator Author

Finding closed. Verified at 5d1bbe4 by running it, including a removal proof of my own finding rather than reading the diff.

The picker sweeps the band now — span=$width — and the caller messages say "the picker swept the whole band", which is true only because it now does. That was the whole finding: the message asserted a sweep that had not happened.

Removal proof, re-imposing the old 300-probe budget on the current tree:

FAIL  a free port beyond the old 300-probe bound is still found: got [NONE] want [30268]

Exactly the check that pins it, and nothing else. Baseline on the same tree, run without PGC_SKIP_BUILD so the build path actually runs: harness_selftest 63 PASSED, replication 41 PASSED.

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.
@ChronicallyJD
ChronicallyJD merged commit d19c2e4 into main Aug 10, 2026
11 checks passed
ChronicallyJD added a commit to ChronicallyJD/pgcolumnar that referenced this pull request Aug 10, 2026
…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.
ChronicallyJD added a commit that referenced this pull request Aug 10, 2026
…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'.
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.

The aux port walk cannot wrap, so a draw at the top of the band fails with 1,999 free ports under it — not exhaustion, and resizing will not fix it

2 participants