Skip to content

U8 backup accessors + the Java version break, G13 measured, D0 validated, B1 resolved, and the mechanical five - #209

Merged
gburd merged 7 commits into
masterfrom
fix/u8-u9-backup-csharp
Oct 7, 2026
Merged

gburd merged 7 commits into
masterfrom
fix/u8-u9-backup-csharp

Conversation

@gburd

@gburd gburd commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Everything you asked for, except C# — which turned out to need a decision rather than code (below).

U8 — the four backup accessors were write-only

Added, as asked. But they weren't missing — setBackupReadCount and its three siblings existed and stored to a private field that nothing ever read. Calling them had no effect. The getters returned the Java field, so an accessor round-trip test would have passed.

DB_HEAP_FULL satisfies J per your call, so pubdef.in is unchanged there.

And a much worse find: the Java binding has been unloadable since the CalVer transition. DbConstants.java is a committed generated file still declaring DB_VERSION 5.3.29, and db_javaJNI does a version handshake at class-init, so every Environment open threw:

Berkeley DB library version 2026.0.9 doesn't match Java class library version 5.3.29

Regenerating fixes it. Added a blocking drift gate — src/dbinc_auto/ has had one for this exact class of mistake; the Java side did not, which is why five months of releases shipped a binding that could not open an environment.

U9 — C# needs your decision, not my code

lang/csharp does not exist. We deleted it in a77b2119d, whose message says "C# is dropped as a supported binding" — 2,634 files, ~154k lines. You said C# is still supported, which directly contradicts that commit, so I stopped rather than guess. See below.

G13 — measured once on 64 cores, and the gate fails

median tpm cv n
t=8 2,090,294 2.93% 5
t=32 485,962 0.78% 5

−76.8% against an 8.8% tolerance. Those cv figures are the point: this is a reproducible negative-scaling defect, not noise. The gate's own --self-test passes both directions on the same box.

So the honest reason perf isn't gated is not "no runner" — it's that the gate would fail immediately and correctly.

P5/D0 — validated, and qualified

