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]

Reply via email to