fix(mem_wal): keep sealed memtables queryable until flush commits#6814
Merged
jackye1995 merged 2 commits intoMay 17, 2026
Merged
Conversation
A concurrent reader could miss rows in the window between `freeze_memtable` and the flush task's manifest commit: the frozen generation was no longer the active memtable but not yet recorded as flushed. Keep frozen-awaiting-flush memtables in `WriterState.frozen_memtables` so they stay in the read union, and drop them by generation only on flush-commit success (retained on failure until a later flush or WAL replay). Add `ShardWriter::in_memory_memtable_refs` and `LsmScanner::with_in_memory_memtables` as the read-path entry point, capturing active + frozen atomically under one state lock. Rename `ActiveMemTableRef` -> `InMemoryMemTableRef` (back-compat alias kept). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Add tests for the sealed-memtable read fix and migrate the in-repo scanner readers onto the frozen-aware entry point: - collector: assert collect()/collect_for_shards() emit active + every frozen memtable, in ascending generation order. - planner: end-to-end scan spanning base/flushed/frozen/active asserting frozen rows are in the read union and dedup by generation across the active/frozen seam. - write: frozen handle dropped on flush-commit success; retained (rows intact) on a fenced/failed flush so the hole cannot reopen. - Migrate planner/vector_search test harnesses and the write.rs WAL-only accessor test from with_active_memtable/active_memtable_ref to with_in_memory_memtables/in_memory_memtable_refs. The planner scan test surfaced a correctness bug in the original fix: collect() pushed `active` (newest generation) before `frozen` (older), but the planner relies on sources being in ascending-generation order (it reverses to gen-DESC for the dedup tiebreaker). A pk written, sealed, then re-written into the new active memtable before the sealed one flushed would resolve to the stale frozen value. LsmDataSourceCollector now sorts in-memory sources by generation, restoring active-wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Problem
A concurrent reader could miss rows in the window between
freeze_memtableand the flush task's manifest commit. During that window the frozen generation is no longer the active memtable, but is not yet recorded as a flushed generation in the manifest — so it falls out of the read union entirely.Fix
WriterState.frozen_memtablesso they stay in the read union (Arc refcount, not a copy — the flush task holds them alive for the drain anyway).ShardWriter::in_memory_memtable_refsandLsmScanner::with_in_memory_memtablesas the read-path entry point — captures active + frozen atomically under onestate.read()(no torn freeze).ActiveMemTableRef→InMemoryMemTableRef(frozen is just the immutable case; same shape). Back-compat alias andwith_active_memtabletest/back-compat entry point retained.LsmDataSourceCollectoremits in-memory sources in ascending generation order (see below).Source-ordering correctness fix
Adding the scan-level test surfaced a latent bug in the first cut:
collect()pushedactive(newest generation) beforefrozen(older). The planner relies on sources being in ascending-generation order — it reverses them to generation-DESC so the newest row gets the lowest stream index and wins the dedup tiebreaker. With active emitted first, a pk that was written, sealed, then re-written into the new active memtable before the sealed one flushed would resolve to the stale frozen value. The collector now sorts in-memory sources bygeneration, restoring active-wins across the active/frozen seam.Tests
collect()/collect_for_shards()emit active + every frozen memtable, in ascending generation order (frozen deliberately registered out of order).All 265
mem_waltests pass;cargo clippy -p lance --tests -D warningsclean.🤖 Generated with Claude Code