diff --git a/CHANGES.txt b/CHANGES.txt index 0d6e661f2e..6b239e6547 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,6 +5,7 @@ * IntrusiveStack.accumulate is not accumulating correctly (CASSANDRA-20670) * Add nodetool get/setguardrailsconfig commands (CASSANDRA-19552) Merged from 4.0: + * Fix IndexOutOfBoundsException in sstablemetadata tool when a range tombstone is a max clustering value (CASSANDRA-20855) * Update Jackson to 2.19.2 (CASSANDRA-20848) * Update commons-lang3 to 3.18.0 (CASSANDRA-20849) * Add NativeTransportMaxConcurrentConnectionsPerIp to StorageProxyMBean (CASSANDRA-20642) diff --git a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java index c4bef35bfd..c32f12d490 100644 --- a/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java +++ b/src/java/org/apache/cassandra/tools/SSTableMetadataViewer.java @@ -356,21 +356,9 @@ public class SSTableMetadataViewer if (validation != null && header != null) printMinMaxToken(descriptor, FBUtilities.newPartitioner(descriptor), header.getKeyType()); - if (header != null && header.getClusteringTypes().size() == stats.minClusteringValues.size()) - { - List> clusteringTypes = header.getClusteringTypes(); - List minClusteringValues = stats.minClusteringValues; - List maxClusteringValues = stats.maxClusteringValues; - String[] minValues = new String[clusteringTypes.size()]; - String[] maxValues = new String[clusteringTypes.size()]; - for (int i = 0; i < clusteringTypes.size(); i++) - { - minValues[i] = clusteringTypes.get(i).getString(minClusteringValues.get(i)); - maxValues[i] = clusteringTypes.get(i).getString(maxClusteringValues.get(i)); - } - field("minClusteringValues", Arrays.toString(minValues)); - field("maxClusteringValues", Arrays.toString(maxValues)); - } + printClusteringValues(header, "minClusteringValues", stats.minClusteringValues); + printClusteringValues(header, "maxClusteringValues", stats.maxClusteringValues); + field("Estimated droppable tombstones", stats.getEstimatedDroppableTombstoneRatio((int) (currentTimeMillis() / 1000) - this.gc)); field("SSTable Level", stats.sstableLevel); @@ -437,6 +425,21 @@ public class SSTableMetadataViewer } } + private void printClusteringValues(SerializationHeader.Component header, String name, List clusteringValues) + { + if (header != null && header.getClusteringTypes().size() >= clusteringValues.size()) + { + List> clusteringTypes = header.getClusteringTypes(); + int size = Math.min(clusteringTypes.size(), clusteringValues.size()); + String[] values = new String[size]; + for (int i = 0; i < size; i++) + { + values[i] = clusteringTypes.get(i).getString(clusteringValues.get(i)); + } + field(name, Arrays.toString(values)); + } + } + private void field(String field, Object value) { field(field, value, null); diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-CompressionInfo.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-CompressionInfo.db new file mode 100644 index 0000000000..7d7289b0c5 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-CompressionInfo.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Data.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Data.db new file mode 100644 index 0000000000..666fa7e012 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Data.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Digest.crc32 b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Digest.crc32 new file mode 100644 index 0000000000..441a80dd5f --- /dev/null +++ b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Digest.crc32 @@ -0,0 +1 @@ +3617993137 \ No newline at end of file diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Filter.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Filter.db new file mode 100644 index 0000000000..678c0aafe0 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Filter.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Index.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Index.db new file mode 100644 index 0000000000..d533e13df5 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Index.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Statistics.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Statistics.db new file mode 100644 index 0000000000..9f1f6998b8 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Statistics.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Summary.db b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Summary.db new file mode 100644 index 0000000000..b2c39da688 Binary files /dev/null and b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-Summary.db differ diff --git a/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-TOC.txt b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-TOC.txt new file mode 100644 index 0000000000..b5bdca06d3 --- /dev/null +++ b/test/data/sstable_metadata/CASSANDRA_20855/nb-1-big-TOC.txt @@ -0,0 +1,8 @@ +Data.db +TOC.txt +Digest.crc32 +Summary.db +Filter.db +Statistics.db +CompressionInfo.db +Index.db diff --git a/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java index baf7c8c1cf..7d740792dc 100644 --- a/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java +++ b/test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java @@ -18,16 +18,23 @@ package org.apache.cassandra.tools; +import java.io.File; import java.io.IOException; import java.util.Arrays; import com.google.common.base.CharMatcher; +import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; +import org.apache.cassandra.SchemaLoader; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.service.StorageService; import org.apache.cassandra.tools.ToolRunner.ToolResult; import org.assertj.core.api.Assertions; import org.hamcrest.CoreMatchers; @@ -225,4 +232,52 @@ public class SSTableMetadataViewerTest extends OfflineToolUtils assertKeyspaceNotLoaded(); assertServerNotLoaded(); } + + @Test + public void testLoadingOfSStableWithRangeTombstoneAsMaxClustering() throws IOException + { + String sstableWithTombstones = copySSTables("sstable_metadata", "CASSANDRA_20855"); + ToolResult tool = ToolRunner.invokeClass(SSTableMetadataViewer.class, sstableWithTombstones); + assertTrue(tool.getStdout(), CharMatcher.ascii().matchesAllOf(tool.getStdout())); + Assertions.assertThat(tool.getStdout()).contains(sstableWithTombstones.replaceAll("-Data\\.db$", "")); + Assertions.assertThat(tool.getStdout()).contains("minClusteringValues: [clust_key_1_1, clust_key_2_1]"); + // Range tombstone end as a max clustering value, only the first clustering key is stored in metadata + Assertions.assertThat(tool.getStdout()).contains("maxClusteringValues: [clust_key_1_2]"); + assertTrue(tool.getStderr(), tool.getStderr().isEmpty()); + assertEquals(0, tool.getExitCode()); + assertGoodEnvPostTest(); + } + + @Test + @Ignore + // the logic is used only to generate test data for testLoadingOfSStableWithRangeTombstoneAsMaxClustering + // the result of the generation is committed to SCM + public void generateSSTableWithRangeTombstone() throws IOException + { + SchemaLoader.prepareServer(); + StorageService.instance.initServer(); + Keyspace.setInitialized(); + QueryProcessor.executeInternal("CREATE KEYSPACE sstable_metadata WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}"); + QueryProcessor.executeInternal("CREATE TABLE sstable_metadata.CASSANDRA_20855 (pk text, ck_1 text, ck_2 text, val text, PRIMARY KEY (pk, ck_1, ck_2))"); + + QueryProcessor.executeInternal(String.format("INSERT INTO sstable_metadata.CASSANDRA_20855 (pk, ck_1, ck_2, val) VALUES ('%s', '%s', '%s', '%s')", + "part_key_1", "clust_key_1_1", "clust_key_2_1", "value")); + + QueryProcessor.executeInternal(String.format("DELETE FROM sstable_metadata.CASSANDRA_20855 WHERE pk = '%s' AND ck_1 = '%s'", + "part_key_1", "clust_key_1_2")); + + StorageService.instance.forceKeyspaceFlush("sstable_metadata"); + } + + + private static String copySSTables(String keyspace, String table) throws IOException + { + File targetKeyspaceDir = new File("build/test/cassandra/data/", keyspace); + targetKeyspaceDir.mkdirs(); + File targetTableDir = new File(targetKeyspaceDir, table); + File srcKeyspaceDir = new File("test/data/" + keyspace); + FileUtils.copyDirectory(new File(srcKeyspaceDir, table), targetTableDir); + File[] sstableFiles = targetTableDir.listFiles((file) -> file.isFile() && file.getName().endsWith("-Data.db")); + return sstableFiles[0].getAbsolutePath(); + } }