What
A wrong-variable substitution can type-check on one major and fail only on
another. @jdatcmd substituted rel for grel while working on #1213: it
compiled on PG 18 and failed on PG 17. Nothing else in the pipeline can see
that — not review, not the suite (which runs on one major during feature work),
not the .so fingerprint, which only says the binary matches the source it was
built from.
A build-only preflight across all supported majors closes it, and it costs a
compile rather than a cluster.
What it costs, measured
On pgcolumnar-audit, PG 15a/16a/17a/18a/19a, make -j8, no install and no
cluster:
pg15a 5s pg16a 6s pg17a 6s
pg18a 2s pg19a 4s
TOTAL 23s
Twenty-three seconds to compile the tree against all five majors.
The obvious implementation is a trap, and I fell into it while measuring this
My first loop was make clean; PG_CONFIG=pg<N>a make per major. It recompiles —
35 objects each time, so the clean works — and it leaves the tree holding the
objects of the last major built. The very next suite run, for PG 18, died:
FATAL: could not load library ".../pg18a/lib/postgresql/pgcolumnar.so":
undefined symbol: build_simple_rel_hook
FATAL: no cluster of our own on port 27095 after 8 attempts
build_simple_rel_hook is the PG 19 name. The loop ended on pg19a, and the
suite inherited its objects.
Why the existing guard did not catch it
lib.sh already has one — "Objects from another major link but do not load
(#536)" — and it is defeated here:
pgc_build_needs_clean() {
...
[ "$have" = "$want" ] && echo no || echo yes
}
have is the build stamp: which major the harness last built. want is the
current major. Neither is a property of the objects in the tree. A hand-run
make leaves foreign objects and never touches the stamp, so have = 18 and
want = 18 agree while src/*.o are PG 19's, the clean is skipped, make finds
everything up to date, and PG 19 objects get installed into the PG 18 prefix.
The stamp fails closed in one direction and open in the other. A stale or
absent stamp produces a refusal — FATAL: the binary under test was not built from this source — which is safe and which I hit twice today. A stamp that
matches while the objects are foreign produces a silent wrong install. Only the
second direction is dangerous, and it is the one a cross-major preflight creates.
Shape of the work
- The preflight itself: compile against every major, report per major, no
install, no cluster. 23s.
- Leave the tree as it was found. The preflight must remove objects between
majors and rebuild the working major afterwards, or build each major in its
own directory so the shared tree is never touched.
- Close the guard's open direction, which is worth doing whether or not the
preflight lands: derive have from the objects rather than from a stamp the
harness writes. A .o's major is recoverable without trusting anyone's
bookkeeping, and any check that depends on a hand-written record is defeated by
a hand-run command.
Item 3 is the one with value beyond this feature: it is the reason a preflight
would be safe to run at all, and today it is the reason a plain make in the
wrong order silently breaks the next suite.
Raised with @jdatcmd, who offered to take the implementation; I have written it
up and am happy either way round.
What
A wrong-variable substitution can type-check on one major and fail only on
another. @jdatcmd substituted
relforgrelwhile working on #1213: itcompiled on PG 18 and failed on PG 17. Nothing else in the pipeline can see
that — not review, not the suite (which runs on one major during feature work),
not the
.sofingerprint, which only says the binary matches the source it wasbuilt from.
A build-only preflight across all supported majors closes it, and it costs a
compile rather than a cluster.
What it costs, measured
On
pgcolumnar-audit, PG 15a/16a/17a/18a/19a,make -j8, no install and nocluster:
Twenty-three seconds to compile the tree against all five majors.
The obvious implementation is a trap, and I fell into it while measuring this
My first loop was
make clean; PG_CONFIG=pg<N>a makeper major. It recompiles —35 objects each time, so the clean works — and it leaves the tree holding the
objects of the last major built. The very next suite run, for PG 18, died:
build_simple_rel_hookis the PG 19 name. The loop ended on pg19a, and thesuite inherited its objects.
Why the existing guard did not catch it
lib.shalready has one — "Objects from another major link but do not load(#536)" — and it is defeated here:
haveis the build stamp: which major the harness last built.wantis thecurrent major. Neither is a property of the objects in the tree. A hand-run
makeleaves foreign objects and never touches the stamp, sohave = 18andwant = 18agree whilesrc/*.oare PG 19's, the clean is skipped,makefindseverything up to date, and PG 19 objects get installed into the PG 18 prefix.
The stamp fails closed in one direction and open in the other. A stale or
absent stamp produces a refusal —
FATAL: the binary under test was not built from this source— which is safe and which I hit twice today. A stamp thatmatches while the objects are foreign produces a silent wrong install. Only the
second direction is dangerous, and it is the one a cross-major preflight creates.
Shape of the work
install, no cluster. 23s.
majors and rebuild the working major afterwards, or build each major in its
own directory so the shared tree is never touched.
preflight lands: derive
havefrom the objects rather than from a stamp theharness writes. A
.o's major is recoverable without trusting anyone'sbookkeeping, and any check that depends on a hand-written record is defeated by
a hand-run command.
Item 3 is the one with value beyond this feature: it is the reason a preflight
would be safe to run at all, and today it is the reason a plain
makein thewrong order silently breaks the next suite.
Raised with @jdatcmd, who offered to take the implementation; I have written it
up and am happy either way round.