Two claims hold: records/row is 3.093 (matching RFC 0008's 3.10), and batching gives +49% (72k→108k rows/s at 1→8 rows/txn).

One does not. A control arm with an empty data item — same record count, fewer bytes — gives +86.8% at 4 rows/txn, larger than the +35% batching itself buys there. At 1 row/txn the same control is only +5.9%, which is where RFC 0008's handoff finding was measured.

Bytes under the latch dominate exactly at the batch sizes D0's projection relies on. Not a refutation — a design constraint: the win must come from one record referencing a single page image, not two payload copies merged.

B1 — resolved by deletion

You said fix and merge if we feel it should be merged. It shouldn't: PIN-REMEASURE-2026-09.md already measured it as a regression (1.55×/1.36× slower where it fires) with a vacuous original verdict. Fixing the correctness bug would be making a measured regression safe. Deleted it and the three other parked negative-result branches; all reports, harnesses and raw profiles stay on master.

The mechanical five

T2 __os_malloc returns an offset pointer, so the harness's free() was invalid (engine callers use __os_free, unreachable from a public-API test). T6 compiler detection now verifies by compiling with -fsanitize=address. U2 paths resolved against the script, verified identical output from four cwds. U4/U5 dead paths fixed — U5 matters because it's an export-restriction tool that would fail silently on the day it runs.

gburd added 6 commits October 6, 2026 18:49
setBackupReadCount, setBackupReadSleep, setBackupSize and setBackupWriteDirect
existed and looked correct, but were WRITE-ONLY: each stored to a private field
and nothing ever pushed the value into DB_ENV->set_backup_config. Calling them
had no effect at all. The getters returned the Java field, so an accessor
round-trip test would have passed, and compilation obviously could not see it.

Fixed by wiring all four into configureEnvironment's diff, alongside their peers,
and by reading them back in the from-handle path. Two details read out of
src/env/env_backup.c rather than assumed:

  - set_backup_config calls __env_backup_alloc itself, so the setters do NOT
    require a BackupHandler to have been installed first and can be applied like
    any other setting.
  - get_backup_config returns EINVAL when no backup handle exists, and a handle
    exists exactly when a BackupHandler was installed -- so the read-back is
    conditioned on that rather than swallowing an exception.

Found while testing this, and far worse: the Java binding has been UNLOADABLE
since the CalVer transition. DbConstants.java is a committed generated file and
still carried

    DB_VERSION_MAJOR = 5; DB_VERSION_MINOR = 3; DB_VERSION_PATCH = 29;

so db_javaJNI's version handshake threw on every single Environment open:

    java.lang.RuntimeException: Berkeley DB library version 2026.0.9
    doesn't match Java class library version 5.3.29

Regenerating with dist/s_java_const fixes it. The root cause is that
s_java_const is NOT invoked by dist/s_all, so nothing regenerates this file --
the same shape as build_windows/db.h, which IS covered. That gap is why five
months of CalVer releases shipped a Java binding that could not open an
environment.

Test: test/db/run_u8_backup_config.sh (+ U8Check.java), registered in
test/MANIFEST, SKIPs without a JDK or JNI library. It opens a real environment,
sets values unlike any default, and reads each back THROUGH THE C LAYER via
Environment.getConfig() -- the only check that can distinguish "stored in a Java
field" from "reached the library".

Teeth verified: removing just the read_count push makes the test report
"read_count reached the library (4096, got 0)", which is precisely the pre-fix
behaviour.
DbConstants.java is generated from src/dbinc/db.in and dist/RELEASE by
dist/s_java_const, and it is COMMITTED. Nothing regenerated it for five months,
so after the CalVer transition it still declared DB_VERSION 5.3.29 while the
library reported 2026.0.9 -- and db_javaJNI does a version handshake at
class-init, so every Environment open threw and the Java binding was effectively
unloadable in every release since v2026.04.

src/dbinc_auto/ has had a blocking drift gate for precisely this class of
mistake since the ABI work. The Java side did not, which is the whole reason the
breakage survived that long: s_java_const IS reachable from s_all (via s_java),
but s_all is not run in CI, so 'regenerable' and 'actually regenerated' were
different things and only one of them was checked.

The new step regenerates and diffs, blocking on any difference, with an error
message naming the command to re-run.
G12/G13 have never measured anything because the measuring job needs a
self-hosted big-box runner that does not exist. Ran it once manually on EC2.

Dedicated c6id.16xlarge, 64 vCPU Xeon 8375C, THP off, 5 alternating reps per
arm, 20s each, TPROC-C:

    t=8   median 2,090,294 tpm    cv 2.93%   n=5
    t=32  median   485,962 tpm    cv 0.78%   n=5
    delta -76.8%   tolerance 8.8% (3x the t=8 cv, floored at 5%)

    VERDICT scale-shape FAIL ... failing_steps=t32:-76.8%

The coefficients of variation are the important part: 2.93% and 0.78% over five
alternating reps means the -76.8% step is nowhere near the noise, so this is a
reproducible negative-scaling defect rather than a measurement artefact. The
gate's own --self-test passes in BOTH directions on the same machine, so the
failure is a statement about the engine and not about the harness.

Two consequences worth being explicit about:

- G13 cannot become a blocking gate until the defect is fixed. The honest reason
  performance is not gated is not 'no runner available' -- it is that the gate
  would fail immediately and legitimately.
- The shape is the one P1 and P4 already attacked (peak near t=2-8, long
  decline). The remaining measured lever is P5's batching, design D0, which RFC
  0008 puts at +189% at t=32 for 4 rows/txn.

Raw data committed as test/bench/G13-SCALE-SHAPE-2026-10.tsv so the next run has
a comparable baseline.
…w, breaks at 4

You asked me to validate D0 before accepting it. Building it means a new log
record type, a recovery function, log_verify work and a DB_LOGVERSION bump, so
the premise is worth testing first with existing knobs. test/bench/d0_probe.c
does that on a dedicated 64 vCPU c6id.16xlarge.

Two of D0's claims hold:

  records-per-row is 3.093 at 1 row/txn, measured independently, matching RFC
  0008's 3.10.

  batching raises throughput as predicted -- 72,341 -> 97,812 -> 107,625 rows/s
  at 1/4/8 rows per txn, as records/row falls 3.09 -> 2.34 -> 2.22, i.e. +49%.

One does not. D0's premise, inherited from RFC 0008's Finding 3, is that latch
ACQUISITIONS dominate and the bytes moved inside the critical section are noise.
A control arm that stores an EMPTY data item -- same record count (2.27 vs 2.34),
fewer bytes -- gives +86.8% at 4 rows/txn:

    4 rows/txn, 100-byte data   97.8k rows/s  (3 reps: 98455 97400 97472)
    4 rows/txn, empty data     182.7k rows/s  (3 reps: 181185 181332 185533)

That is LARGER than the +35% batching itself buys at the same point. At 1 row per
transaction the identical control is only +5.9%, which is exactly where RFC
0008's handoff-cost finding was measured -- so the original finding was not
wrong, it was measured at the one batch size where bytes genuinely are noise.

Conclusion: bytes under the latch become dominant precisely at the batch sizes
D0's own projection depends on, so halving the record count will not deliver the
projected gain by itself. This is a design constraint rather than a refutation:
the win has to come from one record carrying both items BY REFERENCE to a single
page image, not from merging two payload copies into one record. A naive
combined record that still copies key and data separately would land much nearer
+35% than +189%.

Harness note: the first multi-row run hung silently. That was my bug, not the
engine's -- 32 threads touching 2+ keys per transaction with NO deadlock detector
configured. set_lk_detect(DB_LOCK_YOUNGEST) fixed it, and aborted transactions
are not counted as progress.
The instruction was to fix the correctness bug and merge perf/bhpin-r1 if we feel
the branch should be merged. It should not, and the evidence for that was already
on master: PIN-REMEASURE-2026-09.md re-measured all four parked perf branches
from behind the cursor-mutex wall that had invalidated their original verdicts,
and bhpin-r1 came back as 'REGRESSION as it stands (and its old verdict was
vacuous)' -- 1.55x/1.36x slower where its option actually fires.

