Repository navigation
fix(storage): keep primary keys findable when one checkpoint splits a slot twice - #1513
Merged
adsharma merged 1 commit intoNov 5, 2026
Merged
Conversation
… slot twice HashIndex::splitSlots buffers new overflow slots in a vector. One checkpoint can grow a sub-index far enough to split a slot created earlier in the same pass. That slot's overflow chain lives in the buffer, so appending to the buffer can reallocate it while originalSlot still points into it, and the pass goes on reading freed memory. The allocator's metadata overwrites the start of the freed slot, which holds its fingerprints, so entries move to the new chain with wrong fingerprints: lookups miss them while the rows stay intact. Invalidating an entry in the freed slot is lost as well, which leaves a stale copy in the old chain. A lost key can be inserted again, leaving two rows with one primary key. Every key type is affected, since the slot layout and the fingerprint check are shared. A deque keeps its elements in place when it grows at the end. The trigger is the growth between checkpoints. 400,000 keys with commits at 140,000 and 390,000 rows lost 7 keys on b247d46 and on 0.22.1. On 0.22.1, commits at 200,000 and 400,000 lost none and one commit lost none. The regression test loses the same 9 of 18 chosen keys on 0.18.0, 0.22.1 and b247d46. A database built before this fix keeps its lost entries until the table is reloaded, and keeps the stale copies. A growing checkpoint over those finishes on the 0.22.1 release build, with the other keys still found. On a runtime-checks build it fails `newSlotId == newSlotIterator.idx()`. That check stays a DASSERT: throwing in release would stop checkpoints that work today. Cost on a runtime-checks build, single-threaded 400,000-key load with those commits, against b247d46: 7,505.5M vs 7,507.0M instructions (repeats differ by 1.7M), 1,890 more mallocs on 2,236,783. A load in one commit never builds this buffer. Fixes LadybugDB#1510 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adsharma
marked this pull request as ready for review
November 5, 2026 21:47
adsharma
self-requested a review
November 5, 2026 21:47
adsharma
approved these changes
Nov 5, 2026
|
I ran into the same bug independently on a different load and found the same cause: the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This work was produced with the help of language models.
Fixes #1510.
Copying 990,442
STRINGkeys into one table with commits at 141,501 rows and then every 250,000 left 5 keys unreachable on 0.22.1:MATCH (n:G {id: $k}) RETURN count(n)returned 0. With commits every 250,000 rows from the start, no key was lost. A smaller load with keys chosen by hash loses 9 of 18 keys on 0.18.0, 0.22.1 andmain. There a scan still returns all 600 rows, and a relCOPYfails withCopy exception: Unable to find primary key value long-enough-primary-key-229740.Inserting a lost key again succeeds, which leaves two rows with one primary key, also after a reopen.INT64keys lose entries too.Fix.
HashIndex::splitSlotsbuffers new overflow slots in a vector. One checkpoint can grow an index far enough to split a slot created earlier in the same pass, so the chain being read sits in the buffer being appended to. A reallocation leftoriginalSloton freed memory, whose first bytes hold the slot's fingerprints and get overwritten by the allocator, so entries moved with wrong fingerprints. The buffer is now astd::deque, whose elements stay in place as it grows.The on-disk format is unchanged. A database built before this fix keeps its lost entries until the table is reloaded, and keeps stale copies of moved entries in the old chains. A growing checkpoint over those finishes on the 0.22.1 release build, with the other keys still found, and fails
newSlotId == newSlotIterator.idx()on a runtime-checks build, this fix included.Tests.
SplitOfChainInNewOverflowSlotsKeepsKeysReachablepicks keys by hash so that the second of two checkpoints moves 18 of them twice, then looks up all 600 keys after a reopen. Onmain9 lookups return 0. That failure depends on what the allocator writes into freed memory and has only been run with glibc. The test runs at the default 4 KiB page only, since its keys are chosen for an index starting at 16 slots. Full ctest: 3358/3358.Cost. Only checkpoints that grow an index take this path. A single-threaded 400,000-key load with commits at 140,000 and 390,000 rows costs 1,890 more mallocs on 2,236,783, and its instruction count stays within repeat noise (runtime-checks builds).
The issue's separate failure with an 8 GiB
buffer_pool_sizeisn't touched here and needs its own issue.