This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch cassandra-5.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/cassandra-5.0 by this push:
new 4bd98de6ee SAI Component Checksum Validation Should be Segment-Aware
4bd98de6ee is described below
commit 4bd98de6eeaebdaa4dbbadf4570c2ded4cb01067
Author: Caleb Rackliffe <[email protected]>
AuthorDate: Tue Jul 14 12:33:05 2026 -0500
SAI Component Checksum Validation Should be Segment-Aware
patch by Caleb Rackliffe; reviewed by Francisco Guerrero for CASSANDRA-21516
---
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 --git a/CHANGES.txt b/CHANGES.txt
index 77745b48f7..8acee65e3d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.0.9
+ * 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)
* putShortVolatile is not volatile in InMemoryTrie (CASSANDRA-21353)
diff --git
a/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java
b/src/java/org/apache/cassandra/index/sai/disk/io/IndexInputReader.java
index b97c727c6a..7cfef29943 100644
--- 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,8 @@ package org.apache.cassandra.index.sai.disk.io;
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;
@@ -29,10 +31,13 @@ import org.apache.lucene.store.IndexInput;
* This is a wrapper over a Cassandra {@link RandomAccessReader} that provides
an {@link IndexInput}
* interface for Lucene classes that need {@link IndexInput}. This is an
optimisation because the
* Lucene {@link DataInput} reads bytes one at a time whereas the {@link
RandomAccessReader} is
- * optimised to read multibyte objects faster.
+ * optimized to read multibyte objects faster.
*/
+@NotThreadSafe
public class IndexInputReader extends IndexInput
{
+ public static final Runnable NO_OP_ON_CLOSE = () -> {};
+
/**
* the byte order of `input`'s native readX operations doesn't matter,
* because we only use `readFully` and `readByte` methods. IndexInput
calls these
@@ -41,27 +46,47 @@ public class IndexInputReader extends IndexInput
private final RandomAccessReader input;
private final Runnable doOnClose;
- private IndexInputReader(RandomAccessReader input, Runnable doOnClose)
+ /** Absolute offset in the underlying file that this input's position 0
refers to. */
+ private final long offset;
+
+ /** Bounded length of this input, in bytes. */
+ private final long length;
+
+ private IndexInputReader(RandomAccessReader input, Runnable doOnClose,
long offset, long length)
{
super(input.getPath());
this.input = input;
this.doOnClose = doOnClose;
+ this.offset = offset;
+ this.length = length;
}
public static IndexInputReader create(RandomAccessReader input)
{
- return new IndexInputReader(input, () -> {});
+ // Top-level inputs own the underlying reader; folding its close into
doOnClose lets us
+ // avoid a separate ownership flag on the class.
+ return new IndexInputReader(input, input::close, 0L, input.length());
}
public static IndexInputReader create(RandomAccessReader input, Runnable
doOnClose)
{
- return new IndexInputReader(input, doOnClose);
+ Runnable close = () -> {
+ try
+ {
+ input.close();
+ }
+ finally
+ {
+ doOnClose.run();
+ }
+ };
+ return new IndexInputReader(input, close, 0L, input.length());
}
public static IndexInputReader create(FileHandle handle)
{
RandomAccessReader reader = handle.createReader();
- return new IndexInputReader(reader, () -> {});
+ return new IndexInputReader(reader, reader::close, 0L,
reader.length());
}
@Override
@@ -79,37 +104,42 @@ public class IndexInputReader extends IndexInput
@Override
public void close()
{
- try
- {
- input.close();
- }
- finally
- {
- doOnClose.run();
- }
+ doOnClose.run();
}
@Override
public long getFilePointer()
{
- return input.getFilePointer();
+ return input.getFilePointer() - offset;
}
@Override
public void seek(long position)
{
- input.seek(position);
+ if (position > length)
+ throw new IllegalArgumentException("Cannot seek to position " +
position + " past length of " + length);
+
+ input.seek(offset + position);
}
@Override
public long length()
{
- return input.length();
+ return length;
}
@Override
public IndexInput slice(String sliceDescription, long offset, long length)
{
- throw new UnsupportedOperationException("Slice operations are not
supported");
+ if (offset < 0 || length < 0 || offset + length > this.length)
+ throw new IllegalArgumentException("Invalid slice: offset=" +
offset + ", length=" + length + ", parent length=" + this.length + " for " +
sliceDescription);
+
+ // Slices share the underlying reader with their parent; the no-op
close keeps the parent's lifecycle intact.
+ IndexInputReader slice = new IndexInputReader(input, NO_OP_ON_CLOSE,
this.offset + offset, length);
+
+ // Seek to the beginning of the slice...
+ slice.seek(0);
+
+ return slice;
}
}
diff --git
a/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java
b/src/java/org/apache/cassandra/index/sai/disk/v1/V1OnDiskFormat.java
index 8d8266ac34..a77fad1888 100644
--- 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,6 +21,7 @@ package org.apache.cassandra.index.sai.disk.v1;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.EnumSet;
+import java.util.List;
import java.util.Set;
import com.google.common.annotations.VisibleForTesting;
@@ -42,6 +43,7 @@ import
org.apache.cassandra.index.sai.disk.format.IndexComponent;
import org.apache.cassandra.index.sai.disk.format.IndexDescriptor;
import org.apache.cassandra.index.sai.disk.format.OnDiskFormat;
import org.apache.cassandra.index.sai.disk.v1.segment.SegmentBuilder;
+import org.apache.cassandra.index.sai.disk.v1.segment.SegmentMetadata;
import org.apache.cassandra.index.sai.metrics.AbstractMetrics;
import org.apache.cassandra.index.sai.utils.IndexIdentifier;
import org.apache.cassandra.index.sai.utils.IndexTermType;
@@ -50,6 +52,8 @@ import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.DefaultNameFactory;
import org.apache.cassandra.utils.Throwables;
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.store.IndexInput;
import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory;
@@ -95,6 +99,18 @@ public class V1OnDiskFormat implements OnDiskFormat
IndexComponent.TERMS_DATA,
IndexComponent.POSTING_LISTS);
+ /**
+ * Per-column components whose files are written in append mode with one
SAI codec footer
+ * per segment (see {@link
org.apache.cassandra.index.sai.disk.v1.bbtree.NumericIndexWriter},
+ * {@link
org.apache.cassandra.index.sai.disk.v1.trie.TrieTermsDictionaryWriter},
+ * {@link org.apache.cassandra.index.sai.disk.v1.postings.PostingsWriter},
and
+ * {@link org.apache.cassandra.index.sai.disk.v1.vector.OnHeapGraph}).
+ */
+ private static final Set<IndexComponent> SEGMENTED_COMPONENTS =
EnumSet.of(IndexComponent.BALANCED_TREE,
+
IndexComponent.POSTING_LISTS,
+
IndexComponent.TERMS_DATA,
+
IndexComponent.COMPRESSED_VECTORS);
+
/**
* Global limit on heap consumed by all index segment building that occurs
outside the context of Memtable flush.
* <p>
@@ -218,12 +234,87 @@ public class V1OnDiskFormat implements OnDiskFormat
}
}
+ if (isEmptyIndex)
+ return;
+
+ // Safely read the segment metadata so we can validate per-segment
checksums below...
+ List<SegmentMetadata> segments = null;
+ if (checksum)
+ {
+ validateIndexComponent(indexDescriptor, indexIdentifier,
IndexComponent.META, true);
+ try
+ {
+ segments =
SegmentMetadata.load(MetadataSource.loadColumnMetadata(indexDescriptor,
indexIdentifier), indexDescriptor.primaryKeyFactory);
+ }
+ catch (IOException e)
+ {
+ rethrowIOException(e);
+ }
+ }
+
for (IndexComponent indexComponent :
perColumnIndexComponents(indexTermType))
{
- if (!isEmptyIndex && isNotBuildCompletionMarker(indexComponent))
+ if (isNotBuildCompletionMarker(indexComponent))
+ {
+ // META was validated up-front in CHECKSUM mode; don't
validate it twice.
+ if (checksum && indexComponent == IndexComponent.META)
+ continue;
+
+ if (checksum && SEGMENTED_COMPONENTS.contains(indexComponent))
+ {
+ assert segments != null : "No segment metadata available!";
+ validateSegmentedIndexComponent(indexDescriptor,
indexIdentifier, indexComponent, segments, indexTermType.isVector());
+ }
+ else
+ validateIndexComponent(indexDescriptor, indexIdentifier,
indexComponent, checksum);
+ }
+ }
+ }
+
+ private static void validateSegmentedIndexComponent(IndexDescriptor
indexDescriptor,
+ IndexIdentifier
indexIdentifier,
+ IndexComponent
indexComponent,
+ List<SegmentMetadata>
segments,
+ boolean
payloadOnlyMetadata)
+ {
+ try (IndexInput input =
indexDescriptor.openPerIndexInput(indexComponent, indexIdentifier))
+ {
+ long fileLength = input.length();
+ long frameStart = 0;
+
+ for (SegmentMetadata segment : segments)
{
- validateIndexComponent(indexDescriptor, indexIdentifier,
indexComponent, checksum);
+ SegmentMetadata.ComponentMetadata cm =
segment.componentMetadatas.get(indexComponent);
+
+ // Non-vector writers record offsets as the codec-framed
segment starts (before
+ // the header) and length as the full framed length (through
the footer). The vector
+ // writer (OnHeapGraph#writeData) instead records the offset
as the payload start (after
+ // the header) and length as just the payload length, because
vector readers seek
+ // directly at the payload. Segments are written contiguously
in append mode, so we can
+ // recover the vector-path frame extent by walking segment
ends and adding the trailing
+ // 16-byte codec footer.
+ long frameEnd = payloadOnlyMetadata ? cm.offset + cm.length +
CodecUtil.footerLength() : cm.offset + cm.length;
+
+ if (frameEnd > fileLength || frameEnd < frameStart)
+ throw new CorruptIndexException(String.format("Segment
frame [%d, %d) is inconsistent with component file length %d",
+ frameStart,
frameEnd, fileLength),
+ indexComponent.name + '@'
+ frameStart);
+
+ IndexInput slice = input.slice(indexComponent.name + '@' +
frameStart, frameStart, frameEnd - frameStart);
+ SAICodecUtils.validateChecksum(slice);
+ frameStart = frameEnd;
}
+
+ if (frameStart != fileLength)
+ throw new CorruptIndexException(String.format("Component file
length %d does not match combined frame length of all segments %d",
+ fileLength,
frameStart),
+ indexComponent.name);
+ }
+ catch (Exception e)
+ {
+ logger.warn(indexDescriptor.logMessage("Segmented checksum
validation failed for index component {} on SSTable {}"),
+ indexComponent, indexDescriptor.sstableDescriptor);
+ rethrowIOException(e);
}
}
@@ -244,9 +335,7 @@ public class V1OnDiskFormat implements OnDiskFormat
catch (Exception e)
{
logger.warn(indexDescriptor.logMessage("{} failed for index
component {} on SSTable {}"),
- checksum ? "Checksum validation" : "Validation",
- indexComponent,
- indexDescriptor.sstableDescriptor);
+ checksum ? "Checksum validation" : "Validation",
indexComponent, indexDescriptor.sstableDescriptor);
rethrowIOException(e);
}
}
diff --git
a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
index ae6e62ce12..d21331c1ab 100644
---
a/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
+++
b/test/unit/org/apache/cassandra/index/sai/cql/StorageAttachedIndexDDLTest.java
@@ -52,8 +52,10 @@ import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.db.marshal.FloatType;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.UTF8Type;
+import org.apache.cassandra.db.marshal.VectorType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.SecondaryIndexManager;
@@ -1428,6 +1430,27 @@ public class StorageAttachedIndexDDLTest extends
SAITester
assertEquals(Arrays.asList(2L, 1L), toSize.apply(iterator.next()));
}
+ @Test
+ public void multiSegmentVectorIndexPassesChecksumValidation()
+ {
+ createTable("CREATE TABLE %s (pk int, val vector<float, 3>, PRIMARY
KEY(pk))");
+
+ int vectorCount = 100;
+ for (int pk = 0; pk < vectorCount; pk++)
+ execute("INSERT INTO %s (pk, val) VALUES (" + pk + ", [" + pk +
".0, " + (pk + 1) + ".0, " + (pk + 2) + ".0])");
+
+ flush();
+
+ SegmentBuilder.updateLastValidSegmentRowId(17); // 17 rows per segment
-> multi-segment build
+ IndexIdentifier vectorIndexIdentifier =
createIndexIdentifier(createIndex("CREATE CUSTOM INDEX ON %s(val) USING
'StorageAttachedIndex'"));
+ IndexTermType vectorIndexTermType =
createIndexTermType(VectorType.getInstance(FloatType.instance, 3));
+
+ // A vector index writes CompressedVectors.db, TermsData.db, and
PostingLists.db in append
+ // mode with one SAI codec footer per segment (see
OnHeapGraph.writeData). Multi-segment
+ // builds therefore need segment-aware checksum validation.
+ assertTrue(verifyChecksum(vectorIndexTermType, vectorIndexIdentifier));
+ }
+
private void assertZeroSegmentBuilderUsage()
{
assertEquals("Segment memory limiter should revert to zero.", 0L,
getSegmentBufferUsedBytes());
diff --git
a/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java
b/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java
index f0d1eae642..e6c0111028 100644
--- a/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/disk/v1/SegmentFlushTest.java
@@ -18,11 +18,14 @@
package org.apache.cassandra.index.sai.disk.v1;
import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Date;
import java.util.List;
import com.google.common.base.Stopwatch;
@@ -33,11 +36,13 @@ import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.DecoratedKey;
+import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.rows.BTreeRow;
import org.apache.cassandra.db.rows.BufferCell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.dht.Murmur3Partitioner;
+import org.apache.cassandra.index.sai.IndexValidation;
import org.apache.cassandra.index.sai.SAITester;
import org.apache.cassandra.index.sai.StorageAttachedIndex;
import org.apache.cassandra.index.sai.disk.format.IndexComponent;
@@ -56,10 +61,13 @@ import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
import org.apache.cassandra.utils.bytecomparable.ByteSource;
+import org.apache.lucene.index.CorruptIndexException;
import static org.apache.cassandra.Util.dk;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public class SegmentFlushTest
{
@@ -86,6 +94,147 @@ public class SegmentFlushTest
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);
+ 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);
+ 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);
+ 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);
+ 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);
+ 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]