Skip to content

cpp_store fix-forward: mmap .idx (no heap slurp), real lmdb probe, rows_per_key gate - #4271

Open
s243a wants to merge 2 commits into
mainfrom
claude/cpp-store-idx-mmap-fix
Open

s243a wants to merge 2 commits into
mainfrom
claude/cpp-store-idx-mmap-fix

Conversation

@s243a

@s243a s243a commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Fix-forward for Fable's review of the merged store-backend change (#4270). New branch off current main; the merged commit is untouched. Do not merge (hub-gated; a kimi review is pending).

Findings addressed

  • P2a (important) — mmap the .idx instead of slurping it into heap. The in-memory index was a whole-file std::string (non-evictable anonymous heap), and the .idx is 39-97% of the store. Because auto routes >2×RAM stores to indexed, under that pressure indexed tried to allocate ~1× MemAvailable of non-evictable heap → bad_alloc → caught as goal failure → silent under-answering. Now .idx is mmap'd (PROT_READ, MAP_PRIVATE) like .data: page-cache-backed + evictable; binary search reads through idx_base_/idx_len_ (bounds-checked idx_u32/idx_u16); ifstream slurp kept as the non-POSIX fallback. Also stops charging the full index size to the D43 counters at open, and corrects the false "small and constant" comment.
  • P2b — auto "lmdb usable" probe matches the C++ lane. Now checks system liblmdb links (#include <lmdb.h> + -llmdb) and best-effort mdb_env_opens an already-built store (catches MDB_INVALID), not just that the npm module loads. Falls back to indexed on any failure.
  • rows_per_key gate. auto picks lmdb only when store > 2×RAM and rows_per_key ≥ 2 (from the .idx header, n_records/n_keys, no scan). ABI symprov (1.03 rows/key) stays on indexed even when huge. Unknown rpk (pre-build) → size-only.
  • P3 + nits. Guard tellg()<0 in ensure_open (was resize(SIZE_MAX) on a failed retry) + close streams/maps on throw (close_indexed); close fds right after mmap; bench_crossover.sh now asserts rows_found identical across backends; validate UW_STORE_LMDB_RAM_FACTOR is an integer; test_auto_select.sh stubs the lmdb probe (no real npm install) and adds rows_per_key cases; noted stat/od are GNU.

Guardrails (all green)

  • Store differential 503/0, corpus 51/0, ABI verify 122/0 (built -O0/nice, one at a time under memory pressure).
  • Byte-frozen template goldens re-baselined (plain 96529→99808, lmdb 96770→100049; +3279 each, gate-independent); test_wam_cpp_templates green.
  • Answer-identity: mmap-indexed rows_found == lmdb on a scale-store cross-check; test_auto_select.sh ALL PASS (incl rpk=1→indexed, rpk=3→lmdb).
  • Frozen resolver.pl / resolver_store.pl / debian/ untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RoXjhStCqoig6944pVNBGe

s243a and others added 2 commits September 15, 2026 22:27
…ws_per_key gate

Fixes Fable's review of the merged store-backend change (#4270). Fix-forward on a
new branch; the merged commit is untouched.

P2a -- mmap the .idx instead of slurping it into heap. The in-memory index was a
whole-file std::string (idx_blob_): non-evictable ANONYMOUS heap, and the .idx is
39-97% of the store. Since auto routes >2x-RAM stores to indexed, under that
pressure indexed tried to allocate ~1x MemAvailable of non-evictable heap ->
bad_alloc -> caught by step_execution as goal failure -> SILENT under-answering.
Now .idx is mmap'd (PROT_READ, MAP_PRIVATE) exactly like .data: page-cache-backed
+ evictable, in-memory binary search reads through idx_base_/idx_len_ (bounds-
checked idx_u32/idx_u16), ifstream slurp kept only as the non-POSIX fallback.
Also: the mmap path charges nothing to the D43 counters at open (was charging the
full index size); the false "small and constant" comment is corrected; munmap +
fd close in the destructor (close_indexed()).

P2b -- auto "lmdb usable" probe now matches what the C++ build needs: not just
that the npm module loads, but that system liblmdb links (#include <lmdb.h> +
-llmdb) and, best-effort, that an already-built lmdb store under DIR/lmdb/* actually
mdb_env_opens (catches MDB_INVALID). Falls back to indexed on any failure; message
reworded to what is probed.

rows_per_key gate -- auto now picks lmdb only when store>2xRAM AND
rows_per_key >= UW_STORE_LMDB_MIN_ROWS_PER_KEY (default 2), computed cheaply from
the UWIX .idx headers (n_records/n_keys, no scan). A ~1-row/key store (ABI symprov
= 1.03) stays on indexed even when huge (lmdb buys nothing). Unknown rpk (pre-
build, no .idx) skips the gate (size-only).

P3 + nits: guard tellg()<0 in ensure_open (was resize(SIZE_MAX) on a failed retry)
and close streams/mmaps on throw paths (close_indexed); close data_fd_/idx_fd_
right after a successful mmap; bench_crossover.sh now ASSERTS rows_found identical
across backends per cell (was print-only); validate UW_STORE_LMDB_RAM_FACTOR is an
integer (else warn+default 2); test_auto_select.sh stubs the lmdb-usable probe so
it can never trigger a real npm install, and adds rows_per_key gate cases; noted
stat/od are GNU/coreutils.

Guardrails: 503 differential + 51 corpus + 122 ABI verify all 0 (built -O0/nice,
one at a time); goldens re-baselined (plain 96529->99808, lmdb 96770->100049;
+3279 each, gate-independent); mmap-indexed rows_found == lmdb on a scale-store
cross-check; test_auto_select ALL PASS (incl rpk=1->indexed, rpk=3->lmdb). Frozen
resolver/store/debian untouched. Docs updated (BACKEND_SELECTION.md: rule now with
rows_per_key + real probe, storage note on the mmap fix, key-sort deferred).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoXjhStCqoig6944pVNBGe
…cold evict

Second-reviewer residuals on PR #4271.

1. idx_key_compare: it formed `const unsigned char* k = idx_base_ + off` even when
   off > idx_len_ (a past-the-end pointer = UB, though never dereferenced), and an
   empty target with off>size fell through to `return 0` -- a spurious "match"
   yielding garbage offsets that read_record then bounds-rejects (dropped rows).
   Now handle out-of-range off BEFORE forming the pointer:
     if (off > idx_len_) return target.empty() ? 1 : -1;
     if (off + len > idx_len_) len = idx_len_ - off;
   Only reachable on a CORRUPT .idx; well-formed stores unaffected (503
   differential + 51 corpus: 0 divergences). Goldens re-baselined (plain
   99808->100151, lmdb 100049->100392; +343 each, gate-independent).

2. bench_main.cpp evict_file: it only called posix_fadvise(DONTNEED), a no-op on
   non-resident pages, so the "cold" numbers were cold-in-name-only. Now
   pre-faults the file (streaming read) BEFORE DONTNEED so eviction has resident
   pages to drop -- the cold lookups genuinely fault from disk. (bench-only; no
   goldens/gates.)

Frozen resolver/store/debian untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoXjhStCqoig6944pVNBGe
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