So the latent correctness bug sat on top of a change that is not worth having
even when correct. Fixing it would have meant spending effort making a measured
regression safe to merge.

Deleted perf/bhpin-r1 and the three other parked branches the same re-measurement
disposed of: perf/mpool-pin and perf/lock-readpath (both NULL, 0.957-1.033x and
0.964-1.019x across 12 cells against a +/-4.8% noise floor) and perf/rsnap-ml
(confirmed regression, 0.063x at private/batch/32).

Nothing is lost. The report, the three A/B harness scripts (pinrm_bhpin_ab.sh,
pinrm_bhpin_fires.sh, pinrm_bhpin_why.sh) and every raw profile under
test/bench/results/pin-remeasure-2026-09/ are on master, which is where a
negative result belongs -- in the tree with its evidence, not in a branch nobody
will revisit.
T2 -- ASan "attempting free on address which was not malloc()-ed" at
test_lock_matrix.c:423. Not a stray pointer, an API-ownership mistake: lock_vec
allocates objlist->data with __os_malloc, and __os_malloc prepends a
db_allocinfo_t header and returns an OFFSET pointer (optionally via
DB_GLOBAL(j_malloc)), so the address a caller sees was never returned by libc
malloc. The engine's own callers use __os_free (txn.c:1015, :1540), which is
internal and unreachable from a public-API test program. The harness now
documents the ownership and does not free. After: 0 ASan errors, rc=0.

