Repository navigation
Conversation
Before: InvertedIndexer::seal() logged a failed column seal and went on. flush() and compact() then overwrote the status, so seal() returned OK, and the segment was sealed with that column unsealed. Now: InvertedColumnIndexer::seal() is split into prepare_seal() (special values, range index, histogram) and mark_sealed() (the seal marker); seal() still runs both. InvertedIndexer::seal() prepares every unsealed column first and returns the first error, as flush() already does for a column error. It marks the columns sealed only after all of them were prepared, so a failed seal leaves no column read-only while the caller keeps writing to the index. Test: SealReturnsColumnSealError stores a term value that cannot be decoded in one of four columns. Before this commit seal() returns OK and the other columns are sealed and reject inserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before: the indexes write without a WAL, so data is durable only once its column family is flushed. The seal wrote each column's marker into the default family together with the column's new range and histogram data, and nothing ordered their flushes. A writable open() lists the default family first, so RocksdbContext::flush() wrote the markers before the range index. A crash in between kept a sealed column with part of its range index, and range queries missed documents. Now: the markers are written only after the data they describe is flushed. InvertedIndexer::seal() flushes between preparing the columns and marking them; InvertedColumnIndexer::seal() flushes between prepare_seal() and mark_sealed(). Whatever RocksDB flushes later, a marker cannot reach disk before its data. Test: SealFlushesItsDataBeforeTheMarker checks that after a column seal the term, range and histogram families have nothing left in memory and the default family holds only the marker. Before this commit the range family still holds 1000 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before: reset_cf() deleted the family's handle once DropColumnFamily() succeeded, but replaced it in cf_handles_ only when CreateColumnFamily() succeeded. If the re-create failed, cf_handles_ kept the deleted pointer, and the next flush() or close() used it (SIGSEGV in DBImpl::Flush during a seal after one failed MANIFEST sync). Now: reset_cf() removes the handle from cf_handles_, as drop_cf() does. Test: FailedResetKeepsNoDeletedHandle fails the re-create's MANIFEST sync through a test-only FileSystem wrapper; before this commit the deleted handle stays in cf_handles_. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before: generate_statistical_indexes() treated an invalid term iterator as the end of the terms. A read error in the middle of the term table ended the scan, the range index was built from the terms before it, and the column was sealed. Range queries then missed documents, also after a later compaction succeeded. Now: the seal fails if the iterator's status is not OK after the scan, or if SeekToLast() does not find the last term (there is at least one term there, so an invalid iterator is an error; this also guards the key() read that follows). Test: ColumnSealFailsOnTermReadError fails the reads of the middle of the term table through a test-only FileSystem wrapper; before this commit the column seal returns OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
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.
Fixes #808.
When a segment seals its inverted index, four defects can make the seal lose an error or keep an incomplete range index. After that, range queries (
<,<=,>,>=) silently return too few documents. One of them also crashes the process after a single failed MANIFEST sync. This PR fixes the four defects, with one commit and one regression test per defect.Problem on main
InvertedIndexer::seal()logs a failed column seal and goes on.flush()andcompact()then overwrite the status, so the seal returns OK and the segment is sealed with that column unsealed.open()lists the default family first, soRocksdbContext::flush()writes the markers first. That happens, for example, to the writing segment's index after a restart. A crash in between keeps a sealed column with only part of its range index, and a later seal skips the column because it is already sealed.reset_cf()keeps a deleted handle. It deletes the handle onceDropColumnFamily()succeeds. It replaces the handle incf_handles_only ifCreateColumnFamily()succeeds. After a failed re-create, the nextflush()orclose()uses the deleted handle. Together with defect 1, one failed MANIFEST sync during a seal crashes the process with SIGSEGV inDBImpl::Flush.generate_statistical_indexes()loops whileiter_term->Valid()and never checksstatus(). A read error in the middle of the term table builds the range index from the terms before it, and the column is sealed. If the error was transient, the seal's compaction succeeds and nothing reports it.Changes
inverted_indexer.cc,inverted_column_indexer.h,inverted_column_indexer_write.cc):InvertedColumnIndexer::seal()is split intoprepare_seal()andmark_sealed().prepare_seal()writes the special values, the range index and the histogram, andmark_sealed()writes the marker.seal()still runs both.InvertedIndexer::seal()prepares every unsealed column and returns the first error, asflush()already does. It marks the columns sealed only after all of them were prepared.InvertedIndexer::seal()flushes between preparing and marking.InvertedColumnIndexer::seal()flushes betweenprepare_seal()andmark_sealed().Flush()waits until its tables are recorded in the MANIFEST, so a marker can no longer reach disk before its data, whatever RocksDB flushes later.InvertedIndexer::seal()adds one flush per seal. Most of that data was flushed anyway by the flush that follows.flush_unlocked(). Review showed that this orders only that explicit flush: an automatic flush of a full default memtable during the seal could still go first. The flush order inflush_unlocked()is therefore unchanged.rocksdb_context.cc, +2 lines): after a failed re-create,reset_cf()erases the handle fromcf_handles_, asdrop_cf()does.get_cf()returns null, and the seal fails with an error.inverted_column_indexer_write.cc, +14 lines): the seal fails if the iterator's status is not OK after the scan.SeekToLast()does not find a term. There is at least one term at that point, so an invalid iterator means an error, and the check guards thekey()read that follows.Tests
tests/db/index/column/inverted_column/inverted_indexer_seal_error_test.cc, new:98aa126SealReturnsColumnSealErrorSealFlushesItsDataBeforeTheMarkerFailedResetKeepsNoDeletedHandleColumnSealFailsOnTermReadErrorSealReturnsColumnSealErrorstores a term value that cannot be decoded in one of four columns. On98aa126,seal()returns OK, and the other three columns are sealed and reject inserts.SealFlushesItsDataBeforeTheMarkerseals a column and then reads the memtable entry counts. On98aa126, the range family still holds 1,000 entries in memory next to the marker. With this PR, only the marker is left.FailedResetKeepsNoDeletedHandlefails the re-create's MANIFEST sync. On98aa126, the deleted handle stays incf_handles_.ColumnSealFailsOnTermReadErrorfails the reads of the middle of a 100,000-term table during the column seal. It then seals again without the fault and checks three range queries. On98aa126, the column seal returns OK.rocksdb::FileSystemWrapperclasses in the test file inject the faults, through the publiccreate_opts_.envofRocksdbContext. Nothing insrc/changes for the tests.SeekToLast(). After a successful scan, the last block is in the block cache, so a test cannot make that read fail.Longer reproductions, not part of this PR, pass with these changes. On
98aa126they fail:98aa126, the crash comes right after the flush that carries the marker. 768 of 1,000 range entries survive, and 82 of 164 range queries are wrong.InvertedIndexer::seal(). On98aa126the process crashes with SIGSEGV. With this PR,seal()returns the error.98aa126, 40 of 164 range queries are wrong after a reopen. With this PR, the column seal fails, the segment seal then succeeds, and 0 queries are wrong.Existing suites with this PR (macOS arm64, CMake Release), all passing:
inverted_column_indexer_*tests andinverted_indexer_util_test;fts_column_indexer_test(93) andfts_rocksdb_reducer_test(18);segment_test(108),segment_helper_test(15),segment_row_id_test(10) andadd_column_batch_boundaries_test(1);fts_multi_segment_test(3),write_recovery_test(7),optimize_recovery_test(1) andcollection_test(104).Merging with #802 and #803
inverted_column_indexer_write.cc: it replaces the tworeset_cf()calls of the seal with an in-place clear. This PR touches other parts of that file: the end of the term loop,SeekToLast(), andseal(). fix(index): empty the range column family in place at seal #803 keepsreset_cf()as a fallback for an unreadable range family, and fix 3 covers that fallback.rocksdb_context.cc, but onlycreate(). This PR changes onlyreset_cf()there.git merge-treereports no conflict with either PR, so whichever merges second needs only a rebase.Out of scope
SegmentImplignores the results ofcolumn_indexer->seal()andinvert_indexers_->flush()when it adds or alters a column (segment.cc, lines 3254 and 3406). There, the segment meta is already updated before the seal, so returning the error needs a decision on how to roll the meta back. Not changed here.mainthis already happens when the final flush or compaction of the seal fails. This PR adds one rare way into it: a failed marker write into the memtable.reset_cf(): [Bug]: A crash while a segment seals can leave a collection that cannot be opened ("Column family[...$RANGES] does not exist") #798 / fix(index): empty the range column family in place at seal #803.Not covered
98aa126tree with this PR's files. Every file this PR touches is byte-identical between98aa126andmain(7f54d94).🤖 Generated with Claude Code