This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch cassandra-6.0 in repository https://gitbox.apache.org/repos/asf/cassandra.git
commit 27cc24bc794453edce39f66389b9355c61d84ed3 Merge: fe11477010 4bd98de6ee Author: Caleb Rackliffe <[email protected]> AuthorDate: Sat Jul 18 23:04:42 2026 -0500 Merge branch 'cassandra-5.0' into cassandra-6.0 * cassandra-5.0: SAI Component Checksum Validation Should be Segment-Aware CHANGES.txt | 1 + .../index/sai/disk/io/IndexInputReader.java | 64 ++++++--- .../index/sai/disk/v1/V1OnDiskFormat.java | 99 +++++++++++++- .../index/sai/cql/StorageAttachedIndexDDLTest.java | 23 ++++ .../index/sai/disk/v1/SegmentFlushTest.java | 149 +++++++++++++++++++++ 5 files changed, 314 insertions(+), 22 deletions(-) diff --cc CHANGES.txt index 55f62c138b,8acee65e3d..f6687fcea4 --- a/CHANGES.txt +++ b/CHANGES.txt @@@ -1,70 -1,7 +1,71 @@@ -5.0.9 +6.0-alpha2 + * Depend only on platform-specific Zstd JNI native libraries (CASSANDRA-21483) + * Expose immediately-executed tasks in the queries virtual table (CASSANDRA-21471) + * Avoid potential deadlock between GlobalLogFollower and GossipStage (CASSANDRA-21384) + * Add CMS membership as a field in ClusterMetadata (CASSANDRA-20736) + * Fix maven remote publishing of Accord artifacts (CASSANDRA-21261) + * Move long running TCM operations to a longer timout (CASSANDRA-21453) + * Offline nodetool commands should not print network options in help (CASSANDRA-20876) + * Defer creation of the system_cluster_metadata keyspace until CMS initialization (CASSANDRA-21477) + * Support direct I/O for background SSTable writes (CASSANDRA-21134) + * Relax assertion on partitioner instances in SinglePartitionReadCommand (CASSANDRA-21251) + * Report cancelled read command execution to coordinator as a RequestFailure.TIMEOUT (CASSANDRA-21468) + * Fix TCM log catchup from peer with snapshots and gaps in the log sequence (CASSANDRA-21455) + * Speed up nodetool doc generation by producing all command help in a single jvm (CASSANDRA-21444) + * Always send TCM commit failures as Messaging failures (CASSANDRA-21457) + * Fix ReadCommand serializedSize() using incorrect epoch (CASSANDRA-21438) + * Allocation improvements in ProtocolVersion, StorageProxy and MerkleTree (CASSANDRA-21199) + * Don’t leave autocompaction disabled during bootstrap and replace (CASSANDRA-21236) + * Make nodetool abortbootstrap more robust (CASSANDRA-21235) + * Don't clear prepared statement cache on nodetool cms initialize (CASSANDRA-21234) + * Improve performance when deserializing cluster metadata (CASSANDRA-21224) + * Minor TokenMap performance improvement (CASSANDRA-21223) + * Handle lost response when committing PrepareMove (CASSANDRA-21222) + * SEPExecutor.maybeExecuteImmediately does not always execute tasks immediately despite available worker capacity (CASSANDRA-21429) + * Safely regain ranges and delete retired command stores (CASSANDRA-21212) + * Reduce memory allocations in miscellaneous places along read path (CASSANDRA-21360) + * Avoid ByteBuffer wrapping in cql3.selection.Selector.InputRow to reduce memory allocation rate (CASSANDRA-21362) + * Reduce cost to calculate BTreeRow.minDeletionTime (CASSANDRA-21414) + * Implement custom CassandraThread to keep direct references to frequently used thread local objects (CASSANDRA-21020) + * Avoid megamorphic call overhead at RandomAccessReader#current (CASSANDRA-21399) + * Enable async GC logging for JDK versions which support it to avoid potential hiccups caused by GC log file I/O blocking (CASSANDRA-21372) + * Add rowsMutatedPerWriteHistogram metric to track rows mutated per write request (CASSANDRA-21320) + * Add TotalRowsRead and TotalRowsMutated counters to TableMetrics for accurate per-table row throughput tracking (CASSANDRA-21321) + * Move exception handling of SPI startup checks when iterating over them (CASSANDRA-21409) + * Avoid type lookup in SerializationHeader#getType if schema and SSTable are aligned (CASSANDRA-21402) + * Replace LongAdder in metric-like logic with ThreadLocalCounter (CASSANDRA-21400) + * BTreeRow.hasLiveData: avoid Cell iteration if there are no cell tombstones (CASSANDRA-21363) + * Reduce memory allocations in row merge logic (CASSANDRA-21359) + * Restore option to avoid hint transfer during decommission (CASSANDRA-21341) + * Add an offline cluster metadata tool (CASSANDRA-19151) + * Accord: Tail Latency Improvements (CASSANDRA-21361) + * Artificial Latency Injection (CASSANDRA-17024) + * Accord: Clean Shutdown/Restart, Rebootstrap, et al (CASSANDRA-21355) + * Reduce memory allocations in SelectStatement.getQuery (CASSANDRA-21351) + * Avoid allocation by getFunctions in SelectStatement.authorize (CASSANDRA-21347) + * Avoid unit conversion in DatabaseDescriptor.getMaxValueSize() for every deserializing Cell (CASSANDRA-21295) + * Fix single token batch atomicity with Accord/non-Accord batches by using the batch log (CASSANDRA-20588) + * Avoid CompactionOptions parsing for every read by WithoutPurgeableTombstones (CASSANDRA-21294) + * Ensure schema created before 2.1 without tableId in folder name can be loaded in SnapshotLoader (CASSANDRA-21246) + * Differentiate between legitimate cases where the first entry is the same as the last entry and empty bounds in SSTableCursorWriter#addIndexBlock() (CASSANDRA-21255) + * Introduce minimum_threshold for data resurrection startup check (CASSANDRA-21293) + * Synchronously publish changes to local gossip state following metadata updates (CASSANDRA-21239) + * Change default for cassandra.set_sep_thread_name to false to reduce CPU usage (CASSANDRA-21089) + * Avoid permission checks for masked columns when the table doesn't have any (CASSANDRA-21299) + * Reduce allocations and array copies due to buffer resizing in LocalDataResponse during row serialization (CASSANDRA-21285) + * Implement a guardrail for client driver versions (CASSANDRA-21146) + * Enable IAuthenticator to declare supported and alterable role options (CASSANDRA-20834) + * Avoid capturing lambda allocation in UnfilteredSerializer.deserializeRowBody (CASSANDRA-21289) + * Avoid Cell iterator allocation for alive rows in MetricsRecording transformation of ReadCommand (CASSANDRA-21288) + * Reuse a single TrackedDataInputPlus instance per UnfilteredSerializer (CASSANDRA-21296) + * In-tree docs are included in binary artifacts (tarball, deb, rpm) as html and man pages (CASSANDRA-17260) + * Add tooling to repair system peers and peers_v2 if inconsistent with cluster metadata (CASSANDRA-21187) + * Fix a removed TTLed row re-appearance in a materialized view after a cursor compaction (CASSANDRA-21152) + * Rework ZSTD dictionary compression logic to create a trainer per training (CASSANDRA-21209) +Merged from 5.0: + * SAI Component Checksum Validation Should be Segment-Aware (CASSANDRA-21516) * Support Python 3.12 and 3.13 in cqlsh (CASSANDRA-20997) - * Fix AssertionError in hasReplicaWithOngoingRepair when parallel_repair_count > 1 (CASSANDRA-21426) + * Make synchronization on VectorMemoryIndex inserts more granular (CASSANDRA-21160) * putShortVolatile is not volatile in InMemoryTrie (CASSANDRA-21353) * Fix RequestFailureReason serializer and nits in a few others (CASSANDRA-21437) * Remove golang dependency in gen-doc and replace with python implementation (CASSANDRA-21432) diff --cc src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java index 0c93f3c1fe,7cfef29943..ed7dcb488c --- a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java +++ b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java @@@ -20,6 -20,10 +20,8 @@@ package org.apache.cassandra.index.sai. import java.io.IOException; + import javax.annotation.concurrent.NotThreadSafe; + -import org.apache.cassandra.io.util.FileHandle; -import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.lucene.store.DataInput; import org.apache.lucene.store.IndexInput; diff --cc src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java index ba8b13ca4c,a77fad1888..5068e039f4 --- a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java +++ b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java @@@ -21,12 -21,10 +21,15 @@@ package org.apache.cassandra.index.sai. import java.io.IOException; import java.io.UncheckedIOException; import java.util.EnumSet; + import java.util.List; import java.util.Set; +import com.codahale.metrics.Gauge; import com.google.common.annotations.VisibleForTesting; + ++import org.apache.lucene.codecs.CodecUtil; ++import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.store.IndexInput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --cc test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java index e2837df331,e6c0111028..d11bd3da95 --- a/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java +++ b/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java @@@ -26,7 -29,6 +29,8 @@@ import java.util.Date import java.util.List; import com.google.common.base.Stopwatch; + ++import org.apache.lucene.index.CorruptIndexException; import org.junit.After; import org.junit.BeforeClass; import org.junit.Test; @@@ -87,6 -94,147 +95,147 @@@ public class SegmentFlushTes SegmentBuilder.updateLastValidSegmentRowId(-1); // reset } + @Test + public void multiSegmentBalancedTreePassesChecksumValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); - ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance); ++ ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List<DecoratedKey> keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Will throw if checksum validation fails: + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + } + + @Test + public void multiSegmentTermsDataPassesChecksumValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); - ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "name", UTF8Type.instance); ++ ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "name", UTF8Type.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List<DecoratedKey> keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, UTF8Type.instance.decompose("a")), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, UTF8Type.instance.decompose("b")), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Will throw if checksum validation fails: + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + } + + @Test + public void multiSegmentBalancedTreePassesHeaderFooterValidation() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); - ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance); ++ ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List<DecoratedKey> keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.HEADER_FOOTER, false, true); + } + + @Test + public void multiSegmentBalancedTreeFailsChecksumOnFirstSegmentByteFlip() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); - ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance); ++ ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List<DecoratedKey> keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Locate segment 0's payload extent so the flip lands inside it. Corrupting the FIRST + // (not last) segment specifically proves the validator inspects every segment -- a + // validator that only checked the trailing footer would miss this and silently pass. + MetadataSource source = MetadataSource.loadColumnMetadata(indexDescriptor, index.identifier()); + List<SegmentMetadata> segments = SegmentMetadata.load(source, indexDescriptor.primaryKeyFactory); + assertEquals(2, segments.size()); + SegmentMetadata.ComponentMetadata cm = segments.get(0).componentMetadatas.get(IndexComponent.BALANCED_TREE); + long flipPosition = cm.offset + cm.length / 2; + + File balancedTree = indexDescriptor.fileFor(IndexComponent.BALANCED_TREE, index.identifier()); + try (RandomAccessFile raf = new RandomAccessFile(balancedTree.toJavaIOFile(), "rw")) + { + raf.seek(flipPosition); + int original = raf.readByte(); + raf.seek(flipPosition); + raf.writeByte(original ^ 0xFF); + } + + try + { + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + fail("Expected corrupted first segment to fail checksum validation"); + } + catch (UncheckedIOException expected) + { + assertTrue("Expected CorruptIndexException cause; got " + expected.getCause(), expected.getCause() instanceof CorruptIndexException); + } + } + + @Test + public void multiSegmentBalancedTreeFailsChecksumOnAppendedGarbage() throws IOException + { + Path tmpDir = Files.createTempDirectory("SegmentFlushTest"); + IndexDescriptor indexDescriptor = IndexDescriptor.create(new Descriptor(new File(tmpDir.toFile()), "ks", "cf", new SequenceBasedSSTableId(1)), Murmur3Partitioner.instance, SAITester.EMPTY_COMPARATOR); - ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance); ++ ColumnMetadata column = ColumnMetadata.regularColumn("sai", "internal", "ts", TimestampType.instance, 1); + StorageAttachedIndex index = SAITester.createMockIndex(column); + + SSTableIndexWriter writer = new SSTableIndexWriter(indexDescriptor, index, V1OnDiskFormat.SEGMENT_BUILD_MEMORY_LIMITER, () -> true); + + List<DecoratedKey> keys = Arrays.asList(dk("1"), dk("2")); + Collections.sort(keys); + + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(0)), createRow(column, TimestampType.instance.decompose(new Date(1_000L))), 0L); + writer.addRow(SAITester.TEST_FACTORY.create(keys.get(1)), createRow(column, TimestampType.instance.decompose(new Date(2_000L))), SegmentBuilder.LAST_VALID_SEGMENT_ROW_ID + 1); + writer.complete(Stopwatch.createStarted()); + + // Append 100 random bytes past the last segment's footer. The per-segment slice loop + // walks each segment's declared frame, then asserts that frameStart == input.length() + // once done. This corruption trips that post-loop invariant, not a per-segment CRC. + SAITester.CorruptionType.APPENDED_DATA.corrupt(indexDescriptor.fileFor(IndexComponent.BALANCED_TREE, index.identifier())); + + try + { + indexDescriptor.validatePerIndexComponents(index.termType(), index.identifier(), IndexValidation.CHECKSUM, true, true); + fail("Expected trailing garbage to fail checksum validation"); + } + catch (UncheckedIOException expected) + { + assertTrue("Expected CorruptIndexException cause; got " + expected.getCause(), expected.getCause() instanceof CorruptIndexException); + } + } + @Test public void testFlushBetweenRowIds() throws Exception { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