T6 -- test/lockmatrix/run.sh had CC=${CC:-clang} with no fallback, so the tier
could not run at all where clang is absent. Now prefers $CC, then clang, gcc,
cc, and VERIFIES the choice by compiling with -fsanitize=address rather than by
checking the binary exists -- a gcc without libasan passes `command -v' and then
fails at link. With nothing suitable it SKIPs with a reason. All three paths
tested: clang when present, gcc when clang is shimmed to fail, SKIP when all
fail.

U2 -- s_chk_message_id resolved MSG_DIR relative to the CALLER's cwd, so run
from dist/ every path missed, grep matched nothing, and it exited 0 having
checked no files. That is the shape that let duplicate message ids reach a
release while a checker for exactly that existed. Both MSG_DIR and gen_msg.awk
are now resolved against the script's own location, and a missing directory is a
hard error. Verified identical output (28 [EXPECTED] lines) from four different
working directories.

U4 -- s_tags probed ctags capabilities against ../../src/db/db.c, a path that
has never existed in this repository. Every probe failed silently, so flags
stayed empty and ctags always ran without -d -t -w. Probes src/db/db_iface.c
now, and warns if that is missing too.

U5 -- s_crypto ran ed(1) against docs/index.html to flip a "this release DOES
include cryptography" sentence; this fork has no such page. Dormant is not
correct: it is an export-restriction tool, so a silent failure on the day it
runs for real would ship a tarball whose docs still claim crypto is present. Now
conditional, and says what to do if such a statement is added to docs_src/.
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Coccinelle convention checks

No new violations. ✅

Resolved since baseline (3) -- update dist/cocci/baseline.txt to lock these in.
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/btree/bt_curadj.c|return (ret);
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/crypto/mersenne/mt19937db.c|return (ret);
rule_mutex_unbalanced|MUTEX_UNBALANCED|src/mp/mp_register.c|return (ret);

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

ABI diff vs v2026.10.1 (libabigail — authoritative)

abidiff exit bitmask: 0

Removed exported symbols (nm -D, _NNNN version suffix normalized)

None.


Advisory: libabigail/nm is the authoritative binary-ABI check; Coccinelle is complementary source-level early warning. See dist/cocci/README.md.

Two corrections to my own first attempt at T2.

First, the diagnosis was incomplete. __os_malloc only prepends its
db_allocinfo_t header and returns an offset pointer under #ifdef DIAGNOSTIC
(src/os/os_alloc.c:268-271). So libc free() on lock_vec's objlist->data is
invalid in a diagnostic build and perfectly valid in a production one -- which
is why I saw the bad free against my own --enable-diagnostic ASan library while
the tier's gate build (--enable-debug, DIAGNOSTIC unset) had never reported it.
The tracker row said 'ASan reports a bad free' without that condition, and the
condition is the whole reason it was hard to place.

Second, my fix was wrong. I removed the free() and wrote a comment arguing that
leaking a few DBT-sized blocks in a short-lived harness was the right trade.
LeakSanitizer disagreed and failed the tier in CI -- correctly, because this tier
runs ASan with leak detection on, so 'do not free' is not available as an option.
The comment sounded reasonable and was checkable in about a minute; I should have
run the tier runner rather than only the bare binary.

The actual fix is __os_free, the deallocator that matches the allocator, reached
by including db_int.h -- exactly what test/c/test_log_verify.c already does for
the same reason -- plus two more -I entries in run.sh so db_int.h's own include
of dbinc/queue.h resolves.

Verified in BOTH builds this time: run.sh rc=0 with '0 check(s) failed' on the
gate build, and 0 ASan/LSan errors against a --enable-diagnostic ASan library.
@gburd
gburd merged commit f677e28 into master Oct 7, 2026
63 checks passed
@gburd
gburd deleted the fix/u8-u9-backup-csharp branch October 7, 2026 01:16
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.

1 participant