Repository navigation
Conversation
db_stress could only build the trie as the secondary index or as the default read index (use_udi_as_primary_index), and selected it per read through the legacy ReadOptions::table_index_factory pointer. db_bench could only build it as the secondary index. kCustomOnly, kStandardRequired, and the kBuiltin/kPreferCustom read selectors were never exercised outside unit tests. Replace use_udi_as_primary_index with --index_mode, defaulting to the library default, and add --read_index, whose auto value keeps reading through the trie when it is the secondary index. Both tools reject combinations that cannot work. The crashtest draws index_mode once per DB together with use_trie_index, draws read_index per invocation, and only disables the paths that cannot work in a mode: backup and partitioned index/filters when reads default to the trie, and kBuiltin reads and format_version < 6 with kCustomOnly.
Describe the index modes, per-read selection, on-disk markers, open-time fallback, cache accounting, parallel builds, migration, and known gaps.
The IndexFactory API and parallel compression with a custom index landed without release notes. Also correct the parallel_threads comment, which still said a custom index forces single-threaded compression.
|
| Check | Count |
|---|---|
cert-err58-cpp |
1 |
concurrency-mt-unsafe |
5 |
cppcoreguidelines-avoid-non-const-global-variables |
1 |
modernize-use-emplace |
1 |
performance-inefficient-vector-operation |
3 |
| Total | 11 |
Details
db_stress_tool/db_stress_test_base.cc (5 warning(s))
db_stress_tool/db_stress_test_base.cc:7003:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7014:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7019:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7028:5: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_test_base.cc:7037:5: warning: function is not thread safe [concurrency-mt-unsafe]
utilities/trie_index/trie_index_factory.cc (2 warning(s))
utilities/trie_index/trie_index_factory.cc:30:49: warning: initialization of 'trie_index_factory_type_info' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
utilities/trie_index/trie_index_factory.cc:30:49: warning: variable 'trie_index_factory_type_info' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]
utilities/trie_index/trie_index_test.cc (4 warning(s))
utilities/trie_index/trie_index_test.cc:2538:7: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
utilities/trie_index/trie_index_test.cc:6687:13: warning: use emplace_back instead of push_back [modernize-use-emplace]
utilities/trie_index/trie_index_test.cc:6769:5: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
utilities/trie_index/trie_index_test.cc:6792:5: warning: 'push_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
819c9f4 to
cd9b272
Compare
The trie index (format 1, a LOUDS succinct trie) was larger than the standard index with index_block_restart_interval=16 on every measured key shape and up to 84% slower on Get. It also had correctness gaps: it dropped the empty user key, stored offsets as 32-bit host-order integers, accepted any comparator until flush time, and crashed on some malformed index blocks that still passed their checksum. Add format 2, opted into with TrieIndexFactory's new format_version option. It is a path-compressed byte trie over buckets of eight front-coded separators, with block handles in linear frame-of-reference columns that store a residual width per group of 64 values, little-endian and read in place. The reader checks the whole block at open, including that block offsets never decrease, so a malformed index fails with Corruption. Readers accept both formats; the factory still writes format 1 by default. Fix format 1 in place: store the empty user key, reject unknown header flags, validate the trie structure and bitvector rank/select data on open, bound counts read from the block, and count all auxiliary memory. DB::Open now rejects the trie factory with a non-bytewise comparator, an unknown format_version, or format 1 on big-endian hosts. db_stress, db_bench, and the crash test take --trie_index_format_version; the crash test draws it per invocation so DBs mix both formats. Also correct the table.h rollback text: an index_mode change through SetOptions() only reaches SSTs opened afterwards.
cd9b272 to
a4d2c11
Compare
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit a4d2c11 SummaryThis PR adds trie index format 2 (path-compressed trie over front-coded buckets with frame-of-reference columns) and fixes several correctness bugs in format 1 (empty user key handling, malformed block validation, memory accounting). The changes are well-structured and the bug fixes are sound. I identified 3 HIGH and 5 MEDIUM findings, none of which are blockers -- the HIGHs are existing bugs that this PR correctly fixes. High-severity findings (3):
Full review (click to expand)Findings🔴 HIGHH1:
H2: At H3: No unknown flags rejection (existing bug, fixed by PR) Current 🔶 MEDIUMM1: Per CLAUDE.md, defaulted parameters are the "#1 trap on refactoring." The change from M2: Format version dispatch asymmetry (write vs. read)
M3: The extraction of scan bound checking from M4: db_crashtest.py module-level random choices reduce per-invocation coverage diversity
M5: Relaxed atomics in
⚪ LOW / OBSERVATIONSL1: Empty key fix is correct The L2: ~200us per SST open is well within tolerance. The validation runs once per SST open for format 1 indexes and catches structural corruption that would otherwise cause silent wrong results or crashes during read operations. L3: Format 2 portability improvement Format 2 uses L4: Good defense-in-depth. Prevents crafted input from causing oversized vector allocations that could OOM the process during SST open. Cross-Component Analysis
Test Coverage AssessmentThe PR should include tests for:
Behavioral CompatibilityNo breaking behavioral changes for existing users:
ℹ️ About this responseGenerated by Claude Code. Limitations:
Commands:
|
Summary
Adds trie index format 2, an opt-in serialized format for the experimental trie user-defined index (
TrieIndexFactory), and fixes correctness bugs in the existing format 1. Readers accept both formats; the factory keeps writing format 1 by default.Stacked commits. This branch also carries three prerequisite commits that have no separate PR. They can be reviewed commit by commit:
The rest of this description covers the main commit, a4d2c11.
Format 2 layout (full spec in
utilities/trie_index/trie_v2.h):Why the change is required
The trie index is meant to beat the standard index on prefix-heavy keys, but format 1 does not. On a 3M-key, 48-byte-key DB with about 2 blocks per prefix it is 8x the size of the standard index at
index_block_restart_interval=16and about twice as slow on Get as the default standard index (table below). Most of its size is per-edge child-position tables (also copied to the heap at open) and an always-on seqno table. Format 1 also has correctness gaps that matter for production use (below). Fixing size and speed needs a new layout, so this adds a format version instead of changing format 1 in place.Previous behavior
""as a separator (a lonePut("", v), or a first data block holding only"") failed to flush with Corruption in kStandardRequired, kCustomDefault, and kCustomOnly, which stopped writes; in kStandardDefault its trie was silently unused.DB::Openaccepted the trie factory with a non-bytewise comparator; the first flush then failed with a background error.memcpys, so indexes are not portable between little- and big-endian hosts.std::length_errorfrom a hugeresize, or read out of bounds after an overflow-count sum wrapped in 32 bits. Unknown header flags and a nonzero reserved field were accepted.ApproximateMemoryUsage()omittedoverflow_base_and the aligned copy made for unaligned blocks.New behavior
TrieIndexFactorytakesformat_version(1 or 2), also settable asuser_defined_index_factory={id=trie_index;format_version=2}. Format 1 stays the default.NewReaderdispatches on the version in the header, so every factory reads both formats and a DB can mix them.DB::Open(throughTrieIndexFactory::ValidateOptions) rejects a non-bytewise comparator, aformat_versionother than 1 or 2, and format 1 on big-endian hosts.--trie_index_format_version; the crash test draws it per invocation.Implementation
utilities/trie_index/trie_v2.{h,cc}:TrieV2Builder,TrieV2Index, andTrieV2Iterator.TrieV2Index::Initvalidates the header, section bounds, trie, columns, and side tables, and checks that block offsets never decrease (with room for the gap after each block when sizes are derived, so a size cannot wrap). Bucket data is checked as it is decoded. All loads are unaligned little-endianDecodeFixed*reads on the block in place, so the reader allocates nothing proportional to the index.DirectMonotonicWriterand tantivy's blockwise linear codec.TrieV2Builder::Finishreturns InvalidArgument if block offsets decrease.TrieV2IndexReaderandTrieV2IndexIteratorintrie_index_factory.{h,cc}. Scan-bound handling forPrepare()moved into aTrieScanBoundshelper shared by both iterators.TrieIndexBuilderto serialize its buffered entries in the configured format. For format 2,EstimatedSize()tracks key bytes, front-coding headers, handle ranges, duplicates, and tags incrementally and passes them toTrieV2Builder::SizeBound(), which sits next to the serializer. Blocks still being compressed in a parallel build are charged like the largest block so far, so the estimate can fall short until their handles arrive.louds_trie.ccandbitvector.cc:LoudsTrie::ValidateStructure(): node and leaf counts, child-position tables (checked in one word-at-a-time pass overs_louds), a BFS level walk bounded bymax_depth, and chains re-walked against the trie;Bitvector::ValidateDerivedData(): rank LUT, select hints, and bits past the end. Select-hint construction moved intoForEachSelectHint, shared by the writer and the validator;num_chains, use a 64-bit overflow prefix sum, and reject unknown flags.table.hrollback text said loweringindex_modetakes effect immediately. A table reader fixes its routing when it opens the SST, so aSetOptions()change reaches only SSTs opened afterwards; the text now says so. The IndexFactory component doc describes both formats and the new rejections. Three release notes are added.Safety, compatibility, and limitations
format_version=2, new SSTs are format 1 and readable by released versions.""; such SSTs could not be written before.user_defined_index_factory={id=trie_index;format_version=N;}. Older binaries needignore_unknown_optionsto load them, as with any new option.DB::Openrejections apply only when the trie factory is configured. Those configurations previously failed at flush time or, on big-endian, wrote non-portable indexes.NewReaderalone, release flags, on db2-shaped data (4 SSTs of about 14.8K data blocks each):NewReadergoes from 121 to 320 us per SST with a 2.5 MB index. Most of the increase is the child-position check, at about 1 ns per trie node.NewReadertakes 26 us per SST with a 0.33 MB index. 18 us of that is the offset-order check, which is reader code only and can be made cheaper without a format change.check_format_compatible.sh.Door: two-way to merge, since format 2 is opt-in and the default is unchanged. Per DB, writing format 2 is one-way for downgrade until those SSTs are rewritten as format 1.
Blast radius: trie index users only.
Test Plan
Performance. Release
db_bench,format_version=8, on an Apple Silicon laptop:key_size=48,prefix_size=32,value_size=100, no compression, 10-bit bloom, 4 KiB blocks,fillrandom,compactwith--seed=7so every Get is found. db2 has 100 keys per prefix (about 2 blocks per prefix); db3 has 20000 keys per prefix (hundreds of blocks per prefix).readrandom --reads=1500000andseekrandom --reads=500000 --seek_nexts=10, single-threaded. "Cached" uses a 2 GiB block cache holding everything; "32 MiB" puts data, index, and filter blocks in a 32 MiB block cache.Not measured: Linux, concurrent reads, or reads from storage.
Unit and DB tests (macOS, Apple clang, debug):
trie_index_test(249 tests) covers:EstimatedSize()bound with uniform and irregular block handles;trie_index_db_test(558) runs every test in kStandardDefault, kCustomDefault, and kCustomOnly with both formats, plus new tests for the empty user key, mixed formats in one DB, and theDB::Openrejections.ASSERT_STATUS_CHECKED=1,trie_index_test,trie_index_db_test, andtable_testpass (7,873 tests).db_crashtest_test.pypasses.COERCE_CONTEXT_SWITCH=1, the new and changed tests (OutlierBlockWidensOnlyItsGroup,DecreasingOffsets,EstimatedSizeBoundsSerializedTrie,BlockHandleEncodingLimit) pass 100 repeats.MalformedIndexIsRejectedOrReadablefails with either format 1 reader fix reverted: an uncaughtstd::length_errorwithout thenum_chainsbound, and theGetOverflowHandlebounds assertion without the 64-bit sum.DecreasingOffsetsfails without the reader's offset-order check.Fuzzing. ASAN and UBSAN builds (Homebrew clang) open and iterate truncated and byte-flipped indexes:
TrieIndexFactory(1): 1.9M cases over 6 seeds, clean;Crash test.
db_crashtest.py blackbox --simple --use_trie_index=1ran for 8 minutes each with--index_mode=1and--index_mode=3. Each run had 9 kill-and-reopen invocations that drew format 1 or 2 per invocation (4 and 5, then 6 and 3), so each DB mixed both formats. Both passed. Compression was pinned to lz4 because this build lacks Snappy.make checkran on the previous revision of this commit and passed except for failures independent of this change (listener_testfile-operation tests that also fail at the base commit, two load-sensitivedb_sst_testSstFileManager tests that pass alone, and tools missing from that build). It was not rerun after the column layout change, which touched onlyutilities/trie_indexand the release note.CI passes on a4d2c11, including the MSVC jobs. Earlier pushes failed on MSVC because a test local was named
far, which<windows.h>defines as a macro, and on GCC-Wshadowbecause of a lambda parameter inlouds_trie.cc. Both are fixed.Not tested locally: Linux and Windows (covered by CI builds and their test shards), big-endian (no CI job; format 1 test cases are not instantiated there), and an actual downgrade to a released binary.