fix: a suite must not report checks against the previously installed .so - #508
Conversation
pgc_setup ran the build and the install with no status check:
make -C "$PGC_SRCDIR" PG_CONFIG="$PGC_PG_CONFIG" >/dev/null
make -C "$PGC_SRCDIR" install PG_CONFIG="$PGC_PG_CONFIG" >/dev/null
lib.sh sets `set -uo pipefail` but not -e, so a failed compile did not stop the
suite. It carried on and ran every check against whatever .so was installed
last, then printed a full report for code that does not exist.
Reported by ChronicallyJD, who caught it only by fingerprinting the installed
.so across a source change and seeing the same hash twice.
Proven by removal, with `this is not valid C and cannot compile;` appended to
src/columnar_bloom.c. With the guard:
-- building
src/columnar_bloom.c:260:1: error: unknown type name 'this'
make: *** [<builtin>: src/columnar_bloom.o] Error 1
FATAL: the build failed, so there is nothing new to test
and no checks run. With the guard removed, the same broken tree:
PASS the filter still skips groups it can prove empty (#467)
PASS a column above the distinct cap is refused a filter, not given a bad one
...
checks run: 19
bloom_sizing.sh: PASSED
Nineteen green checks against a source file that cannot compile. That is worse
than a wrong answer, because it is indistinguishable from a right one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeNm2Gw6h16Z123We3F1vJ
ChronicallyJD
left a comment
There was a problem hiding this comment.
Approving. The removal proof is the part that should go in the commit message if
it is not already there — 19 green checks, four of them premise assertions,
against a source file containing this is not valid C and cannot compile; is a
better argument for this guard than any description of it. It is also the exact
shape I hit: my run reported 8 checks including the very check I had just written,
and the only thing that gave it away was the installed .so hash being identical
either side of a source change that could not have produced it.
I checked the obvious follow-on question and it comes out in your favour: the
matrix path was already covered. run_all_versions.sh status-checks both steps
at :559 and :580 and records FAIL PG$major (build) / (install), so
PGC_SKIP_BUILD suites were never at risk from their own build — the gap was
exactly and only the standalone path this patch fixes. Worth a line in the body,
because the natural reviewer worry is "the authoritative gate skips this block".
One thing to fix, small
The new exit 1 sits between the workdir being created and the trap that
removes it:
lib.sh:108 PGC_WORKDIR="$(mktemp -d /tmp/pgcolumnar-test.XXXXXX)"
lib.sh:125 echo "-- building" <-- new exit 1 here
lib.sh:127 echo "-- installing" <-- and here
lib.sh:140 trap pgc_teardown EXIT
So a build failure now leaks /tmp/pgcolumnar-test.XXXXXX every time. Before this
patch the same path ran on to the trap and cleaned up; the guard is right, but it
turns a compile error into an accumulating one. On a box where someone is
iterating on a change that does not compile — which is precisely when this fires —
that is one directory per attempt, and each holds a full data directory once it
gets further.
Cheapest fix is to move trap pgc_teardown EXIT above the build block. It is
already safe to run early: pgc_teardown keys off PGC_PGDATA and PGC_WORKDIR,
both set by then, and stopping a cluster that was never started is a no-op. If you
would rather not move it, rm -rf "$PGC_WORKDIR" before each exit 1 does the
same job less tidily.
Two things I checked and am happy with
exit 1and notexit 2. #499 established 1 for a hard failure and 2 for
too-few-samples; a build that did not happen is unambiguously the former.- Message on stderr, signal in the exit status. A caller that captures only
stdout still fails, because it fails on the status. That is the right way round —
the reverse (message on stdout, status 0) is the bug being fixed.
Beyond this PR
You are right that it is the third instance this week, and I would add a fourth
from today: the .so fingerprint is what caught this one, and nothing in the
harness prints it. Every suite already knows $($PGC_PG_CONFIG --pkglibdir), so
one line in pgc_setup — echo "-- .so: $(md5sum ...)" — would put the hash in
every log, and a red-on-change proof would then carry its own evidence that the
two arms were different binaries. I have been doing it by hand all day and it has
paid for itself twice. Happy to send that as a follow-up rather than pile it here.
The exit added by the previous commit sat between `mktemp -d` at :108 and
`trap pgc_teardown EXIT`, so a failed build left /tmp/pgcolumnar-test.XXXXXX
behind every time -- and it fires exactly when someone is iterating on code that
does not compile, so they accumulate. Before that commit the path ran on to the
trap. Caught in review by ChronicallyJD.
The trap now arms as soon as there is a directory to remove. Stopping a cluster
that never started is a no-op, so arming it early costs nothing.
The privilege block moved above it rather than the trap alone moving up, which
is the part the review did not have to notice: pgc_teardown reaches pg_ctl
through pgc_pg, which expands PGC_RUNPG, and that array was set AFTER the build.
A trap armed before it would have turned any early failure into an
unbound-variable error under `set -u` instead of a cleanup. PGC_PGDATA was
already set at :109 and needed no move.
Proven by removal, broken source, counting /tmp/pgcolumnar-test.*:
trap above the build (this commit) exit 1, workdirs left: 0
trap below the build (as shipped) exit 1, workdirs left: 1
And the passing path is unchanged: a clean run of bloom_sizing.sh reports 19
checks, PASSED, leaves 0 workdirs and 0 leftover clusters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeNm2Gw6h16Z123We3F1vJ
|
Both points taken. Pushed as The workdir leak is real and is fixedVerified before fixing:
And the passing path is unchanged — a clean One thing the fix needed that the review did not have to catch. Moving the trap alone would have been wrong: The matrix path was already coveredConfirmed rather than taken on trust — On the
|
) Caught by the five-major gate, on all five majors: pushdown_report and analyze_stats both failed, and both for the same reason. ExecScan increments nfiltered1 for every tuple ITS qual rejects, and that is what EXPLAIN prints as "Rows Removed by Filter". Filtering inside the scan means ExecScan never sees a rejected row, so the line silently read 0 on every columnar scan carrying a qual. The plan still looked right; a counter two other suites depend on had stopped counting. FAIL pushdown off examines every row: got [unset] want [199999] FAIL the executor filters far more rows with pushdown off: got [no] want [yes] FAIL having an index available saves the point lookup real work: got [no (0 with the index, 0 without)] want [yes] The filter now counts the rejection where the executor would have counted it, and the late-materialization suite pins it next to the feature that broke it, so this cannot regress silently a second time. Worth recording how nearly this was missed twice. The first verification of the fix ran with PGC_SKIP_BUILD=1 after a manual `make` with no `make install`, so all three suites ran against the PREVIOUSLY INSTALLED .so -- which happened to be the guard-removed binary left by an earlier removal proof. It reported the volatility guard broken as well, which is what made it obvious. Fingerprinted: 3fc733b34589 stale, guard-removed 3 suites failing a411738500db actually installed 14 / 34 / 33 checks, 0 fails That is #508's defect class wearing a different costume: there the build failed silently, here the install was never asked for. Refs #452 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeNm2Gw6h16Z123We3F1vJ
… skips
Three separate defects in one session were a suite reporting checks against a
binary nobody had just built:
- a compile error the harness did not check, which reported 19 green checks
against a source file containing invalid C (#508);
- a PGC_SKIP_BUILD=1 run that skipped the INSTALL, not just the build, and
exercised a guard-removed leftover from an earlier removal proof -- three
suites failing for a reason that had nothing to do with the change;
- objects from one major linked into another's .so, which surfaces as a
cluster that will not start behind a message naming nothing.
Every one produced a plausible PASS/FAIL list. Every one is one line of md5sum
away from being obvious. So pgc_setup now prints that line on every run, whether
or not anything looks wrong -- the point is precisely that nothing does.
It also makes a red-on-change proof self-evidencing. Two arms reporting the same
hash have proved nothing whatever their check counts say, and that is currently
something each person has to remember to verify by hand.
PGC_SKIP_BUILD is named in the same change because the variable is not what it
says: it reads as "skip the build" and means "skip the build AND the install, and
test whatever is already installed". That is correct for the matrix, which
installs once per major before setting it, and a trap for a person who has just
edited source and run make by hand -- which is exactly how the second defect
above happened.
harness_selftest gains two checks. The first is that the line exists. The second
is the one with teeth: it compares what is INSTALLED against what was just BUILT,
using the build tree as an independent source rather than recomputing the
installed hash the same way twice.
Proved by reproducing the original incident rather than by deleting the check:
build a genuinely different binary, do not install it, run with
PGC_SKIP_BUILD=1.
installed 8b58d7fdcb0d, built 7564d4f138ed -> FAIL, as it should
The first attempt at that proof appended a comment to a source file and rebuilt.
The binary was byte-identical, both hashes matched, and the check passed --
which the fingerprint line itself is what revealed. A perturbation that does not
perturb is not a proof, and this one says so out loud.
Refs #508
test: print the .so under test, and name what PGC_SKIP_BUILD actually skips (#508 follow-up)
Reported by @ChronicallyJD, who caught it the only way it can be caught from the
outside: by fingerprinting the installed
.soacross a source change and seeingthe same hash twice.
The defect
pgc_setupran both make steps with no status check:lib.shsetsset -uo pipefailbut not-e, so a failed compile does notstop the suite. It carries straight on and runs every check against whatever
.sowas installed last, then prints a full report for code that does not exist.Scope: the matrix was never at risk, only the standalone path
The obvious reviewer objection, checked rather than assumed:
run_all_versions.shstatus-checks both steps and recordsFAIL PG$major (build)/(install), and separately fails the major on compilerwarnings. Suites run under
PGC_SKIP_BUILDtherefore never built at all andcould not hit this. The gap was exactly and only the standalone
test/<suite>.shpath — which is what a person runs while iterating, i.e. thecase where the source most often does not compile.
Proof by removal
this is not valid C and cannot compile;appended tosrc/columnar_bloom.c.With the guard:
No checks run.
With the guard removed, same broken tree:
Nineteen green checks, including four premise assertions, against a source file
that cannot compile. That is worse than a wrong answer because it is
indistinguishable from a right one — and it is green, so nobody looks.
Second commit: the trap had to move too
Review caught that the new
exit 1sat betweenmktemp -dandtrap pgc_teardown EXIT, leaking a workdir on every failed build — preciselywhen someone is iterating on code that does not compile. Fixed, proven by
removal:
Moving the trap alone would have been wrong:
pgc_teardownreachespg_ctlthrough
pgc_pg, which expandsPGC_RUNPG, set after the build. A trap armedabove it with the array unset turns an early failure into an unbound-variable
error under
set -u. The privilege block moved up with it.Passing path unchanged: clean
bloom_sizing.shgives 19 checks, PASSED, 0workdirs, 0 leftover clusters.
Class
Third instance of the same shape found this week: a step that fails silently and
a report that speaks anyway. #506 is the ClickBench reporter printing the
cold-cache tag on hosts where the drop was refused; #503 lists four sites where a
cost estimate is re-derived and one copy stops moving. The common repair is that
the reporting step must not be reachable when the step it reports on did not
happen.