You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ma...@apache.org on 2026/10/21 13:47:15 UTC
(cassandra) 01/01: Merge branch 'cassandra-4.1' into cassandra-5.0
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
commit d020df9ce3c48c709fedf8f1400996d3f194beac
Merge: 06468fb06b 2bebf8f0b8
Author: Marcus Eriksson <ma...@apache.org>
AuthorDate: Wed Oct 21 15:28:47 2026 +0200
Merge branch 'cassandra-4.1' into cassandra-5.0
CHANGES.txt | 1 +
conf/cassandra.yaml | 8 ++
src/java/org/apache/cassandra/config/Config.java | 5 +
.../cassandra/config/DatabaseDescriptor.java | 10 ++
.../CassandraEntireSSTableStreamReader.java | 39 ++++++
.../cassandra/io/util/DataIntegrityMetadata.java | 10 +-
.../apache/cassandra/metrics/StreamingMetrics.java | 8 ++
.../apache/cassandra/streaming/StreamSession.java | 5 +
.../test/hostreplacement/HostReplacementTest.java | 83 +++++++++++++
.../CassandraEntireSSTableStreamWriterTest.java | 135 +++++++++++++++++++++
.../io/util/DataIntegrityMetadataTest.java | 72 +++++++++++
11 files changed, 375 insertions(+), 1 deletion(-)
diff --cc CHANGES.txt
index ea45e19dfd,c6dbd85e4f..8d2d52ecc9
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@@ -1,23 -1,6 +1,24 @@@
-4.1.13
+5.0.10
+ * Replica filtering protection resurrects rows deleted by a range tombstone that spans pages (CASSANDRA-21781)
+ * Write an sstable with the compression its components were chosen for when ALTER TABLE changes it (CASSANDRA-21780)
+ * Clarify the unset, true and false behaviours of cassandra.reset_bootstrap_progress in the abort message and docs (CASSANDRA-21595)
+ * Isolate SAI index rebuilds from ZCS (CASSANDRA-21520)
+ * Bound and chunk the BTI partition index preload (CASSANDRA-21632)
+ * Restore volatile on CommitLogSegment.cdcState dropped in merge (CASSANDRA-21665)
+ * Fix compressed scan read-ahead buffer Block leak and chunk-cache pollution by scans (CASSANDRA-21671)
+ * A read on a replica that does not know the index yet fails with a NullPointerException (CASSANDRA-21658)
+ * Release hints write buffer when the write executor shuts down (CASSANDRA-21657)
+ * Fix concurrent SAI vector inserts failing once jvector's per-graph pool limit is exceeded (CASSANDRA-21644)
+ * Let the commit log allocator observe the shutdown state (CASSANDRA-21616)
+ * Unwrap LongType properly when calculating min/max terms in V1SSTableIndex (CASSANDRA-21635)
+ * Force repair should ignore min_repair_interval (CASSANDRA-21552)
+ * Render SubnetGroups as JSON in system_views.settings (CASSANDRA-21579)
+ * Avoid rebuilding per-SSTable SAI components unless missing or corrupted (CASSANDRA-21515)
+ * Propagate trickle_fsync settings to compressed SSTable writers (CASSANDRA-21487)
+ * Allow DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(0) to disable read-ahead buffer (CASSANDRA-21522)
+ * Return CorruptSSTableException if chunk metadata and file size are out of sync (CASSANDRA-21519)
Merged from 4.0:
+ * Validate Digest.crc32 when zero-copy streaming SSTables (CASSANDRA-21694)
* Avoid DNS lookup for hostname column in ClientsTable and GossipInfoTable (CASSANDRA-21539)
* Separate memtable memory usage tracking from limit checking, and fixes a a flush writeBarrier deadlock (CASSANDRA-21019)
* Fixes for fully-expired SSTable index notification in compaction (CASSANDRA-21577)
diff --cc src/java/org/apache/cassandra/config/Config.java
index 87cc553176,a59b0ad8e1..dca5f03a4e
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@@ -698,7 -661,11 +698,12 @@@ public class Confi
public volatile int max_concurrent_automatic_sstable_upgrades = 1;
public boolean stream_entire_sstables = true;
+ /**
+ * When receiving a zero-copy stream, validate Digest.crc32 before the sstable is made visible.
+ */
+ public volatile boolean entire_sstable_stream_digest_validation_enabled = false;
+
+ public volatile boolean skip_stream_disk_space_check = false;
public volatile AuditLogOptions audit_logging_options = new AuditLogOptions();
public volatile FullQueryLoggerOptions full_query_logging_options = new FullQueryLoggerOptions();
diff --cc src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index c4fcc4ebef,6c20225b8d..d8e428b643
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@@ -4019,26 -3537,16 +4019,36 @@@ public class DatabaseDescripto
return conf.stream_entire_sstables;
}
+ @VisibleForTesting
+ public static boolean setStreamEntireSSTables(boolean value)
+ {
+ return conf.stream_entire_sstables = value;
+ }
+
+ public static boolean getEntireSSTableStreamDigestValidationEnabled()
+ {
+ return conf.entire_sstable_stream_digest_validation_enabled;
+ }
+
+ public static void setEntireSSTableStreamDigestValidationEnabled(boolean value)
+ {
+ conf.entire_sstable_stream_digest_validation_enabled = value;
+ }
+
+ public static DurationSpec.LongMillisecondsBound getStreamTransferTaskTimeout()
+ {
+ return conf.stream_transfer_task_timeout;
+ }
+
+ public static boolean getSkipStreamDiskSpaceCheck()
+ {
+ return conf.skip_stream_disk_space_check;
+ }
+
+ public static void setSkipStreamDiskSpaceCheck(boolean value)
+ {
+ conf.skip_stream_disk_space_check = value;
+ }
public static String getLocalDataCenter()
{
diff --cc src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
index 97c3b2d4f9,c876926158..81a82c86cf
--- a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
+++ b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
@@@ -29,16 -31,15 +31,18 @@@ import org.apache.cassandra.config.Data
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
+import org.apache.cassandra.io.compress.BufferType;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.Descriptor;
+import org.apache.cassandra.io.sstable.IOOptions;
import org.apache.cassandra.io.sstable.SSTableMultiWriter;
-import org.apache.cassandra.io.sstable.format.SSTableFormat;
-import org.apache.cassandra.io.sstable.format.big.BigTableZeroCopyWriter;
+import org.apache.cassandra.io.sstable.SSTableZeroCopyWriter;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
import org.apache.cassandra.io.util.DataInputPlus;
+ import org.apache.cassandra.io.util.DataIntegrityMetadata;
import org.apache.cassandra.io.util.File;
+import org.apache.cassandra.io.util.SequentialWriterOption;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.streaming.ProgressInfo;
import org.apache.cassandra.streaming.StreamReceiver;
@@@ -153,6 -157,37 +160,38 @@@ public class CassandraEntireSSTableStre
}
}
+ /**
+ * Validates the received {@code Data.db} against {@code Digest.crc32}.
+ * @throws IOException if a digest was streamed and the received data file does not match it
+ */
+ private void validateDataDigest(Descriptor descriptor, ComponentManifest manifest) throws IOException
+ {
+ if (!DatabaseDescriptor.getEntireSSTableStreamDigestValidationEnabled())
+ return;
+
+ List<Component> components = manifest.components();
- if (!components.contains(Component.DATA) || !components.contains(Component.DIGEST))
++ if (!components.contains(Components.DATA) || !components.contains(Components.DIGEST))
+ {
+ NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, TimeUnit.MINUTES,
+ "Accepting zero-copy sstable stream from {} without validating CRC due to " +
+ "missing {} or {} from streamed components: {}",
- session.peer, Component.DATA, Component.DIGEST, components);
++ session.peer, Components.DATA, Components.DIGEST, components);
+ return;
+ }
+
- try (DataIntegrityMetadata.FileDigestValidator validator = new DataIntegrityMetadata.FileDigestValidator(descriptor))
++ try
+ {
- validator.validate();
++ new DataIntegrityMetadata.FileDigestValidator(descriptor.fileFor(Components.DATA),
++ descriptor.fileFor(Components.DIGEST)).validate();
+ }
+ catch (IOException e)
+ {
+ session.countEntireSSTableDigestMismatch();
+ throw new IOException(format("[Stream #%s] Digest mismatch for %s received from %s, aborting stream",
+ session.planId(), descriptor, session.peer), e);
+ }
+ }
+
private File getDataDir(ColumnFamilyStore cfs, long totalSize) throws IOException
{
Directories.DataDirectory localDir = cfs.getDirectories().getWriteableLocation(totalSize);
diff --cc src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
index aef3614da6,65d4e58f0f..7805a87f34
--- a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
+++ b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
@@@ -103,17 -142,21 +103,25 @@@ public class DataIntegrityMetadat
// Validate the entire file
public void validate() throws IOException
{
- CheckedInputStream checkedInputStream = new CheckedInputStream(dataReader, checksum);
- byte[] chunk = new byte[64 * 1024];
-
- while( checkedInputStream.read(chunk) > 0 ) { }
- long calculatedDigestValue = checkedInputStream.getChecksum().getValue();
- if (storedDigestValue != calculatedDigestValue)
+ try (RandomAccessReader digestReader = RandomAccessReader.open(digestFile);
+ RandomAccessReader dataReader = RandomAccessReader.open(dataFile);
+ CheckedInputStream checkedInputStream = new CheckedInputStream(dataReader, checksum);)
{
- long storedDigestValue = Long.parseLong(digestReader.readLine());
- throw new IOException("Corrupted SSTable : " + descriptor.filenameFor(Component.DATA));
++ long storedDigestValue;
++ try
++ {
++ storedDigestValue = Long.parseLong(digestReader.readLine());
++ }
++ catch (NumberFormatException e)
++ {
++ throw new IOException(String.format("Corrupted file: invalid digest value in %s", digestFile), e);
++ }
+ byte[] chunk = new byte[64 * 1024];
+ while (checkedInputStream.read(chunk) > 0) ;
+ long calculatedDigestValue = checkedInputStream.getChecksum().getValue();
+ if (storedDigestValue != calculatedDigestValue)
+ throw new IOException(String.format("Corrupted file: integrity check (digest) failed for %s: %d != %d", dataFile, storedDigestValue, calculatedDigestValue));
}
}
-
- public void close()
- {
- Throwables.perform(digestReader::close,
- dataReader::close);
- }
}
}
diff --cc test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
index 8219d43ad1,fa95a8aebf..c36e83ce16
--- a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
@@@ -35,8 -39,9 +39,10 @@@ import org.apache.cassandra.distributed
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.distributed.api.TokenSupplier;
import org.apache.cassandra.distributed.shared.AssertUtils;
+import org.apache.cassandra.distributed.shared.ClusterUtils;
import org.apache.cassandra.distributed.test.TestBaseImpl;
-import org.apache.cassandra.io.sstable.Component;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
+ import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.assertj.core.api.Assertions;
import static org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK;
@@@ -206,6 -211,83 +212,83 @@@ public class HostReplacementTest extend
}
}
+ private static final int HEALTHY_PK = 10;
+ private static final int CORRUPT_PK = 11;
+
+ /**
+ * With digest validation enabled, a single corrupted sstable must fail the entire stream session, so neither
+ * the corrupted sstable nor its healthy sibling should land on the replacing node.
+ */
+ @Test
+ public void replaceHostAbortsStreamingWhenDigestValidationCatchesCorruption() throws IOException, TimeoutException
+ {
+ TokenSupplier even = TokenSupplier.evenlyDistributedTokens(2);
+ try (Cluster cluster = Cluster.build(2)
+ .withConfig(c -> c.with(Feature.GOSSIP, Feature.NETWORK)
+ .set("entire_sstable_stream_digest_validation_enabled", true))
+ .withTokenSupplier(node -> even.token(node == 3 ? 2 : node))
+ .start())
+ {
+ IInvokableInstance node1 = cluster.get(1);
+ IInvokableInstance nodeToRemove = cluster.get(2);
+
+ setupCluster(cluster);
+ writeHealthyAndCorruptSSTables(cluster, node1);
+
+ stopUnchecked(nodeToRemove);
+
+ IInvokableInstance replacingNode = replaceHostAndStart(cluster, nodeToRemove);
+
+ replacingNode.logs().watchFor("Digest mismatch");
+
+ // the whole stream session is aborted on a digest mismatch, so neither sstable should have landed
+ AssertUtils.assertRows(replacingNode.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = " + HEALTHY_PK), EMPTY_ROWS);
+ AssertUtils.assertRows(replacingNode.executeInternal("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = " + CORRUPT_PK), EMPTY_ROWS);
+ }
+ }
+
+ /**
+ * Flushes two separate sstables on {@code node1} for {@code KEYSPACE.tbl}: one containing {@link #HEALTHY_PK}
+ * left untouched, and one containing {@link #CORRUPT_PK} that gets its Data.db corrupted.
+ */
+ static void writeHealthyAndCorruptSSTables(Cluster cluster, IInvokableInstance node1) throws IOException
+ {
+ node1.nodetoolResult("disableautocompaction").asserts().success();
+
+ cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk) VALUES (?)",
+ ConsistencyLevel.ALL,
+ HEALTHY_PK);
+ node1.flush(KEYSPACE);
+
+ cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk) VALUES (?)",
+ ConsistencyLevel.ALL,
+ CORRUPT_PK);
+ node1.flush(KEYSPACE);
+
+ corruptSSTable(node1, "tbl");
+ }
+
+ /** Flips a byte in the middle of the Data.db file of the most recently flushed sstable for the given table. */
+ static void corruptSSTable(IInvokableInstance instance, String table) throws IOException
+ {
+ String dataFilePath = instance.callOnInstance(() -> {
+ ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(table);
+ SSTableReader sstable = cfs.getLiveSSTables().stream()
+ .max(SSTableReader.maxTimestampAscending)
- .get();
- return sstable.descriptor.fileFor(Component.DATA).absolutePath();
++ .orElseThrow();
++ return sstable.descriptor.fileFor(Components.DATA).absolutePath();
+ });
+
+ try (RandomAccessFile raf = new RandomAccessFile(dataFilePath, "rw"))
+ {
+ long corruptAt = raf.length() / 2;
+ raf.seek(corruptAt);
+ int b = raf.read();
+ raf.seek(corruptAt);
+ raf.write(b ^ 0xFF);
+ }
+ }
+
static void setupCluster(Cluster cluster)
{
fixDistributedSchemas(cluster);
diff --cc test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
index 89a3e9ed36,5184b5ae3c..dcafae0058
--- a/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
+++ b/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
@@@ -22,8 -23,15 +23,10 @@@ import java.nio.ByteBuffer
import java.nio.channels.WritableByteChannel;
import java.util.Collection;
import java.util.Collections;
+ import java.util.LinkedHashMap;
import java.util.Queue;
-import com.google.common.io.ByteStreams;
-
-import org.apache.cassandra.Util;
-import org.apache.cassandra.io.sstable.Component;
-import org.apache.cassandra.io.sstable.Descriptor;
+ import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
@@@ -35,13 -45,11 +38,16 @@@ import io.netty.channel.ChannelPromise
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.embedded.EmbeddedChannel;
import org.apache.cassandra.SchemaLoader;
+import org.apache.cassandra.Util;
++import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.RowUpdateBuilder;
import org.apache.cassandra.db.compaction.CompactionManager;
++import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableMultiWriter;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.locator.InetAddressAndPort;
@@@ -169,6 -189,125 +186,124 @@@ public class CassandraEntireSSTableStre
}
}
+ @Test
+ public void testCorruptDataComponentFailsDigestValidation() throws Throwable
+ {
+ ByteBuf serializedFile = Unpooled.buffer(8192);
+ ComponentManifest manifest = writeEntireSSTable(serializedFile);
+ corruptDataComponent(serializedFile, manifest);
+
+ DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(true);
+ assertThatThrownBy(() -> readEntireSSTable(serializedFile, manifest))
+ .isInstanceOf(IOException.class)
+ .hasMessageContaining("Digest mismatch")
+ .hasCauseInstanceOf(IOException.class);
+ }
+
+ @Test
+ public void testCorruptDataComponentAcceptedWhenValidationDisabled() throws Throwable
+ {
+ ByteBuf serializedFile = Unpooled.buffer(8192);
+ ComponentManifest manifest = writeEntireSSTable(serializedFile);
+ corruptDataComponent(serializedFile, manifest);
+
+ DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(false);
+
+ // shouldn't throw an error
+ SSTableMultiWriter writer = readEntireSSTable(serializedFile, manifest);
+ assertNotNull(writer);
+ writer.abort(null);
+ }
+
+ @Test
+ public void testMissingDigestIsAcceptedWithoutValidation() throws Throwable
+ {
+ DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(true);
+
+ ComponentManifest full;
- try (ComponentContext context = ComponentContext.create(descriptor))
++ try (ComponentContext context = ComponentContext.create(sstable))
+ {
+ full = context.manifest();
+ }
+
+ // remove digest component from sstable
+ LinkedHashMap<Component, Long> withoutDigest = new LinkedHashMap<>();
+ for (Component component : full.components())
+ {
- if (!component.equals(Component.DIGEST))
++ if (!component.equals(Components.DIGEST))
+ withoutDigest.put(component, full.sizeOf(component));
+ }
+ ComponentManifest manifest = new ComponentManifest(withoutDigest);
+
+ // stream from live files
+ ByteBuf serializedFile = Unpooled.buffer(8192);
+ for (Component component : manifest.components())
+ {
+ try (InputStream in = descriptor.fileFor(component).newInputStream())
+ {
- serializedFile.writeBytes(ByteStreams.toByteArray(in));
++ serializedFile.writeBytes(in.readAllBytes());
+ }
+ }
+
+ // shouldn't throw an error
+ SSTableMultiWriter writer = readEntireSSTable(serializedFile, manifest);
+ assertNotNull(writer);
+ writer.abort(null);
+ }
+
+ private ComponentManifest writeEntireSSTable(ByteBuf serializedFile) throws Exception
+ {
+ StreamSession session = setupStreamingSessionForTest();
+ EmbeddedChannel channel = createMockNettyChannel(serializedFile);
+ try (AsyncStreamingOutputPlus out = new AsyncStreamingOutputPlus(channel);
- ComponentContext context = ComponentContext.create(descriptor))
++ ComponentContext context = ComponentContext.create(sstable))
+ {
+ new CassandraEntireSSTableStreamWriter(sstable, session, context).write(out);
+ return context.manifest();
+ }
+ }
+
+ private SSTableMultiWriter readEntireSSTable(ByteBuf serializedFile, ComponentManifest manifest) throws Throwable
+ {
+ StreamSession session = setupStreamingSessionForTest();
+ InetAddressAndPort peer = FBUtilities.getBroadcastAddressAndPort();
+ session.prepareReceiving(new StreamSummary(sstable.metadata().id, 1, 5104));
+
+ CassandraStreamHeader header =
+ CassandraStreamHeader.builder()
- .withSSTableFormat(sstable.descriptor.formatType)
+ .withSSTableVersion(sstable.descriptor.version)
+ .withSSTableLevel(0)
+ .withEstimatedKeys(sstable.estimatedKeys())
+ .withSections(Collections.emptyList())
+ .withSerializationHeader(sstable.header.toComponent())
+ .withComponentManifest(manifest)
+ .isEntireSSTable(true)
- .withFirstKey(sstable.first)
++ .withFirstKey(sstable.getFirst())
+ .withTableId(sstable.metadata().id)
+ .build();
+
+ CassandraEntireSSTableStreamReader reader =
+ new CassandraEntireSSTableStreamReader(new StreamMessageHeader(sstable.metadata().id, peer, session.planId(), false, 0, 0, 0, null),
+ header, session);
+
+ return reader.read(new DataInputBuffer(serializedFile.nioBuffer(), false));
+ }
+
+ /** Flips every bit of one byte in the middle of the Data.db region of an already-serialized stream. */
+ private static void corruptDataComponent(ByteBuf serializedFile, ComponentManifest manifest)
+ {
+ long dataOffset = 0;
+ for (Component component : manifest.components())
+ {
- if (component.equals(Component.DATA))
++ if (component.equals(Components.DATA))
+ break;
+ dataOffset += manifest.sizeOf(component);
+ }
+
- int corruptAt = (int) (dataOffset + manifest.sizeOf(Component.DATA) / 2);
++ int corruptAt = (int) (dataOffset + manifest.sizeOf(Components.DATA) / 2);
+ serializedFile.setByte(corruptAt, serializedFile.getByte(corruptAt) ^ 0xff);
+ }
+
private EmbeddedChannel createMockNettyChannel(ByteBuf serializedFile) throws Exception
{
WritableByteChannel wbc = new WritableByteChannel()
diff --cc test/unit/org/apache/cassandra/io/util/DataIntegrityMetadataTest.java
index 0000000000,0000000000..9a5e2f3e23
new file mode 100644
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/util/DataIntegrityMetadataTest.java
@@@ -1,0 -1,0 +1,72 @@@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one
++ * or more contributor license agreements. See the NOTICE file
++ * distributed with this work for additional information
++ * regarding copyright ownership. The ASF licenses this file
++ * to you under the Apache License, Version 2.0 (the
++ * "License"); you may not use this file except in compliance
++ * with the License. You may obtain a copy of the License at
++ *
++ * http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++package org.apache.cassandra.io.util;
++
++import java.io.IOException;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++
++import org.junit.BeforeClass;
++import org.junit.Rule;
++import org.junit.Test;
++import org.junit.rules.TemporaryFolder;
++
++import org.apache.cassandra.config.DatabaseDescriptor;
++
++import static org.assertj.core.api.Assertions.assertThatThrownBy;
++import static org.junit.Assert.assertEquals;
++
++public class DataIntegrityMetadataTest
++{
++ @BeforeClass
++ public static void setupDD()
++ {
++ DatabaseDescriptor.daemonInitialization();
++ }
++
++ @Rule
++ public TemporaryFolder tempDir = new TemporaryFolder();
++
++ @Test
++ public void testNonNumericDigestValueFailsWithClearError() throws IOException
++ {
++ assertInvalidDigestValueFailsWithClearError("not-a-number");
++ }
++
++ @Test
++ public void testEmptyDigestFileFailsWithClearError() throws IOException
++ {
++ assertInvalidDigestValueFailsWithClearError("");
++ }
++
++ private void assertInvalidDigestValueFailsWithClearError(String digestContent) throws IOException
++ {
++ File dataFile = new File(tempDir.newFile("data.db"));
++ Files.write(dataFile.toPath(), "some bytes".getBytes(StandardCharsets.UTF_8));
++
++ File digestFile = new File(tempDir.newFile("data.crc32"));
++ Files.write(digestFile.toPath(), digestContent.getBytes(StandardCharsets.UTF_8));
++ assertEquals("digest file fixture doesn't contain what this test expects",
++ digestContent.getBytes(StandardCharsets.UTF_8).length, digestFile.length());
++
++ assertThatThrownBy(() -> new DataIntegrityMetadata.FileDigestValidator(dataFile, digestFile).validate())
++ .isInstanceOf(IOException.class)
++ .hasMessageContaining("Corrupted file: invalid digest value in " + digestFile)
++ .hasCauseInstanceOf(NumberFormatException.class);
++ }
++}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org