From ff78780d6125010eb5909477c7a10a2540ca69bc Mon Sep 17 00:00:00 2001 From: Nitsan Wakart Date: Sun, 22 Jun 2025 13:37:29 +0200 Subject: [PATCH] Add cursor based optimized compaction path Adds a compaction implementation utilizing new fixed allocation SSTable reader/writer implementations, and other purpose built code, leading to improved efficiencies. patch by Nitsan Wakart; reviewed by Branimir Lambov, Dmitry Konstantinov for CASSANDRA-20918 --- CHANGES.txt | 1 + build.xml | 1 + .../config/CassandraRelevantProperties.java | 1 + .../org/apache/cassandra/config/Config.java | 3 + .../cassandra/config/DatabaseDescriptor.java | 11 + .../cassandra/db/BufferDecoratedKey.java | 2 +- .../org/apache/cassandra/db/Clustering.java | 2 +- .../cassandra/db/ClusteringComparator.java | 110 +- .../apache/cassandra/db/ClusteringPrefix.java | 2 +- .../cassandra/db/ColumnFamilyStore.java | 5 + src/java/org/apache/cassandra/db/Columns.java | 1 + .../org/apache/cassandra/db/DecoratedKey.java | 4 +- .../apache/cassandra/db/DeletionPurger.java | 8 +- .../org/apache/cassandra/db/DeletionTime.java | 144 +- .../org/apache/cassandra/db/LivenessInfo.java | 185 +- .../cassandra/db/RangeTombstoneList.java | 2 +- .../cassandra/db/ReusableLivenessInfo.java | 94 + .../cassandra/db/SerializationHeader.java | 8 + .../AbstractCompactionPipeline.java | 74 + .../compaction/CompactionStrategyManager.java | 4 +- .../db/compaction/CompactionTask.java | 65 +- .../compaction/CursorCompactionPipeline.java | 112 ++ .../db/compaction/CursorCompactor.java | 1644 +++++++++++++++++ .../IteratorCompactionPipeline.java | 115 ++ .../compaction/LeveledCompactionStrategy.java | 6 + .../db/compaction/StatefulCursor.java | 273 +++ .../writers/CompactionAwareWriter.java | 44 +- .../writers/MajorLeveledCompactionWriter.java | 5 +- .../cassandra/db/guardrails/MaxThreshold.java | 4 +- .../UnfilteredPartitionIterators.java | 2 +- .../apache/cassandra/db/rows/BTreeRow.java | 16 +- .../org/apache/cassandra/db/rows/Cell.java | 59 +- .../org/apache/cassandra/db/rows/Cells.java | 2 +- .../db/rows/RangeTombstoneMarker.java | 6 +- .../db/rows/UnfilteredSerializer.java | 94 +- .../cassandra/dht/ComparableObjectToken.java | 2 +- .../apache/cassandra/dht/IPartitioner.java | 13 + .../cassandra/dht/LocalPartitioner.java | 34 + .../cassandra/dht/Murmur3Partitioner.java | 98 +- .../cassandra/dht/ReusableDecoratedKey.java | 91 + .../io/sstable/AbstractSSTableIterator.java | 2 +- .../sstable/AbstractSSTableSimpleWriter.java | 4 +- .../io/sstable/ClusteringDescriptor.java | 187 ++ .../io/sstable/EmptySSTableScanner.java | 6 + .../cassandra/io/sstable/ISSTableScanner.java | 1 + .../cassandra/io/sstable/IndexInfo.java | 12 + .../io/sstable/PartitionDescriptor.java | 115 ++ .../io/sstable/SSTableCursorKeyReader.java | 157 ++ .../io/sstable/SSTableCursorReader.java | 891 +++++++++ .../io/sstable/SSTableCursorWriter.java | 689 +++++++ .../sstable/SSTableSimpleUnsortedWriter.java | 4 +- .../io/sstable/SSTableSimpleWriter.java | 2 +- .../io/sstable/UnfilteredDescriptor.java | 226 +++ .../io/sstable/format/SSTableReader.java | 11 +- .../io/sstable/format/SSTableScanner.java | 8 + .../sstable/format/SSTableSimpleScanner.java | 7 + .../io/sstable/format/SSTableWriter.java | 14 +- .../format/SortedTablePartitionWriter.java | 51 +- .../sstable/format/SortedTableVerifier.java | 19 +- .../io/sstable/format/SortedTableWriter.java | 22 +- .../format/big/BigFormatPartitionWriter.java | 66 +- .../big/BigSSTableReaderLoadingBuilder.java | 2 +- .../sstable/format/big/BigTableKeyReader.java | 32 +- .../io/sstable/format/big/BigTableReader.java | 4 +- .../sstable/format/big/BigTableScanner.java | 1 + .../sstable/format/big/BigTableVerifier.java | 78 +- .../io/sstable/format/big/BigTableWriter.java | 24 +- .../io/sstable/format/big/RowIndexEntry.java | 49 +- .../format/bti/BtiFormatPartitionWriter.java | 4 +- .../io/sstable/format/bti/BtiTableReader.java | 2 +- .../io/sstable/format/bti/BtiTableWriter.java | 2 +- .../indexsummary/IndexSummaryBuilder.java | 30 +- .../sstable/metadata/MetadataCollector.java | 62 +- .../io/util/ResizableByteBuffer.java | 151 ++ .../cassandra/tools/JsonTransformer.java | 17 +- .../apache/cassandra/utils/BloomFilter.java | 34 + .../apache/cassandra/utils/ByteArrayUtil.java | 15 + .../apache/cassandra/utils/MurmurHash.java | 148 +- .../apache/cassandra/utils/OutputHandler.java | 23 +- .../cassandra/utils/vint/VIntCoding.java | 1 + test/bin/jmh | 2 +- .../distributed/test/FailingRepairTest.java | 6 + .../AccordMigrationReadRaceTestBase.java | 2 +- .../test/accord/InteropTokenRangeTest.java | 2 +- .../cql3/SingleNodeTokenConflictTest.java | 8 +- .../test/log/CoordinatorPathTestBase.java | 12 +- .../test/log/GossipDeadlockTest.java | 2 +- .../log/MetadataChangeSimulationTest.java | 2 +- .../streaming/LCSStreamingKeepLevelTest.java | 4 +- .../test/tcm/CMSPlacementAfterMoveTest.java | 2 +- .../format/ForwardingSSTableReader.java | 4 +- .../service/accord/AccordJournalBurnTest.java | 2 +- .../harry/test/HarryCompactionTest.java | 251 +++ ...HarryCompactionWithRangeDeletionsTest.java | 256 +++ .../harry/test/HarrySSTableWriterTest.java | 190 ++ .../io/sstable/HarrySSTableWriter.java | 656 +++++++ .../test/microbench/CompactionBench.java | 95 +- .../sstable/CompactionLargeCellBench.java | 78 + .../sstable/CompactionStringsBench.java | 92 + .../sstable/CompactionWideRowBench.java | 104 ++ .../sstable/SSTableAbstractBench.java | 111 ++ .../sstable/SSTableAbstractPipeBench.java | 56 + .../sstable/SSTableCursorPipeUtil.java | 150 ++ .../microbench/sstable/SSTablePipeBench.java | 67 + .../sstable/SSTablePipeCursorBench.java | 57 + .../sstable/SSTableRawVisitorBench.java | 310 ++++ .../sstable/SSTableReadingBench.java | 78 + .../sstable/SSTableReadingCursorBench.java | 73 + .../sstable/SSTableReadingFileBench.java | 120 ++ .../SSTableReadingFileCursorBench.java | 218 +++ .../simulator/cluster/KeyspaceActions.java | 4 +- .../org/apache/cassandra/cql3/CQLTester.java | 18 +- .../cassandra/cql3/RandomSchemaTest.java | 46 +- .../db/ClusteringComparatorTest.java | 117 ++ .../db/compaction/CompactionIteratorTest.java | 6 + .../db/compaction/CompactionsCQLTest.java | 3 + .../db/compaction/CompactionsTest.java | 2 + .../CompactionColumnDeleteAndPurgeTest.java | 292 +++ .../simple/CompactionColumnTest.java | 555 ++++++ .../CompactionDeleteAndPurgePKTest.java | 255 +++ .../CompactionDeleteAndPurgeRowTest.java | 637 +++++++ .../simple/CompactionDeletePKTest.java | 294 +++ .../simple/CompactionDeleteRowRangeTest.java | 771 ++++++++ .../simple/CompactionDeleteRowTest.java | 462 +++++ .../CompactionSimpleValueMergeTest.java | 259 +++ .../simple/SimpleCompactionTest.java | 39 + .../BackgroundCompactionTrackingTest.java | 2 +- .../cassandra/dht/Murmur3PartitionerTest.java | 2 +- .../index/accord/RouteIndexTest.java | 4 +- .../cassandra/io/sstable/VerifyTest.java | 55 +- .../cassandra/locator/SimpleStrategyTest.java | 2 +- .../service/paxos/PaxosRepairHistoryTest.java | 2 +- .../cassandra/tools/OfflineToolUtils.java | 2 + .../cassandra/utils/CassandraGenerators.java | 6 +- .../utils/PreSortedBubbleInsertTest.java | 131 ++ .../apache/cassandra/utils/TestHelper.java | 60 + .../ByteSourceComparisonTest.java | 5 + 137 files changed, 13127 insertions(+), 434 deletions(-) create mode 100644 src/java/org/apache/cassandra/db/ReusableLivenessInfo.java create mode 100644 src/java/org/apache/cassandra/db/compaction/AbstractCompactionPipeline.java create mode 100644 src/java/org/apache/cassandra/db/compaction/CursorCompactionPipeline.java create mode 100644 src/java/org/apache/cassandra/db/compaction/CursorCompactor.java create mode 100644 src/java/org/apache/cassandra/db/compaction/IteratorCompactionPipeline.java create mode 100644 src/java/org/apache/cassandra/db/compaction/StatefulCursor.java create mode 100644 src/java/org/apache/cassandra/dht/ReusableDecoratedKey.java create mode 100644 src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java create mode 100644 src/java/org/apache/cassandra/io/sstable/PartitionDescriptor.java create mode 100644 src/java/org/apache/cassandra/io/sstable/SSTableCursorKeyReader.java create mode 100644 src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java create mode 100644 src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java create mode 100644 src/java/org/apache/cassandra/io/sstable/UnfilteredDescriptor.java create mode 100644 src/java/org/apache/cassandra/io/util/ResizableByteBuffer.java create mode 100644 test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java create mode 100644 test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java create mode 100644 test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java create mode 100644 test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionLargeCellBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionStringsBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionWideRowBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractPipeBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeCursorBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableRawVisitorBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingCursorBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileBench.java create mode 100644 test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java create mode 100644 test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnDeleteAndPurgeTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgePKTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgeRowTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeletePKTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowRangeTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/CompactionSimpleValueMergeTest.java create mode 100644 test/unit/org/apache/cassandra/db/compaction/simple/SimpleCompactionTest.java create mode 100644 test/unit/org/apache/cassandra/utils/PreSortedBubbleInsertTest.java create mode 100644 test/unit/org/apache/cassandra/utils/TestHelper.java diff --git a/CHANGES.txt b/CHANGES.txt index 926f4fca76..c4ecfb016c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Add cursor based optimized compaction path (CASSANDRA-20918) * Ensure peers with LEFT status are expired from gossip state (CASSANDRA-21035) * Optimize UTF8Validator.validate for ASCII prefixed Strings (CASSANDRA-21075) * Switch LatencyMetrics to use ThreadLocalTimer/ThreadLocalCounter (CASSANDRA-21080) diff --git a/build.xml b/build.xml index 35359b808a..4dcc5276ae 100644 --- a/build.xml +++ b/build.xml @@ -1414,6 +1414,7 @@ + diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 99726f6b3d..d1458d1bbb 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -193,6 +193,7 @@ public enum CassandraRelevantProperties CONSISTENT_RANGE_MOVEMENT("cassandra.consistent.rangemovement", "true"), CONSISTENT_SIMULTANEOUS_MOVES_ALLOW("cassandra.consistent.simultaneousmoves.allow"), CRYPTO_PROVIDER_CLASS_NAME("cassandra.crypto_provider_class_name"), + CURSOR_COMPACTION_ENABLED("cassandra.cursor_compaction_enabled", "true"), CUSTOM_DISK_ERROR_HANDLER("cassandra.custom_disk_error_handler"), CUSTOM_GUARDRAILS_CONFIG_PROVIDER_CLASS("cassandra.custom_guardrails_config_provider_class"), CUSTOM_QUERY_HANDLER_CLASS("cassandra.custom_query_handler_class"), diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index a8860be63a..2d517fc127 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -49,6 +49,7 @@ import org.apache.cassandra.utils.StorageCompatibilityMode; import static org.apache.cassandra.config.CassandraRelevantProperties.AUTOCOMPACTION_ON_STARTUP_ENABLED; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_AVAILABLE_PROCESSORS; +import static org.apache.cassandra.config.CassandraRelevantProperties.CURSOR_COMPACTION_ENABLED; import static org.apache.cassandra.config.CassandraRelevantProperties.FILE_CACHE_ENABLED; import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE; import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE_KEYSPACES; @@ -661,6 +662,8 @@ public class Config @Replaces(oldName = "enable_drop_compact_storage", converter = Converters.IDENTITY, deprecated = true) public volatile boolean drop_compact_storage_enabled = false; + public boolean cursor_compaction_enabled = CURSOR_COMPACTION_ENABLED.getBoolean(); + public volatile boolean use_statements_enabled = true; /** diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 9e28199f7c..322fbf5ff4 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -4725,6 +4725,17 @@ public class DatabaseDescriptor conf.transient_replication_enabled = enabled; } + public static boolean cursorCompactionEnabled() + { + return conf.cursor_compaction_enabled; + } + + @VisibleForTesting + public static void setCursorCompactionEnabled(boolean cursor_compaction_enabled) + { + conf.cursor_compaction_enabled = cursor_compaction_enabled; + } + public static boolean enableDropCompactStorage() { return conf.drop_compact_storage_enabled; diff --git a/src/java/org/apache/cassandra/db/BufferDecoratedKey.java b/src/java/org/apache/cassandra/db/BufferDecoratedKey.java index 07f610fd7c..06b8e44b02 100644 --- a/src/java/org/apache/cassandra/db/BufferDecoratedKey.java +++ b/src/java/org/apache/cassandra/db/BufferDecoratedKey.java @@ -25,7 +25,7 @@ import org.apache.cassandra.utils.bytecomparable.ByteComparable; public class BufferDecoratedKey extends DecoratedKey { - private final ByteBuffer key; + protected ByteBuffer key; public BufferDecoratedKey(Token token, ByteBuffer key) { diff --git a/src/java/org/apache/cassandra/db/Clustering.java b/src/java/org/apache/cassandra/db/Clustering.java index 3e42e4a361..8972161d20 100644 --- a/src/java/org/apache/cassandra/db/Clustering.java +++ b/src/java/org/apache/cassandra/db/Clustering.java @@ -133,7 +133,7 @@ public interface Clustering extends ClusteringPrefix, IMeasurableMemory /** * Serializer for Clustering object. *

- * Because every clustering in a given table must have the same size (ant that size cannot actually change once the table + * Because every clustering in a given table must have the same size (and that size cannot actually change once the table * has been defined), we don't record that size. */ public static class Serializer diff --git a/src/java/org/apache/cassandra/db/ClusteringComparator.java b/src/java/org/apache/cassandra/db/ClusteringComparator.java index 2949130707..82be2a12e4 100644 --- a/src/java/org/apache/cassandra/db/ClusteringComparator.java +++ b/src/java/org/apache/cassandra/db/ClusteringComparator.java @@ -26,14 +26,17 @@ import java.util.Objects; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import org.apache.cassandra.io.sstable.ClusteringDescriptor; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.ByteBufferAccessor; import org.apache.cassandra.db.marshal.ValueAccessor; import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.marshal.AbstractType; -import org.apache.cassandra.serializers.MarshalException; - import org.apache.cassandra.io.sstable.IndexInfo; +import org.apache.cassandra.serializers.MarshalException; +import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.bytecomparable.ByteComparable; import org.apache.cassandra.utils.bytecomparable.ByteSource; +import org.apache.cassandra.utils.vint.VIntCoding; import static org.apache.cassandra.utils.bytecomparable.ByteSource.EXCLUDED; import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT; @@ -156,6 +159,107 @@ public class ClusteringComparator implements Comparator return s1 < s2 ? c1.kind().comparedToClustering : -c2.kind().comparedToClustering; } + public static int compare(ClusteringDescriptor c1, ClusteringDescriptor c2) + { + final int c1Size = c1.clusteringColumnsBound(); + final int c2Size = c2.clusteringColumnsBound(); + final int minColumns = Math.min(c1Size, c2Size); + + final int cmp = compare(c1.clusteringTypes(), c1.clusteringBuffer(), c2.clusteringBuffer(), minColumns); + if (cmp != 0) + return cmp; + + final ClusteringPrefix.Kind c1Kind = c1.clusteringKind(); + final ClusteringPrefix.Kind c2Kind = c2.clusteringKind(); + if (c1Size == c2Size) + { + return ClusteringPrefix.Kind.compare(c1Kind, c2Kind); + } + + return c1Size < c2Size ? c1Kind.comparedToClustering : -c2Kind.comparedToClustering; + } + + public static int compare(AbstractType[] types, ByteBuffer c1, ByteBuffer c2) { + return compare(types, c1, c2, types.length); + } + + private static int compare(AbstractType[] types, ByteBuffer c1, ByteBuffer c2, int size) + { + long clusteringBlock1 = 0; + long clusteringBlock2 = 0; + final int position1 = c1.position(); + final int position2 = c2.position(); + final int limit1 = c1.limit(); + final int limit2 = c2.limit(); + try + { + for (int clusteringIndex = 0; clusteringIndex < size; clusteringIndex++) + { + if (clusteringIndex % 32 == 0) + { + clusteringBlock1 = VIntCoding.readUnsignedVInt(c1); + clusteringBlock2 = VIntCoding.readUnsignedVInt(c2); + } + + AbstractType type = types[clusteringIndex]; + + byte v1Flags = (byte) (clusteringBlock1 & 0b11); + byte v2Flags = (byte) (clusteringBlock2 & 0b11); + + // both values are present + if ((v1Flags|v2Flags) == 0) + { + boolean isByteOrderComparable = type.isByteOrderComparable; + int vlen1,vlen2; + if (type.isValueLengthFixed()) + { + vlen1 = vlen2 = type.valueLengthIfFixed(); + } + else + { + vlen1 = VIntCoding.readUnsignedVInt32(c1); + vlen2 = VIntCoding.readUnsignedVInt32(c2); + } + int v1Limit = c1.position() + vlen1; + if (v1Limit > limit1) + throw new IllegalArgumentException("Value limit exceeds buffer limit."); + c1.limit(v1Limit); + int v2Limit = c2.position() + vlen2; + if (v2Limit > limit2) + throw new IllegalArgumentException("Value limit exceeds buffer limit."); + c2.limit(v2Limit); + int cmp = isByteOrderComparable ? + ByteBufferUtil.compareUnsigned(c1, c2) : + type.compareCustom(c1, ByteBufferAccessor.instance, c2, ByteBufferAccessor.instance); + if (cmp != 0) + return cmp; + c1.position(v1Limit); + c2.position(v2Limit); + c1.limit(limit1); + c2.limit(limit2); + } + // present > not present + else + { + // null (0b10) is smaller than empty (0b01) which is smaller than valued (0b00); + // compare swapped arguments to reverse the order + int cmp = Long.compare(v2Flags, v1Flags); + if (cmp != 0) + return cmp; + // null/empty == null/empty, continue... + } + clusteringBlock1 = clusteringBlock1 >>> 2; + clusteringBlock2 = clusteringBlock2 >>> 2; + } + } + finally + { + c1.position(position1).limit(limit1); + c2.position(position2).limit(limit2); + } + return 0; + } + public int compare(Clustering c1, Clustering c2) { return compare(c1, c2, size()); diff --git a/src/java/org/apache/cassandra/db/ClusteringPrefix.java b/src/java/org/apache/cassandra/db/ClusteringPrefix.java index c7687bb80f..e6e3c76c43 100644 --- a/src/java/org/apache/cassandra/db/ClusteringPrefix.java +++ b/src/java/org/apache/cassandra/db/ClusteringPrefix.java @@ -519,7 +519,7 @@ public interface ClusteringPrefix extends IMeasurableMemory, Clusterable return result; } - byte[][] deserializeValuesWithoutSize(DataInputPlus in, int size, int version, List> types) throws IOException + public byte[][] deserializeValuesWithoutSize(DataInputPlus in, int size, int version, List> types) throws IOException { // Callers of this method should handle the case where size = 0 (in all case we want to return a special value anyway). assert size > 0; diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java index 4cc2615cd4..089e5bf230 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java @@ -2330,6 +2330,11 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner return partitionKeySetIgnoreGcGrace.contains(dk); } + public boolean shouldIgnoreGcGraceForAnyKey() + { + return !partitionKeySetIgnoreGcGrace.isEmpty(); + } + public static Iterable all() { List> stores = new ArrayList<>(Schema.instance.getKeyspaces().size()); diff --git a/src/java/org/apache/cassandra/db/Columns.java b/src/java/org/apache/cassandra/db/Columns.java index 3e014d3254..f5b5641852 100644 --- a/src/java/org/apache/cassandra/db/Columns.java +++ b/src/java/org/apache/cassandra/db/Columns.java @@ -529,6 +529,7 @@ public class Columns extends AbstractCollection implements Colle int supersetCount = superset.size(); if (columnCount == supersetCount) { + /** This is prevented by caller for row serialization: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serializeRowBody}*/ out.writeUnsignedVInt32(0); } else if (supersetCount < 64) diff --git a/src/java/org/apache/cassandra/db/DecoratedKey.java b/src/java/org/apache/cassandra/db/DecoratedKey.java index 309f764a96..36612040d0 100644 --- a/src/java/org/apache/cassandra/db/DecoratedKey.java +++ b/src/java/org/apache/cassandra/db/DecoratedKey.java @@ -114,7 +114,7 @@ public abstract class DecoratedKey implements PartitionPosition, FilterKey // The OSS50 version avoids this by adding a terminator. return ByteSource.withTerminatorMaybeLegacy(version, ByteSource.END_OF_STREAM, - token.asComparableBytes(version), + getToken().asComparableBytes(version), keyComparableBytes(version)); } @@ -127,7 +127,7 @@ public abstract class DecoratedKey implements PartitionPosition, FilterKey return ByteSource.withTerminator( before ? ByteSource.LT_NEXT_COMPONENT : ByteSource.GT_NEXT_COMPONENT, - token.asComparableBytes(version), + getToken().asComparableBytes(version), keyComparableBytes(version)); }; } diff --git a/src/java/org/apache/cassandra/db/DeletionPurger.java b/src/java/org/apache/cassandra/db/DeletionPurger.java index 795817fd3d..2c3f69a7cb 100644 --- a/src/java/org/apache/cassandra/db/DeletionPurger.java +++ b/src/java/org/apache/cassandra/db/DeletionPurger.java @@ -19,16 +19,16 @@ package org.apache.cassandra.db; public interface DeletionPurger { - public static final DeletionPurger PURGE_ALL = (ts, ldt) -> true; + DeletionPurger PURGE_ALL = (ts, ldt) -> true; - public boolean shouldPurge(long timestamp, long localDeletionTime); + boolean shouldPurge(long timestamp, long localDeletionTime); - public default boolean shouldPurge(DeletionTime dt) + default boolean shouldPurge(DeletionTime dt) { return !dt.isLive() && shouldPurge(dt.markedForDeleteAt(), dt.localDeletionTime()); } - public default boolean shouldPurge(LivenessInfo liveness, long nowInSec) + default boolean shouldPurge(LivenessInfo liveness, long nowInSec) { return !liveness.isLive(nowInSec) && shouldPurge(liveness.timestamp(), liveness.localExpirationTime()); } diff --git a/src/java/org/apache/cassandra/db/DeletionTime.java b/src/java/org/apache/cassandra/db/DeletionTime.java index 5970fbb042..1d54a00fff 100644 --- a/src/java/org/apache/cassandra/db/DeletionTime.java +++ b/src/java/org/apache/cassandra/db/DeletionTime.java @@ -36,27 +36,29 @@ import static java.lang.Math.min; /** * Information on deletion of a storage engine object. */ -public class DeletionTime implements Comparable, IMeasurableMemory +public abstract class DeletionTime implements Comparable, IMeasurableMemory { - public static final long EMPTY_SIZE = ObjectSizes.measure(new DeletionTime(0, 0)); + private static final int LOCAL_DELETION_TIME_LIVE = Cell.deletionTimeLongToUnsignedInteger(Long.MAX_VALUE); + private static final long MARKED_FOR_DELETE_AT_LIVE = Long.MIN_VALUE; + public static final long EMPTY_SIZE = ObjectSizes.measure(new ImmutableDeletionTime(0, 0)); /** * A special DeletionTime that signifies that there is no top-level (row) tombstone. */ - public static final DeletionTime LIVE = new DeletionTime(Long.MIN_VALUE, Long.MAX_VALUE); + public static final DeletionTime LIVE = new ImmutableDeletionTime(MARKED_FOR_DELETE_AT_LIVE, LOCAL_DELETION_TIME_LIVE); private static final Serializer serializer = new Serializer(); private static final Serializer legacySerializer = new LegacySerializer(); - private final long markedForDeleteAt; - final int localDeletionTimeUnsignedInteger; + protected long markedForDeleteAt; + protected int localDeletionTimeUnsignedInteger; public static DeletionTime build(long markedForDeleteAt, long localDeletionTime) { // Negative ldts can only be a result of a corruption or when scrubbing legacy sstables with overflown int ldts return localDeletionTime < 0 || localDeletionTime > Cell.MAX_DELETION_TIME ? new InvalidDeletionTime(markedForDeleteAt) - : new DeletionTime(markedForDeleteAt, localDeletionTime); + : new ImmutableDeletionTime(markedForDeleteAt, localDeletionTime); } // Do not use. This is a perf optimization where some data structures known to hold valid uints are allowed to use it. @@ -65,7 +67,7 @@ public class DeletionTime implements Comparable, IMeasurableMemory { return CassandraUInt.compare(Cell.MAX_DELETION_TIME_UNSIGNED_INTEGER, localDeletionTimeUnsignedInteger) < 0 ? new InvalidDeletionTime(markedForDeleteAt) - : new DeletionTime(markedForDeleteAt, localDeletionTimeUnsignedInteger); + : new ImmutableDeletionTime(markedForDeleteAt, localDeletionTimeUnsignedInteger); } private DeletionTime(long markedForDeleteAt, long localDeletionTime) @@ -98,6 +100,11 @@ public class DeletionTime implements Comparable, IMeasurableMemory return Cell.deletionTimeUnsignedIntegerToLong(localDeletionTimeUnsignedInteger); } + public int localDeletionTimeUnsignedInteger() + { + return localDeletionTimeUnsignedInteger; + } + /** * Returns whether this DeletionTime is live, that is deletes no columns. */ @@ -143,7 +150,7 @@ public class DeletionTime implements Comparable, IMeasurableMemory @Override public String toString() { - return String.format("deletedAt=%d, localDeletion=%d", markedForDeleteAt(), localDeletionTime()); + return isLive() ? "LIVE" : String.format("deletedAt=%d, localDeletion=%d", markedForDeleteAt(), localDeletionTime()); } public int compareTo(DeletionTime dt) @@ -155,6 +162,10 @@ public class DeletionTime implements Comparable, IMeasurableMemory else return CassandraUInt.compare(localDeletionTimeUnsignedInteger, dt.localDeletionTimeUnsignedInteger); } + /** + * supersedes: supplants, replaces, in this case: "is more recent" + * @return true if dt is deleted BEFORE this (markedForDeleteAt > dt.markedForDeleteAt || (markedForDeleteAt == dt.markedForDeleteAt && localDeletionTime > dt.localDeletionTime)) + */ public boolean supersedes(DeletionTime dt) { return markedForDeleteAt() > dt.markedForDeleteAt() || (markedForDeleteAt() == dt.markedForDeleteAt() && localDeletionTime() > dt.localDeletionTime()); @@ -209,7 +220,7 @@ public class DeletionTime implements Comparable, IMeasurableMemory public void serialize(DeletionTime delTime, DataOutputPlus out) throws IOException { - if (delTime == LIVE) + if (delTime == LIVE || delTime.isLive()) out.writeByte(IS_LIVE_DELETION); else { @@ -238,7 +249,30 @@ public class DeletionTime implements Comparable, IMeasurableMemory long mfda = readBytesToMFDA(flags, bytes1, bytes2, bytes4); int localDeletionTimeUnsignedInteger = in.readInt(); - return new DeletionTime(mfda, localDeletionTimeUnsignedInteger); + return new ImmutableDeletionTime(mfda, localDeletionTimeUnsignedInteger); + } + } + + public void deserialize(DataInputPlus in, ReusableDeletionTime reuse) throws IOException + { + int flags = in.readByte(); + if ((flags & IS_LIVE_DELETION) != 0) + { + if ((flags & 0xFF) != IS_LIVE_DELETION) + throw new IOException("Corrupted sstable. Invalid flags found deserializing DeletionTime: " + Integer.toBinaryString(flags & 0xFF)); + reuse.resetLive(); + } + else + { + // Read the remaining 7 bytes + int bytes1 = in.readByte(); + int bytes2 = in.readShort(); + int bytes4 = in.readInt(); + + long mfda = readBytesToMFDA(flags, bytes1, bytes2, bytes4); + int localDeletionTimeUnsignedInteger = in.readInt(); + + reuse.reset(mfda, localDeletionTimeUnsignedInteger); } } @@ -256,7 +290,7 @@ public class DeletionTime implements Comparable, IMeasurableMemory long mfda = buf.getLong(offset); int localDeletionTimeUnsignedInteger = buf.getInt(offset + TypeSizes.LONG_SIZE); - return new DeletionTime(mfda, localDeletionTimeUnsignedInteger); + return new ImmutableDeletionTime(mfda, localDeletionTimeUnsignedInteger); } } @@ -315,13 +349,26 @@ public class DeletionTime implements Comparable, IMeasurableMemory : DeletionTime.build(mfda, ldt); } + public void deserialize(DataInputPlus in, ReusableDeletionTime reuse) throws IOException + { + int ldt = in.readInt(); + long mfda = in.readLong(); + if (mfda == Long.MIN_VALUE && ldt == Integer.MAX_VALUE) { + reuse.resetLive(); + } + else + { + reuse.reset(mfda, ldt); + } + } + public DeletionTime deserialize(ByteBuffer buf, int offset) { int ldt = buf.getInt(offset); long mfda = buf.getLong(offset + 4); return mfda == Long.MIN_VALUE && ldt == Integer.MAX_VALUE ? LIVE - : new DeletionTime(mfda, ldt); + : new ImmutableDeletionTime(mfda, ldt); } public void skip(DataInputPlus in) throws IOException @@ -336,8 +383,22 @@ public class DeletionTime implements Comparable, IMeasurableMemory } } + + private static class ImmutableDeletionTime extends DeletionTime + { + private ImmutableDeletionTime(long markedForDeleteAt, long localDeletionTime) + { + super(markedForDeleteAt, localDeletionTime); + } + + private ImmutableDeletionTime(long markedForDeleteAt, int localDeletionTimeUnsignedInteger) + { + super(markedForDeleteAt, localDeletionTimeUnsignedInteger); + } + } + // When scrubbing legacy sstables (overflown) or upon sstable corruption we could have negative ldts - public static class InvalidDeletionTime extends DeletionTime + public static class InvalidDeletionTime extends ImmutableDeletionTime { private InvalidDeletionTime(long markedForDeleteAt) { @@ -358,4 +419,61 @@ public class DeletionTime implements Comparable, IMeasurableMemory return false; } } + + public static class ReusableDeletionTime extends DeletionTime + { + private ReusableDeletionTime(long markedForDeleteAt, int localDeletionTimeUnsignedInteger) + { + super(markedForDeleteAt, localDeletionTimeUnsignedInteger); + } + + public void resetLive() + { + markedForDeleteAt = MARKED_FOR_DELETE_AT_LIVE; + localDeletionTimeUnsignedInteger = LOCAL_DELETION_TIME_LIVE; + } + + void reset(long markedForDeleteAt, int localDeletionTimeUnsignedInteger) + { + this.markedForDeleteAt = markedForDeleteAt; + this.localDeletionTimeUnsignedInteger = localDeletionTimeUnsignedInteger; + } + + public void reset(long markedForDeleteAt, long localDeletionTime) + { + this.markedForDeleteAt = markedForDeleteAt; + if (localDeletionTime < 0 || localDeletionTime > Cell.MAX_DELETION_TIME) // invalid + this.localDeletionTimeUnsignedInteger = Cell.MAX_DELETION_TIME_UNSIGNED_INTEGER + 1; + else + this.localDeletionTimeUnsignedInteger = Cell.deletionTimeLongToUnsignedInteger(localDeletionTime); + } + + public void reset(DeletionTime deletionTime) + { + if (deletionTime == null) + resetLive(); + else + reset(deletionTime.markedForDeleteAt, deletionTime.localDeletionTimeUnsignedInteger); + } + + @Override + public boolean validate() + { + return localDeletionTimeUnsignedInteger == LOCAL_DELETION_TIME_LIVE || CassandraUInt.compare(Cell.MAX_DELETION_TIME_UNSIGNED_INTEGER, localDeletionTimeUnsignedInteger) >= 0; + } + + public static ReusableDeletionTime copy(DeletionTime original) + { + if (original == null) + return live(); + // Negative ldts can only be a result of a corruption or when scrubbing legacy sstables with overflown int ldts + return new ReusableDeletionTime(original.markedForDeleteAt, original.localDeletionTimeUnsignedInteger); + } + + public static ReusableDeletionTime live() + { + // Negative ldts can only be a result of a corruption or when scrubbing legacy sstables with overflown int ldts + return new ReusableDeletionTime(LIVE.markedForDeleteAt, LIVE.localDeletionTimeUnsignedInteger); + } + } } diff --git a/src/java/org/apache/cassandra/db/LivenessInfo.java b/src/java/org/apache/cassandra/db/LivenessInfo.java index 168473add5..e72c408d1e 100644 --- a/src/java/org/apache/cassandra/db/LivenessInfo.java +++ b/src/java/org/apache/cassandra/db/LivenessInfo.java @@ -37,35 +37,28 @@ import org.apache.cassandra.utils.ObjectSizes; * unaffected (of course, the rest of said row data might be ttl'ed on its own but this is * separate). */ -public class LivenessInfo implements IMeasurableMemory +public interface LivenessInfo extends IMeasurableMemory { - public static final long NO_TIMESTAMP = Long.MIN_VALUE; - public static final int NO_TTL = Cell.NO_TTL; + long NO_TIMESTAMP = Long.MIN_VALUE; + int NO_TTL = Cell.NO_TTL; /** * Used as flag for representing an expired liveness. * * TTL per request is at most 20 yrs, so this shouldn't conflict * (See {@link org.apache.cassandra.cql3.Attributes#MAX_TTL}) */ - public static final int EXPIRED_LIVENESS_TTL = Integer.MAX_VALUE; - public static final long NO_EXPIRATION_TIME = Cell.NO_DELETION_TIME; + int EXPIRED_LIVENESS_TTL = Integer.MAX_VALUE; + long NO_EXPIRATION_TIME = Cell.NO_DELETION_TIME; - public static final LivenessInfo EMPTY = new LivenessInfo(NO_TIMESTAMP); - private static final long UNSHARED_HEAP_SIZE = ObjectSizes.measure(EMPTY); + LivenessInfo EMPTY = new ImmutableLivenessInfo(NO_TIMESTAMP); + long UNSHARED_HEAP_SIZE = ObjectSizes.measure(EMPTY); - protected final long timestamp; - - protected LivenessInfo(long timestamp) + static LivenessInfo create(long timestamp, long nowInSec) { - this.timestamp = timestamp; + return new ImmutableLivenessInfo(timestamp); } - public static LivenessInfo create(long timestamp, long nowInSec) - { - return new LivenessInfo(timestamp); - } - - public static LivenessInfo expiring(long timestamp, int ttl, long nowInSec) + static LivenessInfo expiring(long timestamp, int ttl, long nowInSec) { assert ttl != EXPIRED_LIVENESS_TTL; return new ExpiringLivenessInfo(timestamp, ttl, ExpirationDateOverflowHandling.computeLocalExpirationTime(nowInSec, ttl)); @@ -86,7 +79,7 @@ public class LivenessInfo implements IMeasurableMemory : expiring(timestamp, ttl, nowInSec, applyOverflowPolicy); } - public static LivenessInfo create(long timestamp, int ttl, long nowInSec) + static LivenessInfo create(long timestamp, int ttl, long nowInSec) { return ttl == NO_TTL ? create(timestamp, nowInSec) @@ -95,11 +88,11 @@ public class LivenessInfo implements IMeasurableMemory // Note that this ctor takes the expiration time, not the current time. // Use when you know that's what you want. - public static LivenessInfo withExpirationTime(long timestamp, int ttl, long localExpirationTime) + static LivenessInfo withExpirationTime(long timestamp, int ttl, long localExpirationTime) { if (ttl == EXPIRED_LIVENESS_TTL) return new ExpiredLivenessInfo(timestamp, ttl, localExpirationTime); - return ttl == NO_TTL ? new LivenessInfo(timestamp) : new ExpiringLivenessInfo(timestamp, ttl, localExpirationTime); + return ttl == NO_TTL ? new ImmutableLivenessInfo(timestamp) : new ExpiringLivenessInfo(timestamp, ttl, localExpirationTime); } /** @@ -107,9 +100,9 @@ public class LivenessInfo implements IMeasurableMemory * * @return whether this liveness info is empty or not. */ - public boolean isEmpty() + default boolean isEmpty() { - return timestamp == NO_TIMESTAMP; + return timestamp() == NO_TIMESTAMP; } /** @@ -117,18 +110,11 @@ public class LivenessInfo implements IMeasurableMemory * * @return the liveness info timestamp (or {@link #NO_TIMESTAMP} if the info is empty). */ - public long timestamp() - { - return timestamp; - } - + long timestamp(); /** * Whether the info has a ttl. */ - public boolean isExpiring() - { - return false; - } + boolean isExpiring(); /** * The ttl (if any) on the row primary key columns or {@link #NO_TTL} if it is not @@ -137,19 +123,13 @@ public class LivenessInfo implements IMeasurableMemory * Please note that this value is the TTL that was set originally and is thus not * changing. */ - public int ttl() - { - return NO_TTL; - } + int ttl(); /** * The expiration time (in seconds) if the info is expiring ({@link #NO_EXPIRATION_TIME} otherwise). * */ - public long localExpirationTime() - { - return NO_EXPIRATION_TIME; - } + long localExpirationTime(); /** * Whether that info is still live. @@ -160,17 +140,15 @@ public class LivenessInfo implements IMeasurableMemory * @param nowInSec the current time in seconds. * @return whether this liveness info is live or not. */ - public boolean isLive(long nowInSec) - { - return !isEmpty(); - } + boolean isLive(long nowInSec); + /** * Adds this liveness information to the provided digest. * * @param digest the digest to add this liveness information to. */ - public void digest(Digest digest) + default void digest(Digest digest) { digest.updateWithLong(timestamp()); } @@ -180,7 +158,7 @@ public class LivenessInfo implements IMeasurableMemory * * @throws MarshalException if some of the data is corrupted. */ - public void validate() + default void validate() { } @@ -189,18 +167,18 @@ public class LivenessInfo implements IMeasurableMemory * * @return the size of the data this liveness information contains. */ - public int dataSize() + default int dataSize() { return TypeSizes.sizeof(timestamp()); } /** * Whether this liveness information supersedes another one (that is - * whether is has a greater timestamp than the other or not). + * whether it has a greater timestamp than the other or not). * * If timestamps are the same and none of them are expired livenessInfo, * livenessInfo with greater TTL supersedes another. It also means, if timestamps are the same, - * ttl superseders no-ttl. This is the same rule as {@link Conflicts#resolveRegular} + * ttl superseders no-ttl. This is the same rule as {@link org.apache.cassandra.db.rows.Cells#resolveRegular} * * If timestamps are the same and one of them is expired livenessInfo. Expired livenessInfo * supersedes, ie. tombstone supersedes. @@ -216,10 +194,12 @@ public class LivenessInfo implements IMeasurableMemory * * @return whether this {@code LivenessInfo} supersedes {@code other}. */ - public boolean supersedes(LivenessInfo other) + default boolean supersedes(LivenessInfo other) { - if (timestamp != other.timestamp) - return timestamp > other.timestamp; + long tTimestamp = timestamp(); + long oTimestamp = other.timestamp(); + if (tTimestamp != oTimestamp) + return tTimestamp > oTimestamp; if (isExpired() ^ other.isExpired()) return isExpired(); if (isExpiring() == other.isExpiring()) @@ -231,7 +211,7 @@ public class LivenessInfo implements IMeasurableMemory return isExpiring(); } - protected boolean isExpired() + default boolean isExpired() { return false; } @@ -244,47 +224,24 @@ public class LivenessInfo implements IMeasurableMemory * as timestamp. If it has no timestamp however, this liveness info is returned * unchanged. */ - public LivenessInfo withUpdatedTimestamp(long newTimestamp) + default LivenessInfo withUpdatedTimestamp(long newTimestamp) { - return new LivenessInfo(newTimestamp); + return new ImmutableLivenessInfo(newTimestamp); } - public LivenessInfo withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime) + default LivenessInfo withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime) { return LivenessInfo.create(newTimestamp, ttl(), newLocalDeletionTime, true); } // C14227 To prevent row resurrection and be backwards compatible sometimes we need to force an overflowed ldt - public LivenessInfo withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime, boolean applyOverflowPolicy) + default LivenessInfo withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, long newLocalDeletionTime, boolean applyOverflowPolicy) { return LivenessInfo.create(newTimestamp, ttl(), newLocalDeletionTime, applyOverflowPolicy); } @Override - public String toString() - { - return String.format("[ts=%d]", timestamp); - } - - @Override - public boolean equals(Object other) - { - if(!(other instanceof LivenessInfo)) - return false; - - LivenessInfo that = (LivenessInfo)other; - return this.timestamp() == that.timestamp() - && this.ttl() == that.ttl() - && this.localExpirationTime() == that.localExpirationTime(); - } - - @Override - public int hashCode() - { - return Objects.hash(timestamp(), ttl(), localExpirationTime()); - } - - public long unsharedHeapSize() + default long unsharedHeapSize() { return this == EMPTY ? 0 : UNSHARED_HEAP_SIZE; } @@ -295,7 +252,7 @@ public class LivenessInfo implements IMeasurableMemory * * See {@link org.apache.cassandra.db.view.ViewUpdateGenerator#deleteOldEntryInternal}. */ - private static class ExpiredLivenessInfo extends ExpiringLivenessInfo + class ExpiredLivenessInfo extends ExpiringLivenessInfo { private ExpiredLivenessInfo(long timestamp, int ttl, long localExpirationTime) { @@ -324,7 +281,7 @@ public class LivenessInfo implements IMeasurableMemory } } - private static class ExpiringLivenessInfo extends LivenessInfo + class ExpiringLivenessInfo extends ImmutableLivenessInfo { private final int ttl; private final long localExpirationTime; @@ -401,7 +358,7 @@ public class LivenessInfo implements IMeasurableMemory @Override public String toString() { - return String.format("[ts=%d ttl=%d, let=%d]", timestamp, ttl, localExpirationTime); + return String.format("[ts=%d ttl=%d, let=%d]", timestamp(), ttl, localExpirationTime); } public long unsharedHeapSize() @@ -409,4 +366,66 @@ public class LivenessInfo implements IMeasurableMemory return UNSHARED_HEAP_SIZE; } } + + class ImmutableLivenessInfo implements LivenessInfo { + private final long timestamp; + + private ImmutableLivenessInfo(long timestamp) + { + this.timestamp = timestamp; + } + + @Override + public final long timestamp() + { + return timestamp; + } + + public int ttl() + { + return NO_TTL; + } + + @Override + public boolean isExpiring() + { + return false; + } + + @Override + public long localExpirationTime() + { + return NO_EXPIRATION_TIME; + } + + @Override + public boolean isLive(long nowInSec) + { + return !isEmpty(); + } + + @Override + public String toString() + { + return String.format("[ts=%d]", timestamp); + } + + @Override + public boolean equals(Object other) + { + if(!(other instanceof LivenessInfo)) + return false; + + LivenessInfo that = (LivenessInfo)other; + return this.timestamp() == that.timestamp() + && this.ttl() == that.ttl() + && this.localExpirationTime() == that.localExpirationTime(); + } + + @Override + public int hashCode() + { + return Objects.hash(timestamp(), ttl(), localExpirationTime()); + } + } } diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java index 963985788a..cf85d728cf 100644 --- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java +++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java @@ -145,7 +145,7 @@ public class RangeTombstoneList implements Iterable, IMeasurable add(tombstone.deletedSlice().start(), tombstone.deletedSlice().end(), tombstone.deletionTime().markedForDeleteAt(), - tombstone.deletionTime().localDeletionTimeUnsignedInteger); + tombstone.deletionTime().localDeletionTimeUnsignedInteger()); } /** diff --git a/src/java/org/apache/cassandra/db/ReusableLivenessInfo.java b/src/java/org/apache/cassandra/db/ReusableLivenessInfo.java new file mode 100644 index 0000000000..b3c34eac01 --- /dev/null +++ b/src/java/org/apache/cassandra/db/ReusableLivenessInfo.java @@ -0,0 +1,94 @@ +/* + * 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.db; + +// TODO: maybe flatten into descriptor classes? +public class ReusableLivenessInfo implements LivenessInfo +{ + private int ttl = NO_TTL; + private long localExpirationTime = NO_EXPIRATION_TIME; + private long timestamp = NO_TIMESTAMP; + + @Override + public long timestamp() + { + return timestamp; + } + + @Override + public int ttl() + { + return ttl; + } + + @Override + public long localExpirationTime() + { + return localExpirationTime; + } + + @Override + public boolean isExpiring() + { + return localExpirationTime != NO_EXPIRATION_TIME; + } + + /** + * {@link org.apache.cassandra.db.rows.AbstractCell#isTombstone()} + */ + public boolean isTombstone() + { + return localExpirationTime != NO_EXPIRATION_TIME && ttl == NO_TTL; + } + + @Override + public boolean isLive(long nowInSec) + { + return localExpirationTime == NO_EXPIRATION_TIME || ttl != NO_TTL && !isExpired(nowInSec); + } + + public boolean isExpired(long nowInSec) + { + return nowInSec >= localExpirationTime; + } + + public void ttlToTombstone() + { + // LET/LDT is now the time the TTL would have expired + localExpirationTime = localExpirationTime - ttl; + ttl = NO_TTL; + } + + public void reset(long timestamp, int ttl, long localExpirationTime) + { + this.timestamp = timestamp; + this.ttl = ttl; + this.localExpirationTime = localExpirationTime; + } + + @Override + public String toString() + { + return "ReusableLivenessInfo{" + ((timestamp == NO_TIMESTAMP && ttl == NO_TTL && localExpirationTime == NO_EXPIRATION_TIME) ? "NONE }" : + "timestamp=" + (timestamp == NO_TIMESTAMP ? "NO_TIMESTAMP" : timestamp) + + ", ttl=" + (ttl == NO_TTL ? "NO_TTL" : ttl) + + ", localExpirationTime=" + (localExpirationTime == NO_EXPIRATION_TIME ? "NO_EXPIRATION_TIME" : localExpirationTime) + + '}'); + } +} diff --git a/src/java/org/apache/cassandra/db/SerializationHeader.java b/src/java/org/apache/cassandra/db/SerializationHeader.java index 0c5fc53beb..61d87c847c 100644 --- a/src/java/org/apache/cassandra/db/SerializationHeader.java +++ b/src/java/org/apache/cassandra/db/SerializationHeader.java @@ -207,6 +207,14 @@ public class SerializationHeader return DeletionTime.build(markedAt, localDeletionTime); } + public void readDeletionTime(DataInputPlus in, DeletionTime.ReusableDeletionTime reuse) throws IOException + { + long markedAt = readTimestamp(in); + long localDeletionTime = readLocalDeletionTime(in); + reuse.reset(markedAt, localDeletionTime); + } + + public long timestampSerializedSize(long timestamp) { return TypeSizes.sizeofUnsignedVInt(timestamp - stats.minTimestamp); diff --git a/src/java/org/apache/cassandra/db/compaction/AbstractCompactionPipeline.java b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionPipeline.java new file mode 100644 index 0000000000..a01c154868 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/AbstractCompactionPipeline.java @@ -0,0 +1,74 @@ +/* + * 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.db.compaction; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.AbstractCompactionController; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +abstract class AbstractCompactionPipeline extends CompactionInfo.Holder implements AutoCloseable { + static AbstractCompactionPipeline create( + CompactionTask task, + OperationType type, + AbstractCompactionStrategy.ScannerList scanners, + AbstractCompactionController controller, + long nowInSec, + TimeUUID compactionId) + { + if (DatabaseDescriptor.cursorCompactionEnabled()) { + if (CursorCompactor.isSupported(scanners, controller)) + { + return new CursorCompactionPipeline(task, type, scanners, controller, nowInSec, compactionId); + } + } + return new IteratorCompactionPipeline(task, type, scanners, controller, nowInSec, compactionId); + } + + abstract boolean processNextPartitionKey() throws IOException; + + public abstract long[] getMergedRowCounts(); + + public abstract long getTotalSourceCQLRows(); + + public abstract long getTotalKeysWritten(); + + public abstract long getTotalBytesScanned(); + + public abstract AutoCloseable openWriterResource(ColumnFamilyStore cfs, + Directories directories, + ILifecycleTransaction transaction, + Set nonExpiredSSTables); + + @Override + public abstract void close() throws IOException; + + public abstract Collection finishWriting(); + + public abstract long estimatedKeys(); + + public abstract void stop(); +} diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java index b8eaa5bd81..c622582a07 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionStrategyManager.java @@ -820,7 +820,7 @@ public class CompactionStrategyManager implements INotificationConsumer * * lives in matches the list index of the holder that's responsible for it */ - public List groupSSTables(Iterable sstables) + public final List groupSSTables(Iterable sstables) { List classified = new ArrayList<>(holders.size()); for (AbstractStrategyHolder holder : holders) @@ -970,7 +970,7 @@ public class CompactionStrategyManager implements INotificationConsumer * @param ranges * @return */ - public AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection sstables, Collection> ranges) + public final AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection sstables, Collection> ranges) { maybeReloadDiskBoundaries(); List scanners = new ArrayList<>(sstables.size()); diff --git a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java index c9af97fe95..74da90f24e 100644 --- a/src/java/org/apache/cassandra/db/compaction/CompactionTask.java +++ b/src/java/org/apache/cassandra/db/compaction/CompactionTask.java @@ -70,7 +70,9 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime; public class CompactionTask extends AbstractCompactionTask { + private static final int MEGABYTE = 1024 * 1024; protected static final Logger logger = LoggerFactory.getLogger(CompactionTask.class); + protected final long gcBefore; protected final boolean keepOriginals; protected static long totalBytesCompacted = 0; @@ -151,9 +153,11 @@ public class CompactionTask extends AbstractCompactionTask * For internal use and testing only. The rest of the system should go through the submit* methods, * which are properly serialized. * Caller is in charge of marking/unmarking the sstables as compacting. + * + * NOTE: this method is a Byteman hook location */ @Override - protected void runMayThrow() throws Exception + protected final void runMayThrow() throws Exception { // The collection of sstables passed may be empty (but not null); even if // it is not empty, it may compact down to nothing if all rows are deleted. @@ -245,7 +249,7 @@ public class CompactionTask extends AbstractCompactionTask long nowInSec = FBUtilities.nowInSeconds(); try (Refs refs = Refs.ref(actuallyCompact); AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(actuallyCompact, rangeList); - CompactionIterator ci = new CompactionIterator(compactionType, scanners.scanners, controller, nowInSec, taskId)) + AbstractCompactionPipeline ci = AbstractCompactionPipeline.create(this, compactionType, scanners, controller, nowInSec, taskId)) { long lastCheckObsoletion = start; inputSizeBytes = scanners.getTotalCompressedSize(); @@ -253,30 +257,29 @@ public class CompactionTask extends AbstractCompactionTask if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) compressionRatio = 1.0; - long lastBytesScanned = 0; - activeCompactions.beginCompaction(ci); - try (CompactionAwareWriter writer = getCompactionAwareWriter(cfs, getDirectories(), transaction, actuallyCompact)) + try (AutoCloseable resource = getCompactionAwareWriter(actuallyCompact, ci)) { + long lastBytesScanned = 0; // Note that we need to re-check this flag after calling beginCompaction above to avoid a window // where the compaction does not exist in activeCompactions but the CSM gets paused. // We already have the sstables marked compacting here so CompactionManager#waitForCessation will // block until the below exception is thrown and the transaction is cancelled. if (!controller.cfs.getCompactionStrategyManager().isActive()) throw new CompactionInterruptedException(ci.getCompactionInfo()); - estimatedKeys = writer.estimatedKeys(); - while (ci.hasNext()) + estimatedKeys = ci.estimatedKeys(); + while (ci.processNextPartitionKey()) { - if (writer.append(ci.next())) - totalKeysWritten++; + long bytesScanned = ci.getTotalBytesScanned(); - ci.setTargetDirectory(writer.getSStableDirectory().path()); - long bytesScanned = scanners.getTotalBytesScanned(); + // If we ingested less than a MB, keep going + if (bytesScanned - lastBytesScanned > MEGABYTE) + { + // Rate limit the scanners, and account for compression + CompactionManager.instance.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); - // Rate limit the scanners, and account for compression - CompactionManager.instance.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); - - lastBytesScanned = bytesScanned; + lastBytesScanned = bytesScanned; + } if (nanoTime() - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L)) { @@ -284,19 +287,27 @@ public class CompactionTask extends AbstractCompactionTask lastCheckObsoletion = nanoTime(); } } + if (ci.getTotalBytesScanned() != lastBytesScanned) + { + // Report any leftover bytes + CompactionManager.instance.compactionRateLimiterAcquire(limiter, ci.getTotalBytesScanned(), lastBytesScanned, compressionRatio); + } timeSpentWritingKeys = TimeUnit.NANOSECONDS.toMillis(nanoTime() - start); // point of no return - newSStables = writer.finish(); + newSStables = finish(ci); } finally { activeCompactions.finishCompaction(ci); mergedRowCounts = ci.getMergedRowCounts(); totalSourceCQLRows = ci.getTotalSourceCQLRows(); + + totalKeysWritten = ci.getTotalKeysWritten(); } } + if (transaction.isOffline()) return; @@ -343,6 +354,28 @@ public class CompactionTask extends AbstractCompactionTask // update the metrics cfs.metric.compactionBytesWritten.inc(endsize); } + catch (Throwable e) + { + /** This should not be needed (because {@link org.apache.cassandra.utils.WrappedRunnable}) but some exceptions seem to slip by in tests */ + logger.debug("Unexpected exception in compaction", e); + throw e; + } + } + + /** + * NOTE: a Byteman hook + */ + protected Collection finish(AbstractCompactionPipeline pipeline) + { + return pipeline.finishWriting(); + } + + /** + * NOTE: a Byteman hook + */ + protected AutoCloseable getCompactionAwareWriter(Set actuallyCompact, AbstractCompactionPipeline pipeline) + { + return pipeline.openWriterResource(cfs, getDirectories(), transaction, actuallyCompact); } public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, diff --git a/src/java/org/apache/cassandra/db/compaction/CursorCompactionPipeline.java b/src/java/org/apache/cassandra/db/compaction/CursorCompactionPipeline.java new file mode 100644 index 0000000000..43a1040eb6 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CursorCompactionPipeline.java @@ -0,0 +1,112 @@ +/* + * 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.db.compaction; + +import org.apache.cassandra.db.AbstractCompactionController; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +class CursorCompactionPipeline extends AbstractCompactionPipeline { + final CursorCompactor cursorCompactor; + final CompactionTask task; + long totalKeysWritten; + CompactionAwareWriter writer; + + CursorCompactionPipeline(CompactionTask task, OperationType type, AbstractCompactionStrategy.ScannerList scanners, AbstractCompactionController controller, long nowInSec, TimeUUID compactionId) { + this.task = task; + cursorCompactor = new CursorCompactor(type, scanners.scanners, controller, nowInSec, compactionId); + } + + public AutoCloseable openWriterResource(ColumnFamilyStore cfs, + Directories directories, + ILifecycleTransaction transaction, + Set nonExpiredSSTables) { + this.writer = task.getCompactionAwareWriter(cfs, directories, transaction, nonExpiredSSTables); + return writer; + } + + + @Override + public Collection finishWriting() { + return writer.finish(); + } + + @Override + public long estimatedKeys() { + return writer.estimatedKeys(); + } + + @Override + public CompactionInfo getCompactionInfo() { + return cursorCompactor.getCompactionInfo(); + } + + @Override + public boolean isGlobal() { + return cursorCompactor.isGlobal(); + } + + @Override + boolean processNextPartitionKey() throws IOException { + if (cursorCompactor.writeNextPartition(writer)) { + totalKeysWritten++; + cursorCompactor.setTargetDirectory(writer.getSStableDirectoryPath()); + return true; + } + return false; + } + + @Override + public long[] getMergedRowCounts() { + return cursorCompactor.getMergedRowsCounts(); + } + + @Override + public long getTotalSourceCQLRows() { + return cursorCompactor.getTotalSourceCQLRows(); + } + + @Override + public long getTotalKeysWritten() { + return totalKeysWritten; + } + + @Override + public long getTotalBytesScanned() { + return cursorCompactor.getTotalBytesScanned(); + } + + @Override + public void close() throws IOException { + cursorCompactor.close(); + } + + @Override + public void stop() { + cursorCompactor.stop(); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java b/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java new file mode 100644 index 0000000000..fe67974f26 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/CursorCompactor.java @@ -0,0 +1,1644 @@ +/* + * 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.db.compaction; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.LongPredicate; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.UnmodifiableIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime; +import org.apache.cassandra.dht.ReusableDecoratedKey; +import org.apache.cassandra.io.sstable.UnfilteredDescriptor; +import org.apache.cassandra.io.sstable.PartitionDescriptor; +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.io.sstable.SSTableCursorWriter; +import org.apache.cassandra.db.AbstractCompactionController; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DeletionPurger; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.SystemKeyspace; +import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.BTreeRow; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Cells; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.io.sstable.format.SortedTableWriter; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.sstable.format.big.BigFormat; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.CompactionParams; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.TimeUUID; + +import static org.apache.cassandra.db.compaction.CursorCompactor.CellResolution.COMPARE; +import static org.apache.cassandra.db.compaction.CursorCompactor.CellResolution.LEFT; +import static org.apache.cassandra.db.compaction.CursorCompactor.CellResolution.RIGHT; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_END; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_HEADER_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_VALUE_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.DONE; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.UNFILTERED_END; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.PARTITION_END; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.PARTITION_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.ROW_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.STATIC_ROW_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.TOMBSTONE_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.isState; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_END_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.EXCL_START_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_END_BOUND; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY; +import static org.apache.cassandra.db.ClusteringPrefix.Kind.INCL_START_BOUND; + +/** + * Compacts the contents of 1..n sstables into a 1..m sstables. The compaction is driven one output partition at a time + * by the {@link CursorCompactionPipeline}. + *

+ * Compaction here implies: + *

    + *
  • Merge source sstable data, such that only latest live values, or tombstones, are present in the output.
  • + *
  • Purge gc-able tombstones if possible (see PurgeFunction below).
  • + *
  • Invalidate cached partitions that are empty post-compaction. This avoids keeping partitions with + * only purgable tombstones in the row cache.
  • + *
  • Keep tracks of the compaction progress.
  • + *
+ * This compaction implementation does not support 2ndary indexes or trie indexes at this time. + *

+* This compaction implmentation avoids garbage creation per partition/row/cell by utilizing reader/writer code +* which supports reusable copies of sstable entry components. The implementation consolidates and duplicates code + * from various classes to support the use of these reusable structures. + *

+ */ +public class CursorCompactor extends CompactionInfo.Holder +{ + public static boolean isSupported(AbstractCompactionStrategy.ScannerList scanners, AbstractCompactionController controller) + { + TableMetadata metadata = controller.cfs.metadata(); + if (unsupportedMetadata(metadata)) return false; + + for (ISSTableScanner scanner : scanners.scanners) + { + // TODO: implement partial range reader + if (!scanner.isFullRange()) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Partial scanners are not supported."); + return false; + } + + for (SSTableReader reader : scanner.getBackingSSTables()) { + Version version = reader.descriptor.version; + if (!version.isLatestVersion()) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Older sstable versions are not supported. version=" + version); + return false; + } + } + } + // BTI index writing is not supported yet + if (!(DatabaseDescriptor.getSelectedSSTableFormat() instanceof BigFormat)) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Only BIG sstable output format is supported. format=" + DatabaseDescriptor.getSelectedSSTableFormat()); + return false; + } + // TODO: Implement CompactionIterator.GarbageSkipper like functionality + if (controller.tombstoneOption != CompactionParams.TombstoneOption.NONE) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Garbage skipping not implemented. controller.tombstoneOption=" + controller.tombstoneOption); + return false; + } + if (LOGGER.isDebugEnabled()) LOGGER.debug("Cursor compaction for table: " + metadata.name + " keyspace: " + metadata.keyspace + " is supported."); + + return true; + } + + public static boolean unsupportedMetadata(TableMetadata metadata) + { + if (!metadata.partitioner.supportsReusableKeys()) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Incompatible partitioner, does not support reusable keys:" + metadata.partitioner.getClass().getSimpleName()); + return true; + } + + if (metadata.indexes.size() != 0) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Additional indexes are not supported. metadata.indexes=" + metadata.indexes); + return true; + } + + if (unsupportedSchema(metadata)) + return true; + return false; + } + + private static boolean unsupportedSchema(TableMetadata metadata) + { + // Cell value merge limitations + for (ColumnMetadata column : metadata.regularAndStaticColumns()) + { + if (column.isComplex()) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Complex columns are not supported. column=" + column); + return true; + } + else if (column.isCounterColumn()) + { + if (LOGGER.isDebugEnabled()) logDebugReason(metadata, "Counter columns are not supported. column=" + column); + return true; + } + } + return false; + } + + private static void logDebugReason(TableMetadata metadata, String reason) + { + LOGGER.debug("Cursor compaction for table: " + metadata.name + " keyspace: " + metadata.keyspace + " is not supported. REASON: " + reason); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(CursorCompactor.class.getName()); + + private final OperationType type; + private final AbstractCompactionController controller; + private final ActiveCompactionsTracker activeCompactions; + private final ImmutableSet sstables; + private final long nowInSec; + private final TimeUUID compactionId; + private final long totalInputBytes; + private final StatefulCursor[] sstableCursors; + private final boolean[] sstableCursorsEqualsNext; + private final boolean hasStaticColumns; + private final boolean enforceStrictLiveness; + + // Keep targetDirectory for compactions, needed for `nodetool compactionstats` + private volatile String targetDirectory; + + private SSTableCursorWriter ssTableCursorWriter; + private boolean finished = false; + + /* + * counters for merged partitions/rows/cells. + * array index represents (number of merged rows - 1), so index 0 is counter for no merge (1 row), + * index 1 is counter for 2 rows merged, and so on. + */ + private final long[] partitionMergeCounters; + private final long[] staticRowMergeCounters; + private final long[] rowMergeCounters; + private final long[] rangeTombstonesMergeCounters; + private final long[] cellMergeCounters; + + // Progress accounting + private long totalBytesRead = 0; + private long totalSourceCQLRows; + private long totalDataBytesWritten; + + // state + final Purger purger; + + private StatefulCursor lastSource = null; + private final ReusableDecoratedKey detachedLastWrittenKey; + + // Partition state. Writes can be delayed if the deletion is purged, or live and partition is empty -> LIVE deletion. + PartitionDescriptor partitionDescriptor; + + // This will be 0 if we haven't written partition header. + int partitionHeaderLength = 0; + private CompactionAwareWriter compactionAwareWriter; + + public CursorCompactor(OperationType type, List scanners, AbstractCompactionController controller, long nowInSec, TimeUUID compactionId) + { + this(type, scanners, controller, nowInSec, compactionId, ActiveCompactionsTracker.NOOP); + } + + private CursorCompactor(OperationType type, + List scanners, + AbstractCompactionController controller, + long nowInSec, + TimeUUID compactionId, + ActiveCompactionsTracker activeCompactions) + { + this.controller = controller; + this.type = type; + this.nowInSec = nowInSec; + this.compactionId = compactionId; + + long inputBytes = 0; + for (ISSTableScanner scanner : scanners) + inputBytes += scanner.getLengthInBytes(); + this.totalInputBytes = inputBytes; + this.partitionMergeCounters = new long[scanners.size()]; + this.staticRowMergeCounters = new long[partitionMergeCounters.length]; + this.rowMergeCounters = new long[partitionMergeCounters.length]; + this.rangeTombstonesMergeCounters = new long[partitionMergeCounters.length]; + this.cellMergeCounters = new long[partitionMergeCounters.length]; + // note that we leak `this` from the constructor when calling beginCompaction below, this means we have to get the sstables before + // calling that to avoid a NPE. + this.sstables = scanners.stream().map(ISSTableScanner::getBackingSSTables).flatMap(Collection::stream).collect(ImmutableSet.toImmutableSet()); + // This is always NOOP, but keep it around in case we need it later to match CompactionIterator + this.activeCompactions = activeCompactions == null ? ActiveCompactionsTracker.NOOP : activeCompactions; + this.activeCompactions.beginCompaction(this); // note that CompactionTask also calls this, but CT only creates CompactionIterator with a NOOP ActiveCompactions + + TableMetadata metadata = metadata(); + this.hasStaticColumns = metadata.hasStaticColumns(); + /** + * Pipeline should end up similar to the one in {@link CompactionIterator}: + * [MERGED -> ?TopPartitionTracker -> GarbageSkipper -> Purger -> org.apache.cassandra.db.transform.DuplicateRowChecker -> Abortable] -> next() + * V - Merge - This is drawing on code all over the place to iterate through the data and merge partitions/rows/cells + * * {@link org.apache.cassandra.db.transform.Transformation}s, applied to above iterator: + * X - Not needed for CompactionTask usage: {@link org.apache.cassandra.metrics.TopPartitionTracker.TombstoneCounter} + * X - Unsupported {@link CompactionIterator.GarbageSkipper} - filters out, or "skips" data shadowed by the provided "tombstone source". + * V - {@link CompactionIterator.Purger} - filters out, or "purges" gc-able tombstones. Also updates bytes read on every row % 100. + * X - Not needed for latest version tables: {@link org.apache.cassandra.db.transform.DuplicateRowChecker} + * V - Abortable - aborts the compaction if the user has requested it (at a certain granularity). + * {@link CompactionIterator#CompactionIterator(OperationType, List, AbstractCompactionController, long, TimeUUID, ActiveCompactionsTracker)} + */ + + // Convert Readers to Cursors + this.sstableCursors = new StatefulCursor[sstables.size()]; + this.sstableCursorsEqualsNext = new boolean[sstables.size()]; + UnmodifiableIterator iterator = sstables.iterator(); + for (int i = 0; i < this.sstableCursors.length; i++) + { + SSTableReader ssTableReader = iterator.next(); + this.sstableCursors[i] = new StatefulCursor(ssTableReader); + } + this.enforceStrictLiveness = controller.cfs.metadata.get().enforceStrictLiveness(); + + purger = new Purger(type, controller, nowInSec); + + detachedLastWrittenKey = metadata.partitioner.createReusableKey(128); + } + + /** + * @return false if finished, true if partition is written (which might require multiple partition reads) + */ + public boolean writeNextPartition(CompactionAwareWriter compactionAwareWriter) throws IOException { + while (!finished) { + if (tryWriteNextPartition(compactionAwareWriter)) { + return true; + } + } + return false; + } + + /** + * @return true if a partition was written + */ + private boolean tryWriteNextPartition(CompactionAwareWriter compactionAwareWriter) throws IOException + { + if (isStopRequested()) + throw new CompactionInterruptedException(getCompactionInfo()); + + int partitionMergeLimit = prepareAndSortForPartitionMerge(); + if (partitionMergeLimit == 0) + { + finish(); + return false; + } + // Top reader is on the current key/header + StatefulCursor currSource = sstableCursors[0]; + partitionDescriptor = currSource.currPartition(); + + // possibly reached boundary of the current writer + try + { + DecoratedKey key = partitionDescriptor.key(); + if (lastSource != null && currSource != lastSource && lastWrittenKey().compareTo(key) >= 0) + throw new IllegalStateException(String.format("Last written key %s >= current key %s", lastWrittenKey(), key)); + + // needed if we actually write a partition, not used otherwise + this.compactionAwareWriter = compactionAwareWriter; + + purger.resetOnNewPartition(key); + boolean written = mergePartitions(partitionMergeLimit); + if (!written) + { + purger.onEmptyPartitionPostPurge(); + // skipping a partition means the prevPartition on the lastSource is no longer stable, copy it + if (detachedLastWrittenKey.getKeyLength() == 0 && lastSource != null) + detachedLastWrittenKey.copyKey(lastSource.prevKey().getKey()); + } + else + { + // last source has will have the prev key recorded, reset the copy + if (detachedLastWrittenKey.getKeyLength() != 0) + detachedLastWrittenKey.reset(); + } + return written; + } + finally + { + lastSource = currSource; + partitionDescriptor = null; + partitionHeaderLength = 0; + } + } + + /** + * See {@link UnfilteredPartitionIterators#merge(List, UnfilteredPartitionIterators.MergeListener)} + */ + private boolean mergePartitions(int partitionMergeLimit) throws IOException + { + partitionMergeCounters[partitionMergeLimit - 1]++; + + // Pick "max" pDeletion + /** {@link UnfilteredRowIterators.UnfilteredRowMergeIterator#collectPartitionLevelDeletion(List, UnfilteredRowIterators.MergeListener)}*/ + final DeletionTime mergedDeletion = mergePartitionDeletions(partitionMergeLimit); + + // maybe purge? If the partition is written out, this will be the deletion we write. + final DeletionTime toWritePartitionDeletion = maybePurgedOutputDeletion(mergedDeletion); + if (toWritePartitionDeletion != DeletionTime.LIVE) { + startPartition(toWritePartitionDeletion); + } + // active deletion tracks the open deletion within a partition, so will change to track range tombstones + DeletionTime activeDeletion = mergedDeletion; + + // Merge any common static rows + if (hasStaticColumns) + { + int staticRowMergeLimit = prepareAndSortStaticForMerge(partitionMergeLimit); + if (staticRowMergeLimit != 0) + { + mergeRows(staticRowMergeLimit, activeDeletion, true, false); + } + if (isPartitionStarted()) + { + if (staticRowMergeLimit == 0) ssTableCursorWriter.writeEmptyStaticRow(); + partitionHeaderLength = (int) (ssTableCursorWriter.getPosition() - ssTableCursorWriter.getPartitionStart()); + } + } + + // Merge any common normal rows + int unfilteredMergeLimit = partitionMergeLimit; + boolean isFirstUnfiltered = true; + int unfilteredCount = 0; + UnfilteredDescriptor lastClustering = null; + while (true) + { + unfilteredMergeLimit = prepareAndSortUnfilteredForMerge(partitionMergeLimit, unfilteredMergeLimit); + if (unfilteredMergeLimit == 0) + break; + int flags = sstableCursors[0].unfiltered().flags(); + if (UnfilteredSerializer.isRow(flags)) + { + if (mergeRows(unfilteredMergeLimit, activeDeletion, false, isFirstUnfiltered)) + { + isFirstUnfiltered = false; + unfilteredCount++; + lastClustering = sstableCursors[0].unfiltered(); + } + } + else if (UnfilteredSerializer.isTombstoneMarker(flags)) { + // the tombstone processing *maybe* writes a marker, and *maybe* changes the `activeOpenRangeDeletion` + if (mergeRangeTombstones(unfilteredMergeLimit, mergedDeletion, isFirstUnfiltered)) + { + isFirstUnfiltered = false; + unfilteredCount++; + lastClustering = sstableCursors[0].unfiltered(); + } + if (activeOpenRangeDeletion == DeletionTime.LIVE) { + activeDeletion = mergedDeletion; + } + else { + activeDeletion = activeOpenRangeDeletion; + } + } + else { + throw new IllegalStateException("Unexpected unfiltered type (not row or tombstone):" + flags); + } + // move along + continueReadingAfterMerge(unfilteredMergeLimit, UNFILTERED_END); + } + + boolean partitionWritten = isPartitionStarted(); + if (partitionWritten) + { + ssTableCursorWriter.writePartitionEnd(partitionDescriptor.keyBytes(), partitionDescriptor.keyLength(), toWritePartitionDeletion, partitionHeaderLength); + // update metadata tracking of min/max clustering on last unfiltered + if (unfilteredCount > 1) { + ssTableCursorWriter.updateClusteringMetadata(lastClustering); + } + } + // move along + continueReadingAfterMerge(partitionMergeLimit, PARTITION_END); + return partitionWritten; + } + + private void startPartition(DeletionTime toWritePartitionDeletion) throws IOException + { + maybeSwitchWriter(compactionAwareWriter); + partitionHeaderLength = ssTableCursorWriter.writePartitionStart( + partitionDescriptor.keyBytes(), + partitionDescriptor.keyLength(), + toWritePartitionDeletion); + } + + private DeletionTime maybePurgedOutputDeletion(DeletionTime mergedDeletion) throws IOException + { + final DeletionTime toWritePartitionDeletion; + + if (!mergedDeletion.isLive() && !purger.shouldPurge(mergedDeletion)) + { + toWritePartitionDeletion = mergedDeletion; + } + else + { + toWritePartitionDeletion = DeletionTime.LIVE; + } + return toWritePartitionDeletion; + } + + private DeletionTime mergePartitionDeletions(int partitionMergeLimit) + { + DeletionTime mergedDeletion = partitionDescriptor.deletionTime(); + for (int i = 1; i < partitionMergeLimit; i++) + { + DeletionTime otherDeletionTime = sstableCursors[i].currPartition().deletionTime(); + if (!mergedDeletion.supersedes(otherDeletionTime)) + mergedDeletion = otherDeletionTime; + } + return mergedDeletion; + } + + /** + * We have a common clustering and need to merge data. Cells might be different in different rows, but collision is + * likely at this stage (probably). + * {@link Row.Merger#merge(DeletionTime)} + */ + private boolean mergeRows(int rowMergeLimit, DeletionTime partitionActiveDeletion, boolean isStatic, boolean isFirstUnfiltered) throws IOException + { + if (isStopRequested()) + throw new CompactionInterruptedException(getCompactionInfo()); + + if (isStatic) + { + staticRowMergeCounters[rowMergeLimit - 1]++; + } + else + { + rowMergeCounters[rowMergeLimit - 1]++; + } + + // merge deletion/liveness + /** {@link Row.Merger#merge(DeletionTime)}*/ + UnfilteredDescriptor row = sstableCursors[0].unfiltered(); + + LivenessInfo mergedRowInfo = row.livenessInfo(); + DeletionTime mergedRowDeletion = row.deletionTime(); + + for (int i = 1; i < rowMergeLimit; i++) + { + // TODO: can validate state here + row = sstableCursors[i].unfiltered(); + // TODO: maybe flags more optimal(avoid ref loads and comaparisons etc) + if (row.livenessInfo().supersedes(mergedRowInfo)) + mergedRowInfo = row.livenessInfo(); + if (row.deletionTime().supersedes(mergedRowDeletion)) + mergedRowDeletion = row.deletionTime(); + } + + /** + * See: {@link BTreeRow#purge(DeletionPurger, long, boolean)} + */ + DeletionTime rowActiveDeletion = partitionActiveDeletion; + if (mergedRowDeletion.supersedes(rowActiveDeletion)) + { + rowActiveDeletion = mergedRowDeletion; // deletion is in effect before purge takes effect + mergedRowDeletion = purger.shouldPurge(mergedRowDeletion) ? DeletionTime.LIVE : mergedRowDeletion; + } + else + { + // partition delete takes over + mergedRowDeletion = DeletionTime.LIVE; + } + + if (rowActiveDeletion.deletes(mergedRowInfo) || purger.shouldPurge(mergedRowInfo, nowInSec)) + { + mergedRowInfo = LivenessInfo.EMPTY; + } + + boolean isRowDropped = mergedRowDeletion.isLive() && mergedRowInfo.isEmpty(); + + if (!isRowDropped) + { + lateStartRow(mergedRowInfo, mergedRowDeletion, isStatic); + } + + if (isRowDropped && enforceStrictLiveness) + { + skipRowsOnStrictLiveness(rowMergeLimit, isStatic); + } + else + { + int cellMergeLimit = rowMergeLimit; + // loop through the columns and copy/merge each cell + while (true) + { + // advance cursors that need to read the cell header + for (int i = 0; i < cellMergeLimit; i++) + { + int readerState = sstableCursors[i].state(); + if (readerState == CELL_HEADER_START) + { + sstableCursors[i].readCellHeader(); + } + } + // Sort rows by cells + cellMergeLimit = prepareAndSortCellsForMerge(rowMergeLimit, cellMergeLimit); + if (cellMergeLimit == 0) + break; + isRowDropped = mergeCells(cellMergeLimit, rowActiveDeletion, mergedRowInfo, isRowDropped, isStatic); + // move along + continueReadingAfterMerge(cellMergeLimit, CELL_END); + } + if (!isRowDropped) + ssTableCursorWriter.writeRowEnd(sstableCursors[0].unfiltered(), isFirstUnfiltered); + } + if (isRowDropped && isStatic && + isPartitionStarted()) + // if the partition write has not started, keep delaying it, might be an empty partition (purged+no data) + { + ssTableCursorWriter.writeEmptyStaticRow(); + } + return !isRowDropped; + } + + private void skipRowsOnStrictLiveness(int rowMergeLimit, boolean isStatic) throws IOException + { + for (int i = 0; i < rowMergeLimit; i++) + { + if (sstableCursors[i].state() != UNFILTERED_END){ + if (isStatic) + sstableCursors[i].skipStaticRow(); + else + sstableCursors[i].skipUnfiltered(); + } + } + } + + private DataOutputBuffer tempCellBuffer1 = new DataOutputBuffer(); + private DataOutputBuffer tempCellBuffer2 = new DataOutputBuffer(); + private final byte[] copyColumnValueBuffer = new byte[4096]; // used to copy cell contents (maybe piecemeal if very large, since we don't have a direct read option) + + /** + * {@link Row.Merger.ColumnDataReducer#getReduced()} <-- applied the delete before reconcile, should not make a difference? + * {@link Cells#reconcile(Cell, Cell)} + */ + private boolean mergeCells(int cellMergeLimit, DeletionTime activeDeletion, LivenessInfo rowLiveness, boolean isRowDropped, boolean isStatic) throws IOException + { + cellMergeCounters[cellMergeLimit - 1]++; + // Nothing to sort, we basically need to pick the correct data to copy. + // -> the latest data. + // TODO: handle counters/complex cells + StatefulCursor cellSource = sstableCursors[0]; + SSTableCursorReader.CellCursor cellCursor = cellSource.cellCursor(); + ReusableLivenessInfo cellLiveness = cellCursor.cellLiveness; + DataOutputBuffer tempCellBuffer = null; + + if (cellCursor.cellColumn.isComplex()) + throw new UnsupportedOperationException("TODO: Not ready for complex cells."); + if (cellCursor.cellColumn.isCounterColumn()) + throw new UnsupportedOperationException("TODO: Not ready for counter cells."); + + /** See: {@link Cells#reconcile(Cell, Cell)} */ + // Find latest cell value/delete info, only one cell can win(for now... same timestamp handling awaits)! + for (int i = 1; i < cellMergeLimit; i++) + { + StatefulCursor oCellSource = sstableCursors[i]; + SSTableCursorReader.CellCursor oCellCursor = oCellSource.cellCursor(); + ReusableLivenessInfo oCellLiveness = oCellCursor.cellLiveness; + + CellResolution cellResolution = resolveRegular(cellLiveness, oCellLiveness); + if (cellResolution == LEFT) { + if (oCellSource.state() == CELL_VALUE_START) oCellSource.skipCellValue(); + } + else if (cellResolution == RIGHT) { + if (cellSource.state() == CELL_VALUE_START) cellSource.skipCellValue(); + cellSource = oCellSource; + cellCursor = oCellCursor; + cellLiveness = oCellLiveness; + tempCellBuffer = null; + } + else { // COMPARE + if (activeDeletion.deletes(oCellLiveness)) { + if (oCellSource.state() == CELL_VALUE_START) oCellSource.skipCellValue(); + } + else { + // copy out the values for comparison + if (cellSource.state() == CELL_VALUE_START) + { + if (tempCellBuffer != null) + throw new IllegalStateException("tempCellBuffer should be null if cellSource has a value to be read."); + tempCellBuffer1.clear(); + + cellSource.copyCellValue(tempCellBuffer1, copyColumnValueBuffer); + + tempCellBuffer = tempCellBuffer1; // assume cell1 is going to be bigger + } + else if (tempCellBuffer == null) { + // potential trash value in buffer1 + tempCellBuffer1.clear(); + } + else if (tempCellBuffer != tempCellBuffer1) { + throw new IllegalStateException("tempCellBuffer should be tempCellBuffer1 if cellSource has been read."); + } + tempCellBuffer2.clear(); + if (oCellSource.state() == CELL_VALUE_START) + oCellSource.copyCellValue(tempCellBuffer2, copyColumnValueBuffer); + + int compare = Arrays.compareUnsigned(tempCellBuffer1.getData(), 0, tempCellBuffer1.getLength(), tempCellBuffer2.getData(), 0, tempCellBuffer2.getLength()); + if (compare >= 0) { + // swap the buffers + tempCellBuffer = tempCellBuffer1; + tempCellBuffer1 = tempCellBuffer2; + tempCellBuffer2 = tempCellBuffer; + + // tempCellBuffer != null -> tempCellBuffer == tempCellBuffer1 + tempCellBuffer = tempCellBuffer1; + + cellSource = oCellSource; + cellCursor = oCellCursor; + cellLiveness = oCellLiveness; + } + } + } + } + + + /** + * {@link Cell.Serializer#serialize} + */ + int cellFlags = cellCursor.cellFlags; + + /** {@link org.apache.cassandra.db.rows.AbstractCell#purge(org.apache.cassandra.db.DeletionPurger, long)} */ + // if `isExpiring` => has ttl, and TTL has lapsed, convert the TTL to a tombstone + if (Cell.Serializer.isExpiring(cellFlags) && cellLiveness.isExpired(nowInSec)) { + cellLiveness.ttlToTombstone(); + // remove the value, this is a tombstone now + if (Cell.Serializer.hasValue(cellFlags)) + { + cellFlags = cellFlags | Cell.Serializer.HAS_EMPTY_VALUE_MASK; + if (cellSource.state() == CELL_VALUE_START) + { + if (tempCellBuffer != null) throw new IllegalStateException("Either copied buffer or ready to copy reader, not both."); + cellSource.skipCellValue(); + } + else if (tempCellBuffer != null) { + tempCellBuffer = null; + } + else + { + throw new IllegalStateException("Flags and state contradict"); + } + } + } + + if (activeDeletion.deletes(cellLiveness) || purger.shouldPurge(cellLiveness, nowInSec)) + { + if (Cell.Serializer.hasValue(cellFlags)) + { + // we're dropping the cell, but could do: cellFlags = cellFlags | Cell.Serializer.HAS_EMPTY_VALUE_MASK; + if (cellSource.state() == CELL_VALUE_START) + { + if (tempCellBuffer != null) throw new IllegalStateException("Either copied buffer or ready to copy reader, not both."); + cellSource.skipCellValue(); + } + else if (tempCellBuffer != null) { + // we're dropping the cell, but could do: tempCellBuffer = null; + } + else + { + throw new IllegalStateException("Flags and state contradict"); + } + } + } + else + { + if (isRowDropped) + { + isRowDropped = false; + lateStartRow(isStatic); + } + /** {@link org.apache.cassandra.db.rows.Cell.Serializer#serialize(Cell, ColumnMetadata, DataOutputPlus, LivenessInfo, SerializationHeader)} */ + boolean isDeleted = cellLiveness.isTombstone(); + boolean isExpiring = cellLiveness.isExpiring(); + boolean useRowTimestamp = !rowLiveness.isEmpty() && cellLiveness.timestamp() == rowLiveness.timestamp(); + boolean useRowTTL = isExpiring && rowLiveness.isExpiring() && + cellLiveness.ttl() == rowLiveness.ttl() && + cellLiveness.localExpirationTime() == rowLiveness.localExpirationTime(); + // Re-write cell flags to reflect resulting contents + cellFlags &= Cell.Serializer.HAS_EMPTY_VALUE_MASK; + if (isDeleted) cellFlags |= Cell.Serializer.IS_DELETED_MASK; + if (isExpiring) cellFlags |= Cell.Serializer.IS_EXPIRING_MASK; + if (useRowTimestamp) cellFlags |= Cell.Serializer.USE_ROW_TIMESTAMP_MASK; + if (useRowTTL) cellFlags |= Cell.Serializer.USE_ROW_TTL_MASK; + ssTableCursorWriter.writeCellHeader(cellFlags, cellLiveness, cellSource.cellCursor().cellColumn); + if (Cell.Serializer.hasValue(cellFlags)) { + if (cellSource.state() == CELL_VALUE_START) + { + if (tempCellBuffer != null) throw new IllegalStateException("Either copied buffer or ready to copy reader, not both."); + ssTableCursorWriter.writeCellValue(cellSource, copyColumnValueBuffer); + } + else if (tempCellBuffer != null) + { + ssTableCursorWriter.writeCellValue(tempCellBuffer); + } + else + { + throw new IllegalStateException("Flags and state contradict"); + } + } + + } + return isRowDropped; + } + + enum CellResolution + { + LEFT, RIGHT, COMPARE + } + + private static CellResolution resolveRegular(LivenessInfo left, LivenessInfo right) + { + long leftTimestamp = left.timestamp(); + long rightTimestamp = right.timestamp(); + if (leftTimestamp != rightTimestamp) + return leftTimestamp > rightTimestamp ? LEFT : RIGHT; + + long leftLocalDeletionTime = left.localExpirationTime(); + long rightLocalDeletionTime = right.localExpirationTime(); + + boolean leftIsExpiringOrTombstone = leftLocalDeletionTime != Cell.NO_DELETION_TIME; + boolean rightIsExpiringOrTombstone = rightLocalDeletionTime != Cell.NO_DELETION_TIME; + + if (leftIsExpiringOrTombstone | rightIsExpiringOrTombstone) + { + // Tombstones always win reconciliation with live cells of the same timstamp + // CASSANDRA-14592: for consistency of reconciliation, regardless of system clock at time of reconciliation + // this requires us to treat expiring cells (which will become tombstones at some future date) the same wrt regular cells + if (leftIsExpiringOrTombstone != rightIsExpiringOrTombstone) + return leftIsExpiringOrTombstone ? LEFT : RIGHT; + + // for most historical consistency, we still prefer tombstones over expiring cells. + // While this leads to an inconsistency over which is chosen + // (i.e. before expiry, the pure tombstone; after expiry, whichever is more recent) + // this inconsistency has no user-visible distinction, as at this point they are both logically tombstones + // (the only possible difference is the time at which the cells become purgeable) + boolean leftIsTombstone = !left.isExpiring(); // !isExpiring() == isTombstone(), but does not need to consider localDeletionTime() + boolean rightIsTombstone = !right.isExpiring(); + if (leftIsTombstone != rightIsTombstone) + return leftIsTombstone ? LEFT : RIGHT; + + // ==> (leftIsExpiring && rightIsExpiring) or (leftIsTombstone && rightIsTombstone) + // if both are expiring, we do not want to consult the value bytes if we can avoid it, as like with C-14592 + // the value bytes implicitly depend on the system time at reconciliation, as a + // would otherwise always win (unless it had an empty value), until it expired and was translated to a tombstone + if (leftLocalDeletionTime != rightLocalDeletionTime) + return leftLocalDeletionTime > rightLocalDeletionTime ? LEFT : RIGHT; + } + return COMPARE; + } + + DeletionTime activeOpenRangeDeletion = DeletionTime.LIVE; + final List openMarkers = new ArrayList<>(); + final ArrayDeque reusableMarkersPool = new ArrayDeque<>(); + + /** + * We have a common clustering and need to merge tombstones. Alternatively, we have a series of range tombstones + * whose intersections mutate from bounds into boundary (a combination of 2 bounds). We also need to purge any GC'ed + * deletes. + * + * {@link RangeTombstoneMarker.Merger#merge()} + * + * @return true if written, false otherwise + */ + private boolean mergeRangeTombstones(int rangeTombstoneMergeLimit, DeletionTime partitionDeletion, boolean isFirstUnfiltered) throws IOException + { + if (rangeTombstoneMergeLimit == 0) + { + throw new IllegalStateException(); + } + rangeTombstonesMergeCounters[rangeTombstoneMergeLimit - 1]++; + DeletionTime previousDeletionTimeInMerged = DeletionTime.LIVE; + if (activeOpenRangeDeletion != DeletionTime.LIVE) { + previousDeletionTimeInMerged = getDeletionTimeReusableCopy(activeOpenRangeDeletion); + } + try + { + updateOpenMarkers(rangeTombstoneMergeLimit, partitionDeletion); + + DeletionTime newDeletionTimeInMerged = activeOpenRangeDeletion; + if (previousDeletionTimeInMerged.equals(newDeletionTimeInMerged)) + return false; + + // we will stomp on the unfiltered descriptor and write it out + UnfilteredDescriptor rangeTombstone = sstableCursors[0].unfiltered(); + boolean isBeforeClustering = rangeTombstone.clusteringKind().comparedToClustering < 0; + + // Combining the merge and purge code + if (previousDeletionTimeInMerged == DeletionTime.LIVE) + { + if (purger.shouldPurge(newDeletionTimeInMerged)) + { + return false; + } + else + { + rangeTombstone.clusteringKind(isBeforeClustering ? INCL_START_BOUND : EXCL_START_BOUND); + rangeTombstone.deletionTime().reset(newDeletionTimeInMerged); + } + } + else if (newDeletionTimeInMerged == DeletionTime.LIVE) + { + if (purger.shouldPurge(previousDeletionTimeInMerged)) + { + return false; + } + else + { + rangeTombstone.clusteringKind(isBeforeClustering ? EXCL_END_BOUND : INCL_END_BOUND); + rangeTombstone.deletionTime().reset(previousDeletionTimeInMerged); + } + } + else + { + boolean shouldPurgeClose = purger.shouldPurge(previousDeletionTimeInMerged); + boolean shouldPurgeOpen = purger.shouldPurge(newDeletionTimeInMerged); + + if (shouldPurgeClose && shouldPurgeOpen) + return false; + + if (shouldPurgeClose) + { + rangeTombstone.clusteringKind(isBeforeClustering ? INCL_START_BOUND : EXCL_START_BOUND); + rangeTombstone.deletionTime().reset(newDeletionTimeInMerged); + } + else if (shouldPurgeOpen) + { + rangeTombstone.clusteringKind(isBeforeClustering ? EXCL_END_BOUND : INCL_END_BOUND); + rangeTombstone.deletionTime().reset(previousDeletionTimeInMerged); + } + else { + // Boundary + rangeTombstone.clusteringKind(isBeforeClustering ? EXCL_END_INCL_START_BOUNDARY : INCL_END_EXCL_START_BOUNDARY); + rangeTombstone.deletionTime().reset(previousDeletionTimeInMerged); // close + rangeTombstone.deletionTime2().reset(newDeletionTimeInMerged); // open + } + } + + if (isPartitionStartDelayed()) + { + lateStartPartition(false); + ssTableCursorWriter.writeRangeTombstone(rangeTombstone, true); + } + else { + ssTableCursorWriter.writeRangeTombstone(rangeTombstone, isFirstUnfiltered); + } + return true; + } + finally + { + if (previousDeletionTimeInMerged != DeletionTime.LIVE) + { + reusableMarkersPool.offer((ReusableDeletionTime) previousDeletionTimeInMerged); + } + } + } + + private void updateOpenMarkers(int rangeTombstoneMergeLimit, DeletionTime partitionDeletion) + { + /** Similar to {@link RangeTombstoneMarker.Merger#updateOpenMarkers()} but we validate a close exists for every open.*/ + for (int i = 0; i < rangeTombstoneMergeLimit; i++) + { + UnfilteredDescriptor rangeTombstone = sstableCursors[i].unfiltered(); + if (rangeTombstone.isStartBound()) + { + DeletionTime openRangeDeletion = rangeTombstone.deletionTime(); + addOpenRangeDeletion(partitionDeletion, openRangeDeletion); + } + else if (rangeTombstone.isEndBound()) + { + DeletionTime closeRangeDeletion = rangeTombstone.deletionTime(); + removeOpenRangeDeletion(partitionDeletion, closeRangeDeletion, rangeTombstone); + } + else if (rangeTombstone.isBoundary()) + { + DeletionTime closeRangeDeletion = rangeTombstone.deletionTime(); + removeOpenRangeDeletion(partitionDeletion, closeRangeDeletion, rangeTombstone); + DeletionTime openRangeDeletion = rangeTombstone.deletionTime2(); + addOpenRangeDeletion(partitionDeletion, openRangeDeletion); + } + else + throw new IllegalStateException("Unexpected bound type:" + rangeTombstone.clusteringKind()); + } + + if (activeOpenRangeDeletion == null) + { + recalculateActiveOpen(); + } + } + + private void recalculateActiveOpen() + { + // active open has been invalidated by a close bound matching it, need to scan the list for new max + int size = openMarkers.size(); + if (size == 0) + { + activeOpenRangeDeletion = DeletionTime.LIVE; + return; + } + // find max open marker + DeletionTime maxOpenDeletion = openMarkers.get(0); + for (int i = 1; i < size; i++) + { + DeletionTime openDeletionTime = openMarkers.get(i); + if (openDeletionTime.supersedes(maxOpenDeletion)) + maxOpenDeletion = openDeletionTime; + } + activeOpenRangeDeletion = maxOpenDeletion; + } + + private void removeOpenRangeDeletion(DeletionTime partitionDeletion, DeletionTime closeRangeDeletion, UnfilteredDescriptor rangeTombstone) + { + // filter out markers that are deleted by the `partitionDelete` + if (partitionDeletion != DeletionTime.LIVE && !closeRangeDeletion.supersedes(partitionDeletion)) + { + return; + } + // a close marker should have a matching open in the list + int j = 0; + int size = openMarkers.size(); + ReusableDeletionTime reusableOpenMarker = null; + for (; j < size;j++) { + reusableOpenMarker = openMarkers.get(j); + if (reusableOpenMarker.equals(closeRangeDeletion)) + break; + } + if (j == size) + throw new IllegalStateException("Expected an open marker for this closing marker:" + rangeTombstone); + + reusableMarkersPool.offer(reusableOpenMarker); + if (activeOpenRangeDeletion == reusableOpenMarker) { + // trigger recalculation + activeOpenRangeDeletion = null; + } + if (size == 1) { + openMarkers.clear(); + } + else { + // avoid expensive array copy, take the last element + ReusableDeletionTime deletionTime = openMarkers.remove(size - 1); + if (j != size - 1) + { + // overwrite the matched marker (if it was not the last one) + openMarkers.set(j, deletionTime); + } + } + } + + private void addOpenRangeDeletion(DeletionTime partitionDeletion, DeletionTime openRangeDeletion) + { + // filter out markers that are deleted by the `partitionDelete` + if (partitionDeletion != DeletionTime.LIVE && !openRangeDeletion.supersedes(partitionDeletion)) + { + return; + } + + ReusableDeletionTime reusable = getDeletionTimeReusableCopy(openRangeDeletion); + openMarkers.add(reusable); + if (activeOpenRangeDeletion != null && // invalidated by remove, so full scan is required + (activeOpenRangeDeletion == DeletionTime.LIVE || reusable.supersedes(activeOpenRangeDeletion))) { + activeOpenRangeDeletion = reusable; + } + } + + private ReusableDeletionTime getDeletionTimeReusableCopy(DeletionTime openRangeDeletion) + { + ReusableDeletionTime reusable = reusableMarkersPool.pollLast(); + if (reusable == null) { + reusable = ReusableDeletionTime.copy(openRangeDeletion); + } + else { + reusable.reset(openRangeDeletion); + } + return reusable; + } + + private boolean isPartitionStarted() + { + return partitionHeaderLength != 0; + } + + private boolean isPartitionStartDelayed() + { + return !isPartitionStarted(); + } + + private void continueReadingAfterMerge(int mergeLimit, int endState) + { + for (int i = 0; i < mergeLimit; i++) + { + if (sstableCursors[i].state() == endState){ + sstableCursors[i].continueReading(); + } + } + } + + private void lateStartRow(boolean isStatic) throws IOException + { + lateStartRow(LivenessInfo.EMPTY, DeletionTime.LIVE, isStatic); + } + + private void lateStartRow(LivenessInfo livenessInfo, DeletionTime deletionTime, boolean isStatic) throws IOException + { + if (isPartitionStartDelayed()) + { + lateStartPartition(isStatic); + } + ssTableCursorWriter.writeRowStart(livenessInfo, deletionTime, isStatic); + } + + private void lateStartPartition(boolean isStatic) throws IOException + { + startPartition(DeletionTime.LIVE); + // Did we miss writing an empty static row? + if (!isStatic) + { + if(ssTableCursorWriter.writeEmptyStaticRow()) + partitionHeaderLength = (int) (ssTableCursorWriter.getPosition() - ssTableCursorWriter.getPartitionStart()); + } + } + + private void finish() + { + // only finish writing once + if (!finished) + { + finished = true; + writerRollover(); + } + } + + private void maybeSwitchWriter(CompactionAwareWriter writerProvider) + { + assert !finished; + // Set last key, so this is ready to be closed. + SSTableWriter newWriter = writerProvider.maybeSwitchWriter(partitionDescriptor.key()); + if (newWriter != null) + { + writerRollover(); + + ssTableCursorWriter = new SSTableCursorWriter((SortedTableWriter) newWriter); + ssTableCursorWriter.setFirst(partitionDescriptor.keyBuffer()); + } + assert ssTableCursorWriter != null; + } + + private void writerRollover() + { + if (ssTableCursorWriter != null) { + totalDataBytesWritten += ssTableCursorWriter.getPosition(); + ssTableCursorWriter.setLast(lastWrittenKey().getKey()); + } + ssTableCursorWriter = null; + } + + private DecoratedKey lastWrittenKey() + { + if (detachedLastWrittenKey.getKeyLength() != 0) + return detachedLastWrittenKey; + else + return lastSource.prevKey(); + } + + // SORT AND COMPARE + + /** + * Prepares the cursors array for partition merge. + *

+ * The cursors are in one of 3 states: + *

    + *
  • PARTITION_START - Partition header needs to be loaded in preparation for merge. This is the starting state of all cursors.
  • + *
  • STATIC_ROW_START | ROW_START | TOMBSTONE_START | PARTITION_END - header is loaded. Already sorted.
  • + *
  • DONE - Exhausted cursors. This is the end state of all cursors.
  • + *
+ * After each `mergePartitions` iteration, the recently progressed cursors are at the beginning of the array and are + * either at a new PARTITION_START or DONE. + * We prepare all the cursors in the PARTITION_START state for sorting by loading the key and delete time. We also + * need to push all the DONE cursors to the back of the list. + * + * Once the bounds of the sorting are known we insert sort the freshly read/done cursors into the pre-sorted + * remaining array. After the sort we find the next merge limit, which is to say how many of the top partition keys + * are equal. + * + * @return the next merge limit, or 0 if all cursors are DONE + */ + private int prepareAndSortForPartitionMerge() throws IOException + { + // start by loading in new partition keys from any readers for which we just merged partitions => are + // on partition edge. Exhausted cursors are at the bottom. Mid-read partitions are in the middle. + int perturbedCursors = 0; + for (; perturbedCursors < sstableCursors.length; perturbedCursors++) + { + StatefulCursor sstableCursor = sstableCursors[perturbedCursors]; + int sstableCursorState = sstableCursor.state(); + + if (sstableCursorState == PARTITION_START) + { + sstableCursor.readPartitionHeader(); + updateTotalBytesRead(sstableCursor); + } + else if (isState(sstableCursorState, STATIC_ROW_START | ROW_START | TOMBSTONE_START | PARTITION_END)) + { + // The cursors after this point are sorted, and unmoved + break; + } + else if (sstableCursorState == DONE) + { + if (sstableCursor.resetAfterDone()) + { + updateTotalBytesRead(sstableCursor); + } + else + { + break; + } + } + else + { + throw new IllegalStateException("Cursor is in an unexpected state:" + sstableCursor); + } + } + // no cursors were moved => all done + if (perturbedCursors == 0) + { + assert sstableCursors.length == 0 || sstableCursors[0].state() == DONE; + return 0; + } + + sortPerturbedCursors(perturbedCursors, sstableCursors.length, CursorCompactor::compareByPartitionKey); + // top cursor is DONE -> all cursors are DONE + int state = sstableCursors[0].state(); + if(state == DONE) + { + return 0; + } + assert isState(state, STATIC_ROW_START | ROW_START | TOMBSTONE_START | PARTITION_END); + + int partitionMergeLimit = 1; + for (; partitionMergeLimit < sstableCursors.length; partitionMergeLimit++) + { + if (!sstableCursorsEqualsNext[partitionMergeLimit-1]) + break; + } + return partitionMergeLimit; + } + + + private int prepareAndSortUnfilteredForMerge(int partitionMergeLimit, int prevMergeLimit) throws IOException + { + // move cursors that need to move passed the row header + for (int i = 0; i < prevMergeLimit; i++) + { + StatefulCursor sstableCursor = sstableCursors[i]; + int readerState = sstableCursor.state(); + if (readerState == ROW_START) + { + totalSourceCQLRows++; + sstableCursor.readRowHeader(); + } + if (readerState == TOMBSTONE_START) + { + sstableCursor.readTombstoneMarker(); + } + if (readerState == STATIC_ROW_START) + throw new IllegalStateException("Unexpected static row after static row merge:" + sstableCursor); + } + + // Sort rows by their clustering + sortPerturbedCursors(prevMergeLimit, partitionMergeLimit, CursorCompactor::compareByRowClustering); + int state = sstableCursors[0].state(); + if (state == PARTITION_END) + { + return 0; + } + assert isState(state, UNFILTERED_END | CELL_HEADER_START); + int unfilteredMergeLimit = 1; + for (; unfilteredMergeLimit < partitionMergeLimit; unfilteredMergeLimit++) + { + if (!sstableCursorsEqualsNext[unfilteredMergeLimit-1]) + break; + } + return unfilteredMergeLimit; + } + + private int prepareAndSortStaticForMerge(int partitionMergeLimit) throws IOException + { + sortPerturbedCursors(partitionMergeLimit, partitionMergeLimit, CursorCompactor::compareByStatic); + int state = sstableCursors[0].state(); + if (state != STATIC_ROW_START) + { + assert isState(state, ROW_START|TOMBSTONE_START|PARTITION_END); + return 0; + } + totalSourceCQLRows++; + sstableCursors[0].readStaticRowHeader(); + int staticRowMergeLimit = 1; + for (; staticRowMergeLimit < partitionMergeLimit; staticRowMergeLimit++) + { + if (sstableCursorsEqualsNext[staticRowMergeLimit - 1]) + { + totalSourceCQLRows++; + sstableCursors[staticRowMergeLimit].readStaticRowHeader(); + } + else + break; + } + + return staticRowMergeLimit; + } + + private int prepareAndSortCellsForMerge(int rowMergeLimit, int prevCellMergeLimit) + { + sortPerturbedCursors(prevCellMergeLimit, rowMergeLimit, CursorCompactor::compareByColumn); + // next row/partition/done + if (sstableCursors[0].state() == UNFILTERED_END) + return 0; + + int state = sstableCursors[0].state(); + if (isState(state, UNFILTERED_END | CELL_HEADER_START)) + return 0; + + int cellMergeLimit = 1; + for (; cellMergeLimit < rowMergeLimit; cellMergeLimit++) + { + if (!sstableCursorsEqualsNext[cellMergeLimit - 1]) + break; + } + return cellMergeLimit; + } + + private static int compareByPartitionKey(StatefulCursor c1, StatefulCursor c2) + { + if (c1 == c2) return 0; + int tint = c1.state(); + int oint = c2.state(); + if (tint == DONE && oint == DONE) return 0; + if (tint == DONE) return 1; + if (oint == DONE) return -1; + return c1.currentKey().compareTo(c2.currentKey()); + } + + private static int compareByStatic(StatefulCursor c1, StatefulCursor c2) + { + if (c1 == c2) return 0; + int tState = c1.state(); + int oState = c2.state(); + + if (tState == PARTITION_END && oState == PARTITION_END) return 0; + if (tState == PARTITION_END) return 1; + if (oState == PARTITION_END) return -1; + + return -Boolean.compare(tState == STATIC_ROW_START, oState == STATIC_ROW_START); + } + + private static int compareByRowClustering(StatefulCursor c1, StatefulCursor c2) + { + if (c1 == c2) return 0; + int tState = c1.state(); + int oState = c2.state(); + + if (tState == PARTITION_END && oState == PARTITION_END) return 0; + if (tState == PARTITION_END) return 1; + if (oState == PARTITION_END) return -1; + // Either have cells, or an empty row + boolean tIsAfterHeader = isState(tState, CELL_HEADER_START | UNFILTERED_END); + boolean oIsAfterHeader = isState(oState, CELL_HEADER_START | UNFILTERED_END); + if (tIsAfterHeader && oIsAfterHeader) + return ClusteringComparator.compare(c1.unfiltered(), c2.unfiltered()); + else + throw new IllegalStateException("We only sort through rows ready to be merged/copied. c1 = " + c1 + ", c2 = " + c2); + } + + private static int compareByColumn(StatefulCursor c1, StatefulCursor c2) + { + if (c1 == c2) return 0; + int tState = c1.state(); + int oState = c2.state(); + if (tState == UNFILTERED_END && oState == UNFILTERED_END) return 0; + if (tState == UNFILTERED_END) return 1; + if (oState == UNFILTERED_END) return -1; + + boolean tIsAfterHeader = isState(tState, CELL_VALUE_START | CELL_END); + boolean oIsAfterHeader = isState(oState, CELL_VALUE_START | CELL_END); + if (tIsAfterHeader && oIsAfterHeader) + return c1.cellCursor().cellColumn.compareTo(c2.cellCursor().cellColumn); + else + throw new IllegalStateException("We only sort through cells ready to be merged/copied. c1 = " + c1 + ", c2 = " + c2); + } + + // Purge + + /** + * We are combining code from: + * - {@link org.apache.cassandra.db.compaction.CompactionIterator.Purger} + * - {@link org.apache.cassandra.db.partitions.PurgeFunction} + * - {@link DeletionPurger} + * The original code leans on the {@link org.apache.cassandra.db.transform.Transformation} abstraction and the + * iterator infrastructure which is not fit for purpose here. + */ + static class Purger implements DeletionPurger + { + private final long nowInSec; + + private final long oldestUnrepairedTombstone; + private final boolean onlyPurgeRepairedTombstones; + private final boolean shouldIgnoreGcGraceForAnyKey; + private final OperationType type; + + private boolean ignoreGcGraceSeconds; + private final AbstractCompactionController controller; + + private DecoratedKey partitionKey; + private LongPredicate purgeEvaluator; + + private long compactedUnfiltered; + + Purger(OperationType type, AbstractCompactionController controller, long nowInSec) + { + oldestUnrepairedTombstone = controller.compactingRepaired() ? Long.MAX_VALUE : Integer.MIN_VALUE; + onlyPurgeRepairedTombstones = controller.cfs.getCompactionStrategyManager().onlyPurgeRepairedTombstones(); + shouldIgnoreGcGraceForAnyKey = controller.cfs.shouldIgnoreGcGraceForAnyKey(); + this.nowInSec = nowInSec; + this.controller = controller; + this.type = type; + } + + void resetOnNewPartition(DecoratedKey key) + { + partitionKey = key; + purgeEvaluator = null; + ignoreGcGraceSeconds = shouldIgnoreGcGraceForAnyKey && controller.cfs.shouldIgnoreGcGraceForKey(partitionKey); + } + + void onEmptyPartitionPostPurge() + { + if (type == OperationType.COMPACTION) + controller.cfs.invalidateCachedPartition(partitionKey); + } + + @Override + public boolean shouldPurge(long timestamp, long localDeletionTime) + { + return !(onlyPurgeRepairedTombstones && localDeletionTime >= oldestUnrepairedTombstone) + && (localDeletionTime < controller.gcBefore || ignoreGcGraceSeconds) + && getPurgeEvaluator().test(timestamp); + } + + /* + * Evaluates whether a tombstone with the given deletion timestamp can be purged. This is the minimum + * timestamp for any sstable containing `currentKey` outside of the set of sstables involved in this compaction. + * This is computed lazily on demand as we only need this if there is tombstones and this a bit expensive + * (see #8914). + */ + private LongPredicate getPurgeEvaluator() + { + if (purgeEvaluator == null) + { + purgeEvaluator = controller.getPurgeEvaluator(partitionKey); + } + return purgeEvaluator; + } + } + + // ACCOUNTING CODE + public TableMetadata metadata() + { + return controller.cfs.metadata(); + } + + public CompactionInfo getCompactionInfo() + { + return new CompactionInfo(controller.cfs.metadata(), + type, + getBytesRead(), + totalInputBytes, + compactionId, + sstables, + targetDirectory); + } + + public boolean isGlobal() + { + return false; + } + + public void setTargetDirectory(final String targetDirectory) + { + this.targetDirectory = targetDirectory; + } + + public long[] getMergedParitionsCounts() + { + return partitionMergeCounters; + } + + public long[] getMergedRowsCounts() + { + return rowMergeCounters; + } + + public long[] getMergedCellsCounts() + { + return cellMergeCounters; + } + + public long getTotalSourceCQLRows() + { + return totalSourceCQLRows; + } + + public long getBytesRead() + { + return totalBytesRead; + } + + private void updateTotalBytesRead(StatefulCursor cursor) + { + totalBytesRead += cursor.bytesReadSinceSnapshot(); + } + + public String toString() + { + return this.getCompactionInfo().toString(); + } + + public long getTotalBytesScanned() + { + return getBytesRead(); + } + + private static boolean isPaxos(ColumnFamilyStore cfs) + { + return cfs.name.equals(SystemKeyspace.PAXOS) && cfs.getKeyspaceName().equals(SchemaConstants.SYSTEM_KEYSPACE_NAME); + } + + private long sumHistogram(long[] histogram) + { + long sum = 0; + for (long count : histogram) + { + sum += count; + } + return sum; + } + + private static String mergeHistogramToString(long[] histogram) + { + StringBuilder sb = new StringBuilder(); + long sum = 0; + sb.append("["); + for (int i = 0; i < histogram.length; i++) + { + if (histogram[i] != 0) + { + sb.append(i + 1).append(":").append(histogram[i]).append(", "); + sum += (i + 1) * histogram[i]; + } + } + if (sb.length() > 1) + sb.setLength(sb.length() - 1); //trim trailing comma + sb.append("] = ").append(sum); + return sb.toString(); + } + + public void close() + { + try + { + finish(); + + for (SSTableCursorReader reader : sstableCursors) + { + reader.close(); + } + } + finally + { + activeCompactions.finishCompaction(this); + } + + if (LOGGER.isInfoEnabled()) + { + LOGGER.info("Compaction ended {}: { data bytes read = {}, data bytes written = {}, " + + " input (keys = {}, rows = {}, cells = {}), " + + " output (keys = {}, rows = {}, cells = {})}", + this.compactionId, getTotalBytesScanned(), totalDataBytesWritten, + mergeHistogramToString(partitionMergeCounters), mergeHistogramToString(rowMergeCounters), mergeHistogramToString(cellMergeCounters), + sumHistogram(partitionMergeCounters), sumHistogram(rowMergeCounters), sumHistogram(cellMergeCounters)); + } + } + + private void sortPerturbedCursors(int perturbedLimit, int mergeLimit, Comparator comparator) { + for (; perturbedLimit > 0; perturbedLimit--) { + bubbleInsertElementToPreSorted(sstableCursors, sstableCursorsEqualsNext, perturbedLimit, mergeLimit, comparator); + } + } + + /** + * Use bubble sort to insert the sortedFrom - 1 element into a pre-sorted array, and track element + * equality to next element to help in finding merge ranges. + *

+ * We use this method to sort the cursor array on 3 levels: + *
    + *
  • Partition - insert sort the newly read partitions into the full list, comparing on pKey
  • + *
  • Unfiltered - insert sort the newly read rows into the sub-list of merging partitions, comparing on clustering
  • + *
  • Cell - insert sort the newly read cells into the sub-list of merging rows, comparing on column
  • + *
+ * + * @param preSortedArray partially pre-sorted array of elements to be sorted in place + * @param equalsNext tracking the equality between each element and the next in the sorted array + * @param sortedFrom elements from sortedFrom are assumed sorted + * @param sortedTo the limit of our sort effort + * @param comparator comparing elements in the array + * @param element type + */ + public static void bubbleInsertElementToPreSorted(T[] preSortedArray, + boolean[] equalsNext, + int sortedFrom, + int sortedTo, + Comparator comparator) + { + int insertInto = sortedFrom - 1; + T newElement = preSortedArray[insertInto]; + for (; insertInto < sortedTo - 1; insertInto++) + { + int cmp = comparator.compare(newElement, preSortedArray[insertInto + 1]); + if (cmp < 0) + { + equalsNext[insertInto] = false; + break; + } + else if (cmp == 0) { + equalsNext[insertInto] = true; + break; + } + else + { + // skip the section of equal elements who are smaller than the new element + for (; insertInto < sortedTo - 1; insertInto++) + { + if (!equalsNext[insertInto + 1]) + { + break; + } + preSortedArray[insertInto] = preSortedArray[insertInto + 1]; + equalsNext[insertInto] = true; + } + preSortedArray[insertInto] = preSortedArray[insertInto + 1]; + equalsNext[insertInto] = false; + } + } + preSortedArray[insertInto] = newElement; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/db/compaction/IteratorCompactionPipeline.java b/src/java/org/apache/cassandra/db/compaction/IteratorCompactionPipeline.java new file mode 100644 index 0000000000..f049e9bab4 --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/IteratorCompactionPipeline.java @@ -0,0 +1,115 @@ +/* + * 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.db.compaction; + +import org.apache.cassandra.db.AbstractCompactionController; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter; +import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TimeUUID; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +class IteratorCompactionPipeline extends AbstractCompactionPipeline { + final CompactionIterator ci; + final AbstractCompactionStrategy.ScannerList scanners; + final CompactionTask task; + long totalKeysWritten; + CompactionAwareWriter writer; + + IteratorCompactionPipeline(CompactionTask task, OperationType type, AbstractCompactionStrategy.ScannerList scanners, AbstractCompactionController controller, long nowInSec, TimeUUID compactionId) { + this.task = task; + this.scanners = scanners; + ci = new CompactionIterator(type, this.scanners.scanners, controller, nowInSec, compactionId); + } + + public AutoCloseable openWriterResource(ColumnFamilyStore cfs, + Directories directories, + ILifecycleTransaction transaction, + Set nonExpiredSSTables) { + this.writer = task.getCompactionAwareWriter(cfs, directories, transaction, nonExpiredSSTables); + return writer; + } + + + @Override + public Collection finishWriting() { + return writer.finish(); + } + + @Override + public long estimatedKeys() { + return writer.estimatedKeys(); + } + + @Override + public CompactionInfo getCompactionInfo() { + return ci.getCompactionInfo(); + } + + @Override + public boolean isGlobal() { + return ci.isGlobal(); + } + + @Override + boolean processNextPartitionKey() throws IOException { + if (ci.hasNext()) { + if (writer.append(ci.next())) + totalKeysWritten++; + ci.setTargetDirectory(writer.getSStableDirectoryPath()); + return true; + } + return false; + } + + @Override + public long[] getMergedRowCounts() { + return ci.getMergedRowCounts(); + } + + @Override + public long getTotalSourceCQLRows() { + return ci.getTotalSourceCQLRows(); + } + + @Override + public long getTotalKeysWritten() { + return totalKeysWritten; + } + + @Override + public long getTotalBytesScanned() { + return scanners.getTotalBytesScanned(); + } + + @Override + public void close() throws IOException { + ci.close(); + } + + @Override + public void stop() { + ci.stop(); + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java index e0664a9bc6..b424ec5e99 100644 --- a/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java +++ b/src/java/org/apache/cassandra/db/compaction/LeveledCompactionStrategy.java @@ -297,6 +297,7 @@ public class LeveledCompactionStrategy extends AbstractCompactionStrategy return levelFanoutSize; } + @Override public ScannerList getScanners(Collection sstables, Collection> ranges) { Set[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot(); @@ -432,7 +433,12 @@ public class LeveledCompactionStrategy extends AbstractCompactionStrategy assert sstableIterator.hasNext(); // caller should check intersecting first SSTableReader currentSSTable = sstableIterator.next(); currentScanner = currentSSTable.getScanner(ranges); + } + @Override + public boolean isFullRange() + { + return ranges == null; } public static Collection intersecting(Collection sstables, Collection> ranges) diff --git a/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java b/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java new file mode 100644 index 0000000000..e7bed313fe --- /dev/null +++ b/src/java/org/apache/cassandra/db/compaction/StatefulCursor.java @@ -0,0 +1,273 @@ +/* + * 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.db.compaction; + +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.UnfilteredValidation; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.sstable.PartitionDescriptor; +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.io.sstable.UnfilteredDescriptor; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.db.rows.Cell.INVALID_DELETION_TIME; +import static org.apache.cassandra.db.rows.Cell.NO_DELETION_TIME; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_END; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_HEADER_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.CELL_VALUE_START; +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.isState; + +// Cursor state +class StatefulCursor extends SSTableCursorReader +{ + private final Config.CorruptedTombstoneStrategy corruptedTombstoneStrategy = DatabaseDescriptor.getCorruptedTombstoneStrategy(); + private final boolean corruptedTombstoneValidationEnabled = corruptedTombstoneStrategy != Config.CorruptedTombstoneStrategy.disabled; + + private PartitionDescriptor currPartition; + /** + * used for tracking reader and writer order, as well as last pk + */ + private PartitionDescriptor prevPartition; + + private final UnfilteredDescriptor unfiltered; + + private boolean resetAfterDone = false; + private long bytesReadPositionSnapshot = 0; + + private boolean isOpenRangeTombstonePresent = false; + + public StatefulCursor(SSTableReader reader) + { + super(reader); + currPartition = new PartitionDescriptor(reader.getPartitioner().createReusableKey(0)); + prevPartition = new PartitionDescriptor(reader.getPartitioner().createReusableKey(0)); + unfiltered = new UnfilteredDescriptor(reader.header.clusteringTypes().toArray(AbstractType[]::new)); + } + + public int readPartitionHeader() + { + swapCurrAndPrevPartition(); + int state = readPartitionHeader(currPartition); + + if (prevPartition.keyLength() != 0 && prevPartition.key().compareTo(currPartition.key()) >= 0) + corruptSSTableKeysOOO(); + if (corruptedTombstoneValidationEnabled) + validateInvalidPartitionDeletion(); + return state; + } + + private int corruptSSTableKeysOOO() + { + return corruptSSTable("Keys out of order. Current key: " + keyToString(currentKey()) + " <= " + keyToString(prevKey())); + } + + private void swapCurrAndPrevPartition() + { + PartitionDescriptor temp = currPartition; + currPartition = prevPartition; + prevPartition = temp; + } + + public int skipUnfiltered() + { + if (isState(state(), CELL_HEADER_START | CELL_VALUE_START | CELL_END)) + return super.skipRowCells(unfiltered().dataStart(), unfiltered().size(), false); + + return super.skipUnfiltered(false); + } + + public int skipStaticRow() + { + if (isState(state(), CELL_HEADER_START | CELL_VALUE_START | CELL_END)) + return super.skipRowCells(unfiltered().dataStart(), unfiltered().size(), false); + + return super.skipStaticRow(false); + } + + @Override + public String toString() + { + return "StatefulCursor{" + + "pHeader=" + currPartition() + + ", rHeader=" + unfiltered() + + ", state=" + state() + + '}'; + } + + /** + * @return true if reset, false if already been reset + */ + public boolean resetAfterDone() + { + if (resetAfterDone) + return false; + resetAfterDone = true; + swapCurrAndPrevPartition(); + // only current is reset, prev is still needed. + currPartition().resetPartition(); + unfiltered().resetUnfiltered(); + return true; + } + + DecoratedKey currentKey() + { + return currPartition.key(); + } + + DecoratedKey prevKey() + { + return prevPartition.key(); + } + + public PartitionDescriptor currPartition() + { + return currPartition; + } + + public UnfilteredDescriptor unfiltered() + { + return unfiltered; + } + + public long bytesReadSinceSnapshot() + { + long latestByteReadPosition = isEOF() ? uncompressedLength() : position(); + long cursorBytesRead = latestByteReadPosition - bytesReadPositionSnapshot; + bytesReadPositionSnapshot = latestByteReadPosition; + return cursorBytesRead; + } + + private String keyToString(DecoratedKey key) + { + String keyString; + try + { + keyString = ssTableReader().metadata().partitionKeyType.getString(key.getKey()); + } + catch (Throwable t) + { + keyString = "[corrupt token="+key.getToken()+"]"; + } + return keyString; + } + + public void readRowHeader() + { + super.readRowHeader(unfiltered); + if (corruptedTombstoneValidationEnabled) + validateInvalidRowDeletion(); + } + + public void readTombstoneMarker() + { + super.readTombstoneMarker(unfiltered); + + if (corruptedTombstoneValidationEnabled) + validateInvalidTombstoneDeletion(); + + boolean isStartBound = unfiltered.isStartBound(); + if (isOpenRangeTombstonePresent && isStartBound) + corruptSSTable("Encountered an open range tombstone marker before the prev was closed: " + unfiltered); + if (!isOpenRangeTombstonePresent && !isStartBound) + corruptSSTable("Encountered an close/boundary range tombstone marker before an open one: " + unfiltered); + isOpenRangeTombstonePresent = isStartBound || unfiltered.isBoundary(); + // TODO: can also add verification of open/close timestamp match + } + + public void readStaticRowHeader() + { + super.readStaticRowHeader(unfiltered); + if (corruptedTombstoneValidationEnabled) + validateInvalidRowDeletion(); + } + + @Override + public int readCellHeader() + { + int state = super.readCellHeader(); + if (corruptedTombstoneValidationEnabled) + validateInvalidCellDeletion(); + return state; + } + + private void validateInvalidTombstoneDeletion() + { + if (!unfiltered.deletionTime().validate()) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "rowDeletion="+currPartition.deletionTime().toString()); + } + if (unfiltered.isBoundary() && !unfiltered.deletionTime2().validate()) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "rowDeletion2="+currPartition.deletionTime().toString()); + } + } + + private void validateInvalidCellDeletion() + { + ReusableLivenessInfo cellLiveness = cellCursor().cellLiveness; + long ldt = cellLiveness.localExpirationTime(); + if (cellLiveness.ttl() < 0 || ldt == INVALID_DELETION_TIME || ldt < 0 || (cellLiveness.isExpiring() && ldt == NO_DELETION_TIME)) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "cellLiveness="+cellLiveness); + } + } + + private void validateInvalidRowDeletion() + { + if (!unfiltered.deletionTime().validate()) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "rowDeletion="+currPartition.deletionTime().toString()); + } + ReusableLivenessInfo livenessInfo = unfiltered.livenessInfo(); + if (livenessInfo.isExpiring() && (livenessInfo.ttl() < 0 || livenessInfo.localExpirationTime() < 0)) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "rowLiveness="+livenessInfo.toString()); + } + + } + + private void validateInvalidPartitionDeletion() + { + if (!currPartition.deletionTime().validate()) { + UnfilteredValidation.handleInvalid( + ssTableReader().metadata(), + currPartition.key(), + ssTableReader(), + "partitionLevelDeletion="+currPartition.deletionTime().toString()); + } + } +} diff --git a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java index fd1966ad35..2369b3cbb7 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/CompactionAwareWriter.java @@ -40,7 +40,6 @@ import org.apache.cassandra.io.sstable.SSTableRewriter; import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableWriter; import org.apache.cassandra.io.sstable.metadata.MetadataCollector; -import org.apache.cassandra.io.util.File; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.concurrent.Transactional; @@ -69,7 +68,7 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa private final List diskBoundaries; private int locationIndex; protected Directories.DataDirectory currentDirectory; - + protected String sstableDirectoryPath; public CompactionAwareWriter(ColumnFamilyStore cfs, Directories directories, ILifecycleTransaction txn, @@ -151,9 +150,10 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa return realAppend(partition); } - public final File getSStableDirectory() throws IOException + // hot path, called per partition + public final String getSStableDirectoryPath() throws IOException { - return getDirectories().getLocationForDisk(currentDirectory); + return sstableDirectoryPath; } @Override @@ -173,35 +173,36 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa * specific strategy has decided a new sstable is needed. * Guaranteed to be called before the first call to realAppend. */ - protected void maybeSwitchWriter(DecoratedKey key) + public final SSTableWriter maybeSwitchWriter(DecoratedKey key) { - if (maybeSwitchLocation(key)) - return; - - if (shouldSwitchWriterInCurrentLocation(key)) - switchCompactionWriter(currentDirectory, key); + SSTableWriter newWriter = maybeSwitchLocation(key); + if (newWriter == null && shouldSwitchWriterInCurrentLocation(key)) + { + newWriter = switchCompactionWriter(currentDirectory, key); + } + return newWriter; } /** * Switches the file location and writer and returns true if the new key should be placed in a different data * directory. */ - protected boolean maybeSwitchLocation(DecoratedKey key) + private SSTableWriter maybeSwitchLocation(DecoratedKey key) { if (diskBoundaries == null) { if (locationIndex < 0) { Directories.DataDirectory defaultLocation = getWriteDirectory(nonExpiredSSTables, getExpectedWriteSize()); - switchCompactionWriter(defaultLocation, key); + SSTableWriter writer = switchCompactionWriter(defaultLocation, key); locationIndex = 0; - return true; + return writer; } - return false; + return null; } if (locationIndex > -1 && key.compareTo(diskBoundaries.get(locationIndex)) < 0) - return false; + return null; int prevIdx = locationIndex; while (locationIndex == -1 || key.compareTo(diskBoundaries.get(locationIndex)) > 0) @@ -209,8 +210,7 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa Directories.DataDirectory newLocation = locations.get(locationIndex); if (prevIdx >= 0) logger.debug("Switching write location from {} to {}", locations.get(prevIdx), newLocation); - switchCompactionWriter(newLocation, key); - return true; + return switchCompactionWriter(newLocation, key); } /** @@ -223,14 +223,14 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa * Implementations of this method should finish the current sstable writer and start writing to this directory. *

* Called once before starting to append and then whenever we see a need to start writing to another directory. - * - * @param directory - * @param nextKey */ - protected void switchCompactionWriter(Directories.DataDirectory directory, DecoratedKey nextKey) + protected SSTableWriter switchCompactionWriter(Directories.DataDirectory directory, DecoratedKey nextKey) { currentDirectory = directory; - sstableWriter.switchWriter(sstableWriter(directory, nextKey)); + sstableDirectoryPath = getDirectories().getLocationForDisk(currentDirectory).path(); + SSTableWriter newWriter = sstableWriter(directory, nextKey); + sstableWriter.switchWriter(newWriter); + return newWriter; } protected SSTableWriter sstableWriter(Directories.DataDirectory directory, DecoratedKey nextKey) diff --git a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java index d0fb70587c..455817aaff 100644 --- a/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java +++ b/src/java/org/apache/cassandra/db/compaction/writers/MajorLeveledCompactionWriter.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.compaction.LeveledManifest; import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; public class MajorLeveledCompactionWriter extends CompactionAwareWriter { @@ -87,12 +88,12 @@ public class MajorLeveledCompactionWriter extends CompactionAwareWriter } @Override - public void switchCompactionWriter(Directories.DataDirectory location, DecoratedKey nextKey) + public SSTableWriter switchCompactionWriter(Directories.DataDirectory location, DecoratedKey nextKey) { averageEstimatedKeysPerSSTable = Math.round(((double) averageEstimatedKeysPerSSTable * sstablesWritten + partitionsWritten) / (sstablesWritten + 1)); partitionsWritten = 0; sstablesWritten = 0; - super.switchCompactionWriter(location, nextKey); + return super.switchCompactionWriter(location, nextKey); } protected int sstableLevel() diff --git a/src/java/org/apache/cassandra/db/guardrails/MaxThreshold.java b/src/java/org/apache/cassandra/db/guardrails/MaxThreshold.java index 6203ee2bf5..9f0d260818 100644 --- a/src/java/org/apache/cassandra/db/guardrails/MaxThreshold.java +++ b/src/java/org/apache/cassandra/db/guardrails/MaxThreshold.java @@ -53,14 +53,14 @@ public class MaxThreshold extends Threshold } @Override - protected long failValue(ClientState state) + public long failValue(ClientState state) { long failValue = failThreshold.applyAsLong(state); return failValue <= 0 ? Long.MAX_VALUE : failValue; } @Override - protected long warnValue(ClientState state) + public long warnValue(ClientState state) { long warnValue = warnThreshold.applyAsLong(state); return warnValue <= 0 ? Long.MAX_VALUE : warnValue; diff --git a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java index fd7880e367..aec80ba1cd 100644 --- a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java +++ b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java @@ -288,7 +288,7 @@ public abstract class UnfilteredPartitionIterators } /** - * Digests the the provided iterator. + * Digests the provided iterator. * * Caller must close the provided iterator. * diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java index bf6b5b5061..8bccda8aa7 100644 --- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java +++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java @@ -80,13 +80,15 @@ public class BTreeRow extends AbstractRow private static final Comparator COLUMN_COMPARATOR = (cd1, cd2) -> cd1.column.compareTo(cd2.column); - // We need to filter the tombstones of a row on every read (twice in fact: first to remove purgeable tombstone, and then after reconciliation to remove - // all tombstone since we don't return them to the client) as well as on compaction. But it's likely that many rows won't have any tombstone at all, so - // we want to speed up that case by not having to iterate/copy the row in this case. We could keep a single boolean telling us if we have tombstones, - // but that doesn't work for expiring columns. So instead we keep the deletion time for the first thing in the row to be deleted. This allow at any given - // time to know if we have any deleted information or not. If we any "true" tombstone (i.e. not an expiring cell), this value will be forced to - // Long.MIN_VALUE, but if we don't and have expiring cells, this will the time at which the first expiring cell expires. If we have no tombstones and - // no expiring cells, this will be Cell.MAX_DELETION_TIME; + // We need to filter the tombstones of a row on every read (twice in fact: first to remove purgeable tombstone, + // and then after reconciliation to remove all tombstone since we don't return them to the client) as well as on + // compaction. But it's likely that many rows won't have any tombstone at all, so we want to speed up that case + // by not having to iterate/copy the row in this case. We could keep a single boolean telling us if we have + // tombstones, but that doesn't work for expiring columns. So instead we keep the deletion time for the first + // thing in the row to be deleted. This allows at any given time to know if we have any deleted information or not. + // If we have any "true" tombstone (i.e. not an expiring cell), this value will be forced to Long.MIN_VALUE, + // but if we don't and have expiring cells, this will the time at which the first expiring cell expires. If we + // have no tombstones and no expiring cells, this will be Cell.MAX_DELETION_TIME; private final long minLocalDeletionTime; private BTreeRow(Clustering clustering, diff --git a/src/java/org/apache/cassandra/db/rows/Cell.java b/src/java/org/apache/cassandra/db/rows/Cell.java index d3d0eb0416..d4c015b0cc 100644 --- a/src/java/org/apache/cassandra/db/rows/Cell.java +++ b/src/java/org/apache/cassandra/db/rows/Cell.java @@ -258,7 +258,7 @@ public abstract class Cell extends ColumnData * where not all field are always present (in fact, only the [ flags ] are guaranteed to be present). The fields have the following * meaning: * - [ flags ] is the cell flags. It is a byte for which each bit represents a flag whose meaning is explained below (*_MASK constants) - * - [ timestamp ] is the cell timestamp. Present unless the cell has the USE_TIMESTAMP_MASK. + * - [ timestamp ] is the cell timestamp. Present unless the cell has the USE_ROW_TIMESTAMP_MASK. * - [ deletion time]: the local deletion time for the cell. Present if either the cell is deleted (IS_DELETED_MASK) * or it is expiring (IS_EXPIRING_MASK) but doesn't have the USE_ROW_TTL_MASK. * - [ ttl ]: the ttl for the cell. Present if the row is expiring (IS_EXPIRING_MASK) but doesn't have the @@ -269,13 +269,13 @@ public abstract class Cell extends ColumnData * - [ value ]: the cell value, unless it has the HAS_EMPTY_VALUE_MASK. * - [ path ]: the cell path if the column this is a cell of is complex. */ - static class Serializer + public static class Serializer { - private final static int IS_DELETED_MASK = 0x01; // Whether the cell is a tombstone or not. - private final static int IS_EXPIRING_MASK = 0x02; // Whether the cell is expiring. - private final static int HAS_EMPTY_VALUE_MASK = 0x04; // Wether the cell has an empty value. This will be the case for tombstone in particular. - private final static int USE_ROW_TIMESTAMP_MASK = 0x08; // Wether the cell has the same timestamp than the row this is a cell of. - private final static int USE_ROW_TTL_MASK = 0x10; // Wether the cell has the same ttl than the row this is a cell of. + public final static int IS_DELETED_MASK = 0x01; // Whether the cell is a tombstone or not. + public final static int IS_EXPIRING_MASK = 0x02; // Whether the cell is expiring. + public final static int HAS_EMPTY_VALUE_MASK = 0x04; // Wether the cell has an empty value. This will be the case for tombstone in particular. + public final static int USE_ROW_TIMESTAMP_MASK = 0x08; // Wether the cell has the same timestamp than the row this is a cell of. + public final static int USE_ROW_TTL_MASK = 0x10; // Wether the cell has the same ttl than the row this is a cell of. public void serialize(Cell cell, ColumnMetadata column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException { @@ -319,11 +319,11 @@ public abstract class Cell extends ColumnData public Cell deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnMetadata column, SerializationHeader header, DeserializationHelper helper, ValueAccessor accessor) throws IOException { int flags = in.readUnsignedByte(); - boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0; - boolean isDeleted = (flags & IS_DELETED_MASK) != 0; - boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0; - boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0; - boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0; + boolean hasValue = hasValue(flags); + boolean isDeleted = isDeleted(flags); + boolean isExpiring = isExpiring(flags); + boolean useRowTimestamp = useRowTimestamp(flags); + boolean useRowTTL = useRowTTL(flags); long timestamp = useRowTimestamp ? rowLiveness.timestamp() : header.readTimestamp(in); @@ -390,11 +390,11 @@ public abstract class Cell extends ColumnData public boolean skip(DataInputPlus in, ColumnMetadata column, SerializationHeader header) throws IOException { int flags = in.readUnsignedByte(); - boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0; - boolean isDeleted = (flags & IS_DELETED_MASK) != 0; - boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0; - boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0; - boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0; + boolean hasValue = hasValue(flags); + boolean isDeleted = isDeleted(flags); + boolean isExpiring = isExpiring(flags); + boolean useRowTimestamp = useRowTimestamp(flags); + boolean useRowTTL = useRowTTL(flags); if (!useRowTimestamp) header.skipTimestamp(in); @@ -413,5 +413,30 @@ public abstract class Cell extends ColumnData return true; } + + public static boolean useRowTTL(int cellFlags) + { + return (cellFlags & USE_ROW_TTL_MASK) != 0; + } + + public static boolean useRowTimestamp(int cellFlags) + { + return (cellFlags & USE_ROW_TIMESTAMP_MASK) != 0; + } + + public static boolean isExpiring(int cellFlags) + { + return (cellFlags & IS_EXPIRING_MASK) != 0; + } + + public static boolean isDeleted(int cellFlags) + { + return (cellFlags & IS_DELETED_MASK) != 0; + } + + public static boolean hasValue(int cellFlags) + { + return (cellFlags & HAS_EMPTY_VALUE_MASK) == 0; + } } } diff --git a/src/java/org/apache/cassandra/db/rows/Cells.java b/src/java/org/apache/cassandra/db/rows/Cells.java index 48331a73a6..621a91d19c 100644 --- a/src/java/org/apache/cassandra/db/rows/Cells.java +++ b/src/java/org/apache/cassandra/db/rows/Cells.java @@ -36,7 +36,7 @@ public abstract class Cells private Cells() {} /** - * Collect statistics ont a given cell. + * Collect statistics on a given cell. * * @param cell the cell for which to collect stats. * @param collector the stats collector. diff --git a/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java b/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java index 2db62044fe..377880ae1c 100644 --- a/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java +++ b/src/java/org/apache/cassandra/db/rows/RangeTombstoneMarker.java @@ -159,7 +159,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory return DeletionTime.LIVE; DeletionTime biggestDeletionTime = openMarkers[biggestOpenMarker]; - // it's only open in the merged iterator if it doesn't supersedes the partition level deletion + // it's only open in the merged iterator if it doesn't supersede the partition level deletion return !biggestDeletionTime.supersedes(partitionDeletion) ? DeletionTime.LIVE : biggestDeletionTime; } @@ -172,7 +172,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory continue; // Note that we can have boundaries that are both open and close, but in that case all we care about - // is what it the open deletion after the marker, so we favor the opening part in this case. + // is what is the open deletion after the marker, so we favor the opening part in this case. if (marker.isOpen(reversed)) openMarkers[i] = marker.openDeletionTime(reversed); else @@ -192,7 +192,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory { DeletionTime openMarker = currentOpenDeletionTimeInMerged(); // We only have an open marker in the merged stream if it's not shadowed by the partition deletion (which can be LIVE itself), so - // if have an open marker, we know it's the "active" deletion for the merged stream. + // if we have an open marker, we know it's the "active" deletion for the merged stream. return openMarker.isLive() ? partitionDeletion : openMarker; } } diff --git a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java index 2fcba1bce8..ca1edfdbaf 100644 --- a/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java +++ b/src/java/org/apache/cassandra/db/rows/UnfilteredSerializer.java @@ -99,19 +99,19 @@ public class UnfilteredSerializer /* * Unfiltered flags constants. */ - private final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a field with that flag. - private final static int IS_MARKER = 0x02; // Whether the encoded unfiltered is a marker or a row. All following markers applies only to rows. - private final static int HAS_TIMESTAMP = 0x04; // Whether the encoded row has a timestamp (i.e. if row.partitionKeyLivenessInfo().hasTimestamp() == true). - private final static int HAS_TTL = 0x08; // Whether the encoded row has some expiration info (i.e. if row.partitionKeyLivenessInfo().hasTTL() == true). - private final static int HAS_DELETION = 0x10; // Whether the encoded row has some deletion info. - private final static int HAS_ALL_COLUMNS = 0x20; // Whether the encoded row has all of the columns from the header present. - private final static int HAS_COMPLEX_DELETION = 0x40; // Whether the encoded row has some complex deletion for at least one of its columns. - private final static int EXTENSION_FLAG = 0x80; // If present, another byte is read containing the "extended flags" above. + public final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a field with that flag. + public final static int IS_MARKER = 0x02; // Whether the encoded unfiltered is a marker or a row. All following markers applies only to rows. + public final static int HAS_TIMESTAMP = 0x04; // Whether the encoded row has a timestamp (i.e. if row.partitionKeyLivenessInfo().hasTimestamp() == true). + public final static int HAS_TTL = 0x08; // Whether the encoded row has some expiration info (i.e. if row.partitionKeyLivenessInfo().hasTTL() == true). + public final static int HAS_DELETION = 0x10; // Whether the encoded row has some deletion info. + public final static int HAS_ALL_COLUMNS = 0x20; // Whether the encoded row has all of the columns from the header present. + public final static int HAS_COMPLEX_DELETION = 0x40; // Whether the encoded row has some complex deletion for at least one of its columns. + public final static int EXTENSION_FLAG = 0x80; // If present, another byte is read containing the "extended flags" above. /* * Extended flags */ - private final static int IS_STATIC = 0x01; // Whether the encoded row is a static. If there is no extended flag, the row is assumed not static. + public final static int IS_STATIC = 0x01; // Whether the encoded row is a static. If there is no extended flag, the row is assumed not static. /** * A shadowable tombstone cannot replace a previous row deletion otherwise it could resurrect a * previously deleted cell not updated by a subsequent update, SEE CASSANDRA-11500 @@ -119,7 +119,7 @@ public class UnfilteredSerializer * @deprecated See CASSANDRA-11500 */ @Deprecated(since = "4.0") - private final static int HAS_SHADOWABLE_DELETION = 0x02; // Whether the row deletion is shadowable. If there is no extended flag (or no row deletion), the deletion is assumed not shadowable. + public final static int HAS_SHADOWABLE_DELETION = 0x02; // Whether the row deletion is shadowable. If there is no extended flag (or no row deletion), the deletion is assumed not shadowable. public void serialize(Unfiltered unfiltered, SerializationHelper helper, DataOutputPlus out, int version) throws IOException @@ -220,14 +220,14 @@ public class UnfilteredSerializer LivenessInfo pkLiveness = row.primaryKeyLivenessInfo(); Row.Deletion deletion = row.deletion(); - if ((flags & HAS_TIMESTAMP) != 0) + if (hasTimestamp(flags)) header.writeTimestamp(pkLiveness.timestamp(), out); - if ((flags & HAS_TTL) != 0) + if (hasTTL(flags)) { header.writeTTL(pkLiveness.ttl(), out); header.writeLocalDeletionTime(pkLiveness.localExpirationTime(), out); } - if ((flags & HAS_DELETION) != 0) + if (hasDeletion(flags)) header.writeDeletionTime(deletion.time(), out); if ((flags & HAS_ALL_COLUMNS) == 0) @@ -251,7 +251,7 @@ public class UnfilteredSerializer if (cd.column.isSimple()) Cell.serializer.serialize((Cell) cd, column, out, pkLiveness, header); else - writeComplexColumn((ComplexColumnData) cd, column, (flags & HAS_COMPLEX_DELETION) != 0, pkLiveness, header, out); + writeComplexColumn((ComplexColumnData) cd, column, hasComplexDeletion(flags), pkLiveness, header, out); } catch (IOException e) { @@ -412,7 +412,7 @@ public class UnfilteredSerializer public void writeEndOfPartition(DataOutputPlus out) throws IOException { - out.writeByte((byte)1); + out.writeByte((byte)END_OF_PARTITION); } public long serializedSizeEndOfPartition() @@ -502,12 +502,12 @@ public class UnfilteredSerializer else { assert !isStatic(extendedFlags); // deserializeStaticRow should be used for that. - if ((flags & HAS_DELETION) != 0) + if (hasDeletion(flags)) { assert header.isForSSTable(); - boolean hasTimestamp = (flags & HAS_TIMESTAMP) != 0; - boolean hasTTL = (flags & HAS_TTL) != 0; - boolean deletionIsShadowable = (extendedFlags & HAS_SHADOWABLE_DELETION) != 0; + boolean hasTimestamp = hasTimestamp(flags); + boolean hasTTL = hasTTL(flags); + boolean deletionIsShadowable = deletionIsShadowable(extendedFlags); Clustering clustering = Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes()); long nextPosition = in.readUnsignedVInt() + in.getFilePointer(); in.readUnsignedVInt(); // skip previous unfiltered size @@ -572,12 +572,12 @@ public class UnfilteredSerializer try { boolean isStatic = isStatic(extendedFlags); - boolean hasTimestamp = (flags & HAS_TIMESTAMP) != 0; - boolean hasTTL = (flags & HAS_TTL) != 0; - boolean hasDeletion = (flags & HAS_DELETION) != 0; - boolean deletionIsShadowable = (extendedFlags & HAS_SHADOWABLE_DELETION) != 0; - boolean hasComplexDeletion = (flags & HAS_COMPLEX_DELETION) != 0; - boolean hasAllColumns = (flags & HAS_ALL_COLUMNS) != 0; + boolean hasTimestamp = hasTimestamp(flags); + boolean hasTTL = hasTTL(flags); + boolean hasDeletion = hasDeletion(flags); + boolean deletionIsShadowable = deletionIsShadowable(extendedFlags); + boolean hasComplexDeletion = hasComplexDeletion(flags); + boolean hasAllColumns = hasAllColumns(flags); Columns headerColumns = header.columns(isStatic); if (header.isForSSTable()) @@ -734,7 +734,17 @@ public class UnfilteredSerializer public static Unfiltered.Kind kind(int flags) { - return (flags & IS_MARKER) != 0 ? Unfiltered.Kind.RANGE_TOMBSTONE_MARKER : Unfiltered.Kind.ROW; + return isTombstoneMarker(flags) ? Unfiltered.Kind.RANGE_TOMBSTONE_MARKER : Unfiltered.Kind.ROW; + } + + public static boolean isTombstoneMarker(int flags) + { + return (flags & IS_MARKER) != 0; + } + + public static boolean isRow(int flags) + { + return (flags & IS_MARKER) == 0; } public static boolean isStatic(int extendedFlags) @@ -742,7 +752,12 @@ public class UnfilteredSerializer return (extendedFlags & IS_STATIC) != 0; } - private static boolean isExtended(int flags) + public static boolean deletionIsShadowable(int extendedFlags) + { + return (extendedFlags & HAS_SHADOWABLE_DELETION) != 0; + } + + public static boolean isExtended(int flags) { return (flags & EXTENSION_FLAG) != 0; } @@ -756,4 +771,29 @@ public class UnfilteredSerializer { return row.isStatic() || row.deletion().isShadowable(); } + + public static boolean hasTTL(int flags) + { + return (flags & HAS_TTL) != 0; + } + + public static boolean hasTimestamp(int flags) + { + return (flags & HAS_TIMESTAMP) != 0; + } + + public static boolean hasAllColumns(int flags) + { + return (flags & HAS_ALL_COLUMNS) != 0; + } + + public static boolean hasComplexDeletion(int flags) + { + return (flags & HAS_COMPLEX_DELETION) != 0; + } + + public static boolean hasDeletion(int flags) + { + return (flags & HAS_DELETION) != 0; + } } diff --git a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java index 8aada75663..040e54dbdd 100644 --- a/src/java/org/apache/cassandra/dht/ComparableObjectToken.java +++ b/src/java/org/apache/cassandra/dht/ComparableObjectToken.java @@ -21,7 +21,7 @@ abstract class ComparableObjectToken> extends Token { private static final long serialVersionUID = 1L; - final C token; // Package-private to allow access from subtypes, which should all reside in the dht package. + C token; // Package-private to allow access from subtypes, which should all reside in the dht package. protected ComparableObjectToken(C token) { diff --git a/src/java/org/apache/cassandra/dht/IPartitioner.java b/src/java/org/apache/cassandra/dht/IPartitioner.java index af2d9051b6..ef6d2b23fb 100644 --- a/src/java/org/apache/cassandra/dht/IPartitioner.java +++ b/src/java/org/apache/cassandra/dht/IPartitioner.java @@ -41,6 +41,19 @@ public interface IPartitioner return DatabaseDescriptor.getPartitioner(); } + /** + * @return a new instance of a reusable key + */ + default ReusableDecoratedKey createReusableKey(int initialSize) + { + throw new UnsupportedOperationException(); + } + + default boolean supportsReusableKeys() + { + return false; + } + /** * Transform key to object representation of the on-disk format. * diff --git a/src/java/org/apache/cassandra/dht/LocalPartitioner.java b/src/java/org/apache/cassandra/dht/LocalPartitioner.java index 1fec57065a..2609402af7 100644 --- a/src/java/org/apache/cassandra/dht/LocalPartitioner.java +++ b/src/java/org/apache/cassandra/dht/LocalPartitioner.java @@ -243,4 +243,38 @@ public class LocalPartitioner implements IPartitioner { return AccordBytesSplitter::new; } + + private class ReusableLocalToken extends LocalToken + { + void setToken(ByteBuffer token) + { + this.token = token; + } + } + + private class ReusableLocalKey extends ReusableDecoratedKey + { + public ReusableLocalKey(int initialSize) + { + super(new ReusableLocalToken(), initialSize); + } + + @Override + protected void recalculateToken() + { + ((ReusableLocalToken)getToken()).setToken(key); + } + } + + @Override + public ReusableDecoratedKey createReusableKey(int initialSize) + { + return new ReusableLocalKey(initialSize); + } + + @Override + public boolean supportsReusableKeys() + { + return true; + } } diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java index 74a605946a..e76a7df8ad 100644 --- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java +++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -35,6 +36,7 @@ import com.google.common.primitives.Longs; import accord.primitives.Ranges; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PreHashedDecoratedKey; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.marshal.AbstractType; @@ -177,7 +179,7 @@ public class Murmur3Partitioner implements IPartitioner { static final long serialVersionUID = -5833580143318243006L; - public final long token; + long token; public LongToken(long token) { @@ -320,6 +322,18 @@ public class Murmur3Partitioner implements IPartitioner return new LongToken(normalize(hash[0])); } + private long calculateTokenValue(ByteBuffer key, long[] hash) + { + if (key.remaining() == 0) + { + hash[0] = MINIMUM.token; + hash[1] = 0; + return MINIMUM.token; + } + populateHash(key, hash); + return normalize(hash[0]); + } + @Override public boolean isFixedLength() { @@ -386,10 +400,15 @@ public class Murmur3Partitioner implements IPartitioner private long[] getHash(ByteBuffer key) { long[] hash = new long[2]; - MurmurHash.hash3_x64_128(key, key.position(), key.remaining(), 0, hash); + populateHash(key, hash); return hash; } + private void populateHash(ByteBuffer key, long[] hash) + { + MurmurHash.hash3_x64_128(key, key.position(), key.remaining(), 0, hash); + } + public LongToken getRandomToken() { return getRandomToken(ThreadLocalRandom.current()); @@ -565,4 +584,79 @@ public class Murmur3Partitioner implements IPartitioner { return ignore -> splitter; } + + private static class ReusableLongToken extends LongToken + { + public ReusableLongToken() + { + super(MINIMUM.token); + } + + void setToken(long token) + { + this.token = token; + } + } + + private static class ReusableMurmurKey extends ReusableDecoratedKey + { + protected final long[] hash = new long[2]; + private long tokenValue = MINIMUM.token; + ReusableMurmurKey(int initialSize) + { + super(new ReusableLongToken(), initialSize); + } + + protected void recalculateToken() + { + Token token = getToken(); + ((ReusableLongToken) token).setToken(Murmur3Partitioner.instance.calculateTokenValue(key, hash)); + tokenValue = token.getLongValue(); + } + + @Override + public boolean equals(Object obj) + { + return (obj instanceof ReusableMurmurKey) ? equals((ReusableMurmurKey) obj) : super.equals(obj); + } + + public boolean equals(ReusableMurmurKey obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + + if (tokenValue != obj.tokenValue) + return false; + return Arrays.equals(keyBytes, 0, keyLength, obj.keyBytes, 0, obj.getKeyLength()); // we compare faster than BB.equals for array backed BB + } + + @Override + public int compareTo(PartitionPosition pos) + { + return (pos instanceof ReusableMurmurKey) ? compareTo((ReusableMurmurKey) pos) : super.compareTo(pos); + } + + public int compareTo(ReusableMurmurKey obj) + { + if (this == obj) + return 0; + + int cmp = Long.compare(tokenValue, obj.tokenValue); + return cmp == 0 ? Arrays.compareUnsigned(keyBytes, 0, keyLength, obj.keyBytes, 0, obj.keyLength) : cmp; + } + } + + @Override + public ReusableDecoratedKey createReusableKey(int initialSize) + { + return new ReusableMurmurKey(initialSize); + } + + @Override + public boolean supportsReusableKeys() + { + return true; + } } diff --git a/src/java/org/apache/cassandra/dht/ReusableDecoratedKey.java b/src/java/org/apache/cassandra/dht/ReusableDecoratedKey.java new file mode 100644 index 0000000000..7a9723e17c --- /dev/null +++ b/src/java/org/apache/cassandra/dht/ReusableDecoratedKey.java @@ -0,0 +1,91 @@ +/* + * 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.dht; + +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.BufferDecoratedKey; +import org.apache.cassandra.utils.ByteBufferUtil; + +public abstract class ReusableDecoratedKey extends BufferDecoratedKey +{ + protected int keyLength = 0; + protected byte[] keyBytes; + + public ReusableDecoratedKey(Token token, int initialSize) + { + super(token, ByteBuffer.wrap(new byte[initialSize]).limit(0)); + keyBytes = key.array(); + } + + abstract void recalculateToken(); + + public void copyKey(ByteBuffer newKey) + { + int length = newKey.remaining(); + maybeResizeKey(length); + ByteBufferUtil.copyBytes(newKey, newKey.position(), key, 0, length); + keyLength = length; + key.limit(length); + recalculateToken(); + } + + /** WARNING: retains ref to external buffer */ + public void shadowKey(ByteBuffer newKey, byte[] newKeyBytes, int newKeyLength) + { + key = newKey; + keyBytes = newKeyBytes; + keyLength = newKeyLength; + recalculateToken(); + } + + /** WARNING: retains ref to external buffer */ + public void shadowKey(int newKeyLength) + { + keyLength = newKeyLength; + recalculateToken(); + } + + @Override + public int getKeyLength() + { + return keyLength; + } + + public byte[] keyBytes() + { + return keyBytes; + } + + public void reset() + { + keyLength = 0; + key.limit(0); + recalculateToken(); + } + + private void maybeResizeKey(int length) + { + int capacity = keyBytes.length; + if (capacity > length) + return; + keyBytes = new byte[Math.max(length, capacity * 2)]; + key = ByteBuffer.wrap(keyBytes); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java index e6fbad882e..a320684dd8 100644 --- a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java +++ b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableIterator.java @@ -500,7 +500,7 @@ public abstract class AbstractSSTableIterator { // We use a same reasoning as in handlePreSliceData regarding the strictness of the inequality below. // We want to exclude deserialized unfiltered equal to end, because 1) we won't miss any rows since those - // woudn't be equal to a slice bound and 2) a end bound can be equal to a start bound + // wouldn't be equal to a slice bound and 2) a end bound can be equal to a start bound // (EXCL_END(x) == INCL_START(x) for instance) and in that case we don't want to return start bound because // it's fundamentally excluded. And if the bound is a end (for a range tombstone), it means it's exactly // our slice end, but in that case we will properly close the range tombstone anyway as part of our "close diff --git a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java index 67d13c2327..2487e66fd2 100644 --- a/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/AbstractSSTableSimpleWriter.java @@ -47,7 +47,7 @@ import org.apache.cassandra.service.ActiveRepairService; /** * Base class for the sstable writers used by CQLSSTableWriter. */ -abstract class AbstractSSTableSimpleWriter implements Closeable +public abstract class AbstractSSTableSimpleWriter implements Closeable { protected final File directory; protected final TableMetadataRef metadata; @@ -172,7 +172,7 @@ abstract class AbstractSSTableSimpleWriter implements Closeable } } - PartitionUpdate.Builder getUpdateFor(ByteBuffer key) throws IOException + public PartitionUpdate.Builder getUpdateFor(ByteBuffer key) throws IOException { return getUpdateFor(metadata.get().partitioner.decorateKey(key)); } diff --git a/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java new file mode 100644 index 0000000000..2afbc4db77 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/ClusteringDescriptor.java @@ -0,0 +1,187 @@ +/* + * 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.sstable; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import org.apache.cassandra.io.util.ResizableByteBuffer; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringBound; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.ByteArrayAccessor; +import org.apache.cassandra.io.util.DataInputBuffer; +import org.apache.cassandra.io.util.RandomAccessReader; + +import static org.apache.cassandra.io.sstable.SSTableCursorReader.readUnfilteredClustering; + +public class ClusteringDescriptor extends ResizableByteBuffer +{ + private static final byte MAX_START_KIND = (byte) ClusteringBound.MAX_START.kind().ordinal(); + private static final byte MIN_END_KIND = (byte) ClusteringBound.MIN_END.kind().ordinal(); + + static final byte EXCL_END_BOUND_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.EXCL_END_BOUND.ordinal(); + static final byte INCL_START_BOUND_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.INCL_START_BOUND.ordinal(); + static final byte INCL_END_EXCL_START_BOUNDARY_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.INCL_END_EXCL_START_BOUNDARY.ordinal(); + + static final byte STATIC_CLUSTERING_KIND = (byte)ClusteringPrefix.Kind.STATIC_CLUSTERING.ordinal(); + static final byte ROW_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.CLUSTERING.ordinal(); + + static final byte EXCL_END_INCL_START_BOUNDARY_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.EXCL_END_INCL_START_BOUNDARY.ordinal(); + static final byte INCL_END_BOUND_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.INCL_END_BOUND.ordinal(); + static final byte EXCL_START_BOUND_CLUSTERING_KIND = (byte) ClusteringPrefix.Kind.EXCL_START_BOUND.ordinal(); + + protected final AbstractType[] clusteringTypes; + protected ClusteringPrefix.Kind clusteringKind; + protected byte clusteringKindEncoded; + protected int clusteringColumnsBound; + + public ClusteringDescriptor(AbstractType[] clusteringTypes) + { + this.clusteringTypes = clusteringTypes; + } + + protected void loadClustering(RandomAccessReader dataReader, byte clusteringKind, int clusteringColumnsBound) throws IOException + { + set(ClusteringPrefix.Kind.values()[clusteringKind], clusteringKind, clusteringColumnsBound); + if (clusteringKind != STATIC_CLUSTERING_KIND) + readUnfilteredClustering(dataReader, clusteringTypes, this.clusteringColumnsBound, this); + else + resetBuffer(); + } + + public final ClusteringDescriptor resetMinEnd() { + set(MIN_END_KIND, 0); + resetBuffer(); + return this; + } + + public final ClusteringDescriptor resetMaxStart() { + set(MAX_START_KIND, 0); + resetBuffer(); + return this; + } + + public final boolean isMaxStart() + { + return clusteringKindEncoded == MAX_START_KIND && clusteringColumnsBound == 0; + } + + public final void resetClustering() + { + set(ClusteringPrefix.Kind.CLUSTERING, 0); + + resetBuffer(); + } + + public final void copy(ClusteringDescriptor newClustering) + { + set(newClustering.clusteringKind, newClustering.clusteringColumnsBound()); + overwrite(newClustering.clusteringBytes(), newClustering.clusteringLength()); + } + + private void set(ClusteringPrefix.Kind clusteringKind, int clusteringColumnsBound) { + set(clusteringKind, (byte) clusteringKind.ordinal(), clusteringColumnsBound); + } + + private void set(byte clusteringKindEncoded, int clusteringColumnsBound) { + set(ClusteringPrefix.Kind.values()[clusteringKindEncoded], clusteringKindEncoded, clusteringColumnsBound); + } + + private void set(ClusteringPrefix.Kind clusteringKind, byte clusteringKindEncoded, int clusteringColumnsBound) + { + this.clusteringKindEncoded = clusteringKindEncoded; + this.clusteringKind = clusteringKind; + this.clusteringColumnsBound = clusteringColumnsBound; + } + + // Expose and rename parent data + public final ByteBuffer clusteringBuffer() { + return buffer(); + } + + public final int clusteringLength() { + return length(); + } + + public final byte[] clusteringBytes() { + return bytes(); + } + + public final AbstractType[] clusteringTypes() + { + return clusteringTypes; + } + + public final byte clusteringKindEncoded() { + return clusteringKindEncoded; + } + + public final ClusteringPrefix.Kind clusteringKind() { + return clusteringKind; + } + + public final void clusteringKind(ClusteringPrefix.Kind kind) + { + clusteringKind = kind; + clusteringKindEncoded = (byte)kind.ordinal(); + } + + public final int clusteringColumnsBound() { + return clusteringColumnsBound; + } + + public final boolean isStartBound() + { + return (clusteringKindEncoded == INCL_START_BOUND_CLUSTERING_KIND || clusteringKindEncoded == EXCL_START_BOUND_CLUSTERING_KIND); + } + + public final boolean isEndBound() + { + return (clusteringKindEncoded == INCL_END_BOUND_CLUSTERING_KIND || clusteringKindEncoded == EXCL_END_BOUND_CLUSTERING_KIND); + } + + public final boolean isBoundary() + { + return (clusteringKindEncoded == EXCL_END_INCL_START_BOUNDARY_CLUSTERING_KIND || clusteringKindEncoded == INCL_END_EXCL_START_BOUNDARY_CLUSTERING_KIND); + } + + public final ClusteringPrefix toClusteringPrefix(List> clusteringTypesList) { + if (clusteringKindEncoded == ROW_CLUSTERING_KIND) { + return Clustering.serializer.deserialize(clusteringBuffer(), 0, clusteringTypesList); + } + else if (clusteringColumnsBound == 0) { + return ByteArrayAccessor.factory.bound(clusteringKind); + } + else { + byte[][] values; + try (DataInputBuffer buffer = new DataInputBuffer(clusteringBuffer(), true)) + { + values = ClusteringPrefix.serializer.deserializeValuesWithoutSize(buffer, clusteringColumnsBound, 0, clusteringTypesList); + } + catch (IOException e) + { + throw new RuntimeException("Reading from an in-memory buffer shouldn't trigger an IOException", e); + } + return ByteArrayAccessor.factory.boundOrBoundary(clusteringKind, values); + } + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java index 8976ed4130..fecb6940a4 100644 --- a/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/EmptySSTableScanner.java @@ -56,6 +56,12 @@ public class EmptySSTableScanner extends AbstractUnfilteredPartitionIterator imp return ImmutableSet.of(sstable); } + @Override + public boolean isFullRange() + { + return false; + } + public long getCurrentPosition() { return 0; diff --git a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java index 671bccb824..2cf6280469 100644 --- a/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/ISSTableScanner.java @@ -39,6 +39,7 @@ public interface ISSTableScanner extends UnfilteredPartitionIterator public long getCurrentPosition(); public long getBytesScanned(); public Set getBackingSSTables(); + public boolean isFullRange(); public static void closeAllAndPropagate(Collection scanners, Throwable throwable) { diff --git a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java index 350a98eb9e..607d876c0f 100644 --- a/src/java/org/apache/cassandra/io/sstable/IndexInfo.java +++ b/src/java/org/apache/cassandra/io/sstable/IndexInfo.java @@ -31,6 +31,7 @@ import org.apache.cassandra.io.sstable.format.Version; import org.apache.cassandra.io.sstable.format.big.RowIndexEntry; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.ObjectSizes; /** @@ -87,6 +88,17 @@ public class IndexInfo return new IndexInfo.Serializer(version, header.clusteringTypes()); } + public String toString(TableMetadata metadata) + { + return "IndexInfo{" + + "offset=" + offset + + ", width=" + width + + ", firstName=" + firstName.toString(metadata) + + ", lastName=" + lastName.toString(metadata) + + ", endOpenMarker=" + endOpenMarker + + '}'; + } + public static class Serializer implements ISerializer { // This is the default index size that we use to delta-encode width when serializing so we get better vint-encoding. diff --git a/src/java/org/apache/cassandra/io/sstable/PartitionDescriptor.java b/src/java/org/apache/cassandra/io/sstable/PartitionDescriptor.java new file mode 100644 index 0000000000..425f2413d3 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/PartitionDescriptor.java @@ -0,0 +1,115 @@ +/* + * 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.sstable; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime; +import org.apache.cassandra.dht.ReusableDecoratedKey; +import org.apache.cassandra.io.util.ResizableByteBuffer; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.io.util.RandomAccessReader; + +public final class PartitionDescriptor extends ResizableByteBuffer +{ + private long position; + private final ReusableDeletionTime deletionTime = ReusableDeletionTime.live(); + private final ReusableDecoratedKey decoratedKey; + + public PartitionDescriptor(ReusableDecoratedKey decoratedKey) + { + this.decoratedKey = decoratedKey; + this.decoratedKey.shadowKey(buffer(), bytes(), length()); + } + + /** + * Loads the following structure: + *

+     *   struct partition_header header {
+     *     be16 key_length; // e.g. 8 if long
+     *     byte key[key_length];
+     *     struct deletion_time deletion_time {
+     *       be32 local_deletion_time;
+     *       be64 marked_for_delete_at;
+     *     };
+     *   };
+     *   
+ */ + void load(RandomAccessReader dataReader, DeletionTime.Serializer serializer) throws IOException + { + position = dataReader.getPosition(); + + boolean resize = loadShortLength(dataReader); + if (!resize) + { + decoratedKey.shadowKey(length()); + } + else + { + decoratedKey.shadowKey(buffer(), bytes(), length()); + } + serializer.deserialize(dataReader, deletionTime); + } + + public long position() + { + return position; + } + + public DeletionTime deletionTime() + { + return deletionTime; + } + + public DecoratedKey key() + { + return decoratedKey; + } + + public ByteBuffer keyBuffer() { + return super.buffer(); + } + + public int keyLength() { + return super.length(); + } + + public byte[] keyBytes() { + return super.bytes(); + } + + public void resetPartition() + { + resetBuffer(); + decoratedKey.reset(); + deletionTime.resetLive(); + position = 0; + } + + @Override + public String toString() + { + return "PartitionDescriptor{ key=" + decoratedKey + + ", position=" + position + + ", deletionTime=" + deletionTime + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableCursorKeyReader.java b/src/java/org/apache/cassandra/io/sstable/SSTableCursorKeyReader.java new file mode 100644 index 0000000000..d0bbe73821 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/SSTableCursorKeyReader.java @@ -0,0 +1,157 @@ +/* + * 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.sstable; + +import java.io.IOException; +import java.nio.ByteBuffer; +import javax.annotation.concurrent.NotThreadSafe; + + +import org.apache.cassandra.io.util.ResizableByteBuffer; +import org.apache.cassandra.io.util.FileHandle; +import org.apache.cassandra.io.util.FileUtils; +import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.utils.Throwables; + +@NotThreadSafe +public class SSTableCursorKeyReader implements AutoCloseable +{ + private final FileHandle indexFile; + private final RandomAccessReader indexFileReader; + private final long initialPosition; + + public static class Entry extends ResizableByteBuffer + { + private long dataPosition = -1; + private long keyPosition = -1; + + public void load(RandomAccessReader indexReader) throws IOException + { + keyPosition = indexReader.getFilePointer(); + super.loadShortLength(indexReader); + if (length() != 0) + { + dataPosition = indexReader.readUnsignedVInt(); + // skip row index entries + int size = indexReader.readUnsignedVInt32(); + if (size > 0) + indexReader.skipBytesFully(size); + } + else + { + dataPosition = -1; + } + } + + public long dataPosition() + { + return dataPosition; + } + + public long keyPosition() + { + return keyPosition; + } + + public ByteBuffer getKey() + { + return buffer(); + } + } + + private SSTableCursorKeyReader(FileHandle indexFile, + RandomAccessReader indexFileReader) + { + this.indexFile = indexFile; + this.indexFileReader = indexFileReader; + this.initialPosition = indexFileReader.getFilePointer(); + } + + public static SSTableCursorKeyReader create(RandomAccessReader indexFileReader) throws IOException + { + return new SSTableCursorKeyReader(null, indexFileReader); + } + + @SuppressWarnings({ "resource", "RedundantSuppression" }) // iFile and reader are closed in the BigTableKeyReader#close method + public static SSTableCursorKeyReader create(FileHandle indexFile) throws IOException + { + FileHandle iFile = null; + RandomAccessReader reader = null; + try + { + iFile = indexFile.sharedCopy(); + reader = iFile.createReader(); + return new SSTableCursorKeyReader(iFile, reader); + } + catch (RuntimeException ex) + { + Throwables.closeNonNullAndAddSuppressed(ex, reader, iFile); + throw ex; + } + } + + @Override + public void close() + { + FileUtils.closeQuietly(indexFileReader); + FileUtils.closeQuietly(indexFile); + } + + public boolean advance(Entry entry) throws IOException + { + if (indexFileReader.isEOF()) + { + return false; + } + entry.load(indexFileReader); + return true; + } + + public boolean isExhausted() + { + return indexFileReader.isEOF(); + } + + public long indexPosition() + { + return indexFileReader.getFilePointer(); + } + + public void seek(long position) throws IOException + { + if (position > indexLength()) + throw new IndexOutOfBoundsException("The requested position exceeds the index length"); + indexFileReader.seek(position); + } + + public long indexLength() + { + return indexFileReader.length(); + } + + public void reset() throws IOException + { + indexFileReader.seek(initialPosition); + } + + @Override + public String toString() + { + return String.format("BigTable-SSTableCursorKeyReader(%s), indexPosition=%d", indexFile.path(), indexFileReader.getFilePointer()); + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java b/src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java new file mode 100644 index 0000000000..f9dfc2ffcb --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/SSTableCursorReader.java @@ -0,0 +1,891 @@ +/* + * 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.sstable; + +import java.io.IOException; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.io.util.ResizableByteBuffer; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.CellPath; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.SerializationHelper; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.util.DataInputPlus; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.tools.Util; +import org.apache.cassandra.utils.concurrent.Ref; + +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.*; + +public class SSTableCursorReader implements AutoCloseable +{ + private static final ColumnMetadata[] COLUMN_METADATA_TYPE = new ColumnMetadata[0]; + private final boolean hasStaticColumns; + + public interface State + { + /** start of file, after partition end but before EOF */ + int PARTITION_START = 1; + int STATIC_ROW_START = 1 << 1; + int ROW_START = 1 << 2; + /** common to row/static row cells */ + int CELL_HEADER_START = 1 << 3; + int CELL_VALUE_START = 1 << 4; + int CELL_END = 1 << 5; + int TOMBSTONE_START = 1 << 6; + + /** common to rows/tombstones. Call continue(); for next unfiltered, or maybe partition end */ + int UNFILTERED_END = 1 << 7; + /** at {@link UnfilteredSerializer#isEndOfPartition(int)} */ + int PARTITION_END = 1 << 8; + /** EOF */ + int DONE = 1 << 9; + + /* Special case for seeking in file */ + int SEEK = 1 << 10; + static boolean isState(int state, int mask) { + return (state & mask) != 0; + } + } + + public class CellCursor { + public ReusableLivenessInfo rowLiveness; + public Columns columns; + + public int columnsSize; + public int columnsIndex; + public int cellFlags; + public final ReusableLivenessInfo cellLiveness = new ReusableLivenessInfo(); + public CellPath cellPath; + public AbstractType cellType; + public ColumnMetadata cellColumn; + private ColumnMetadata[] columnsArray; + private AbstractType[] cellTypeArray; + + void init (Columns columns, ReusableLivenessInfo rowLiveness) + { + if (this.columns != columns) + { + // This will be a problem with changing columns + this.columns = columns; + columnsArray = columns.toArray(COLUMN_METADATA_TYPE); + cellTypeArray = new AbstractType[columnsArray.length]; + for (int i = 0; i < columnsArray.length; i++) + { + ColumnMetadata cellColumn = columnsArray[i]; + cellTypeArray[i] = serializationHeader.getType(cellColumn); + } + columnsSize = columns.size(); + } + this.rowLiveness = rowLiveness; + columnsIndex = 0; + cellFlags = 0; + cellPath = null; + cellType = null; + } + + public boolean hasNext() + { + return columnsIndex < columnsSize; + } + + /** + * For Cell deserialization see {@link Cell.Serializer#deserialize} + * + * @return true if the cell has a value, false otherwise + */ + boolean readCellHeader() throws IOException + { + if (!(columnsIndex < columnsSize)) throw new IllegalStateException(); + + // HOTSPOT: suprisingly expensive + int currIndex = columnsIndex++; + cellColumn = columnsArray[currIndex]; + cellType = cellTypeArray[currIndex]; + cellFlags = dataReader.readUnsignedByte(); + // TODO: specialize common case where flags == HAS_VALUE | USE_ROW_TS? + boolean hasValue = Cell.Serializer.hasValue(cellFlags); + boolean isDeleted = Cell.Serializer.isDeleted(cellFlags); + boolean isExpiring = Cell.Serializer.isExpiring(cellFlags); + boolean useRowTimestamp = Cell.Serializer.useRowTimestamp(cellFlags); + boolean useRowTTL = Cell.Serializer.useRowTTL(cellFlags); + + long timestamp = useRowTimestamp ? rowLiveness.timestamp() : serializationHeader.readTimestamp(dataReader); + + long localDeletionTime = useRowTTL + ? rowLiveness.localExpirationTime() + : (isDeleted || isExpiring ? serializationHeader.readLocalDeletionTime(dataReader) : Cell.NO_DELETION_TIME); + + int ttl = useRowTTL ? rowLiveness.ttl() : (isExpiring ? serializationHeader.readTTL(dataReader) : Cell.NO_TTL); + localDeletionTime = Cell.decodeLocalDeletionTime(localDeletionTime, ttl, deserializationHelper); + + cellLiveness.reset(timestamp, ttl, localDeletionTime); + cellPath = cellColumn.isComplex() + ? cellColumn.cellPathSerializer().deserialize(dataReader) + : null; + return hasValue; + } + } + + private final Ref ssTableReaderRef; + private final AbstractType[] clusteringColumnTypes; + private final DeserializationHelper deserializationHelper; + private final SerializationHeader serializationHeader; + + // need to be closed + private final SSTableReader ssTableReader; + private final RandomAccessReader dataReader; + private final DeletionTime.Serializer deletionTimeSerializer; + + private final CellCursor staticRowCellCursor = new CellCursor(); + private final CellCursor rowCellCursor = new CellCursor(); + + + private CellCursor cellCursor; + + // SHARED STATIC_ROW/ROW/TOMB + private int basicUnfilteredFlags = 0; + private int extendedFlags = 0; + + private int state = PARTITION_START; + + public static SSTableCursorReader fromDescriptor(Descriptor desc) throws IOException + { + TableMetadata metadata = Util.metadataFromSSTable(desc); + SSTableReader reader = SSTableReader.openNoValidation(null, desc, TableMetadataRef.forOfflineTools(metadata)); + return new SSTableCursorReader(reader, metadata, reader.ref()); + } + + public SSTableCursorReader(SSTableReader reader) + { + this(reader, reader.metadata(), null); + } + + private SSTableCursorReader(SSTableReader reader, TableMetadata metadata, Ref readerRef) + { + ssTableReader = reader; + ssTableReaderRef = readerRef; + Version version = reader.descriptor.version; + deletionTimeSerializer = DeletionTime.getSerializer(version); + ImmutableList clusteringColumns = metadata.clusteringColumns(); + int clusteringColumnCount = clusteringColumns.size(); + clusteringColumnTypes = new AbstractType[clusteringColumnCount]; + for (int i = 0; i < clusteringColumnTypes.length; i++) + { + clusteringColumnTypes[i] = clusteringColumns.get(i).type; + } + deserializationHelper = new DeserializationHelper(metadata, version.correspondingMessagingVersion(), DeserializationHelper.Flag.LOCAL, null); + serializationHeader = reader.header; + + dataReader = reader.openDataReader(); + hasStaticColumns = metadata.hasStaticColumns(); + } + + @Override + public void close() + { + dataReader.close(); + if (ssTableReaderRef != null) + ssTableReaderRef.close(); + } + + private void resetOnPartitionStart() + { + basicUnfilteredFlags = 0; + extendedFlags = 0; + } + + public int seekPartition(long position) + { + state = SEEK; + if (position == 0) + { + dataReader.seek(position); + state = PARTITION_START; + } + else { + // verify partition start is after a partition end marker + dataReader.seek(position - 1); + try + { + basicUnfilteredFlags = dataReader.readUnsignedByte(); + } + catch (Exception e) + { + return corruptSSTable(e); + } + // end of partition + if (!UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags)) { + throw new IllegalArgumentException("Seeking to a partition at: " + position + " did not result in a valid state"); + } + state = dataReader.isEOF() ? DONE : PARTITION_START; + } + resetOnPartitionStart(); + return state; + } + + public int seekUnfiltered(long position) + { + state = SEEK; + // partition elements (Unfiltered) have flags + dataReader.seek(position); + int state = 0; + try + { + state = checkNextFlagsAfterStaticRowOrUnfilteredStart(false); + } + catch (IOException e) + { + return corruptSSTable(e); + } + if (!isState(state , ROW_START | TOMBSTONE_START | DONE)) throw new IllegalStateException(); + return state; + } + + // struct partition { + // struct partition_header header + // optional row + // struct unfiltered unfiltereds[]; + //}; + public int readPartitionHeader(PartitionDescriptor header) + { + if (state != PARTITION_START) throw new IllegalStateException(); + resetOnPartitionStart(); + try + { + header.load(dataReader, deletionTimeSerializer); + return checkNextFlagsAfterPartitionStart(false); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + // struct static_row { + // byte flags; // preloaded + // byte extended_flags; // preloaded + // varint row_body_size; + // varint prev_unfiltered_size; // for backward traversing, ignored + // optional liveness_info; + // optional deletion_time; + // *** We read the columns in a separate method *** + // optional missing_columns; + // cell[] cells; // potentially only some + //}; + public int readStaticRowHeader(UnfilteredDescriptor unfilteredDescriptor) + { + if (state != STATIC_ROW_START) throw new IllegalStateException(); + try + { + unfilteredDescriptor.loadStaticRow(dataReader, serializationHeader, deserializationHelper, basicUnfilteredFlags, extendedFlags); + } + catch (IOException e) + { + return corruptSSTable(e); + } + + staticRowCellCursor.init(unfilteredDescriptor.rowColumns(), unfilteredDescriptor.livenessInfo()); + cellCursor = staticRowCellCursor; + if (!staticRowCellCursor.hasNext()) + { + try + { + return checkNextFlagsAfterStaticRowOrUnfilteredStart(false); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + else + { + return state = State.CELL_HEADER_START; + } + } + + public int copyCellValue(DataOutputPlus writer, byte[] buffer) throws IOException + { + if (state != CELL_VALUE_START) throw new IllegalStateException(); + if (cellCursor.cellType == null) throw new IllegalStateException(); + int length = cellCursor.cellType.valueLengthIfFixed(); + copyCellContents(writer, buffer, length); + + try + { + if (!cellCursor.hasNext()) + { + try + { + return checkNextFlagsAfterCellValuesEnd(); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + return state = State.CELL_END; + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + // TODO: move to cell cursor? maybe avoid copy through buffer? + private void copyCellContents(DataOutputPlus writer, byte[] transferBuffer, int length) throws IOException + { + if (length >= 0) + { + try + { + dataReader.readFully(transferBuffer, 0, length); + } + catch (Exception e) + { + corruptSSTable(e); + } + writer.write(transferBuffer, 0, length); + } + else + { + try + { + length = dataReader.readUnsignedVInt32(); + } + catch (IOException e) + { + corruptSSTable(e); + } + if (length < 0) + corruptSSTable("Corrupt (negative) value length encountered"); + writer.writeUnsignedVInt32(length); + int remaining = length; + while (remaining > 0) + { + int readLength = Math.min(remaining, transferBuffer.length); + try + { + dataReader.readFully(transferBuffer, 0, readLength); + } + catch (Exception e) + { + corruptSSTable(e); + } + writer.write(transferBuffer, 0, readLength); + remaining -= readLength; + } + } + } + + // struct row { + // byte flags; + // optional clustering_blocks; + // varint row_body_size; + // varint prev_unfiltered_size; // for backward traversing, ignored + // optional liveness_info; + // optional deletion_time; + // *** We read the columns in a separate step *** + // optional missing_columns; + // cell[] cells; // potentially only some + //}; + public int readRowHeader(UnfilteredDescriptor unfilteredDescriptor) + { + if (state != State.ROW_START) throw new IllegalStateException(); + if (!UnfilteredSerializer.isRow(basicUnfilteredFlags)) throw new IllegalStateException(); + try + { + unfilteredDescriptor.loadRow(dataReader, serializationHeader, deserializationHelper, basicUnfilteredFlags); + + rowCellCursor.init(unfilteredDescriptor.rowColumns(), unfilteredDescriptor.livenessInfo()); + cellCursor = rowCellCursor; + if (!rowCellCursor.hasNext()) + { + return checkNextFlagsAfterStaticRowOrUnfilteredStart(false); + } + else + { + return state = State.CELL_HEADER_START; + } + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + // TODO: introduce cell header class + public int readCellHeader() + { + if (state != State.CELL_HEADER_START) throw new IllegalStateException(); + try + { + if (cellCursor.readCellHeader()) + { + return state = State.CELL_VALUE_START; + } + if (!cellCursor.hasNext()) + return checkNextFlagsAfterCellValuesEnd(); + return state = State.CELL_END; + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + public int skipCellValue() + { + if (state != State.CELL_VALUE_START) throw new IllegalStateException(); + try + { + cellCursor.cellType.skipValue(dataReader); + return !cellCursor.hasNext() ? checkNextFlagsAfterCellValuesEnd() : (state = State.CELL_HEADER_START); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + /** + * See: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serialize(RangeTombstoneMarker, SerializationHelper, DataOutputPlus, long, int)} + *
+     * struct range_tombstone_marker {
+     *   byte flags = IS_MARKER;
+     *   byte kind_ordinal;
+     *   be16 bound_values_count;
+     *   struct clustering_block[] clustering_blocks;
+     *   varint marker_body_size;
+     *   varint prev_unfiltered_size;
+     * };
+     * struct range_tombstone_bound_marker : range_tombstone_marker {
+     *   struct delta_deletion_time deletion_time;
+     * };
+     * struct range_tombstone_boundary_marker : range_tombstone_marker {
+     *   struct delta_deletion_time end_deletion_time;
+ *       struct delta_deletion_time start_deletion_time;
+     * };
+     * 
+ * + */ + public int readTombstoneMarker(UnfilteredDescriptor unfilteredDescriptor) + { + try + { + if (state != TOMBSTONE_START) throw new IllegalStateException(); + if (!UnfilteredSerializer.isTombstoneMarker(basicUnfilteredFlags)) throw new IllegalStateException(); + unfilteredDescriptor.loadTombstone(dataReader, serializationHeader, basicUnfilteredFlags); + return checkNextFlagsAfterStaticRowOrUnfilteredStart(false); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + + /** + * {@link ClusteringPrefix.Serializer#deserializeValuesWithoutSize} + */ + static void readUnfilteredClustering(RandomAccessReader dataReader, AbstractType[] types, int clusteringColumnsBound, ResizableByteBuffer clustering) throws IOException + { + clustering.resetBuffer(); + if (clusteringColumnsBound == 0) { + return; + } + long clusteringBlockHeader = 0; + int fixedLengthClusteringLength = 0; + for (int clusteringIndex = 0; clusteringIndex < clusteringColumnsBound; clusteringIndex++) + { + // struct clustering_block { + // varint clustering_block_header; + // simple_cell[] clustering_cells; + // }; + if (clusteringIndex % 32 == 0) + { + if (fixedLengthClusteringLength != 0) { + clustering.loadPart(dataReader, fixedLengthClusteringLength); + fixedLengthClusteringLength = 0; + } + clusteringBlockHeader = dataReader.readUnsignedVInt(); + clustering.writeUnsignedVInt(clusteringBlockHeader); + } + + // load value if present + if ((clusteringBlockHeader & 0b11) == 0) + { + AbstractType type = types[clusteringIndex]; + if (type.isValueLengthFixed()) + { + fixedLengthClusteringLength += type.valueLengthIfFixed(); + } + else + { + if (fixedLengthClusteringLength != 0) { + clustering.loadPart(dataReader, fixedLengthClusteringLength); + fixedLengthClusteringLength = 0; + } + int varLength = dataReader.readUnsignedVInt32(); + clustering.writeUnsignedVInt(varLength); + clustering.loadPart(dataReader, varLength); + } + } + clusteringBlockHeader = clusteringBlockHeader >>> 2; + } + if (fixedLengthClusteringLength != 0) clustering.loadPart(dataReader, fixedLengthClusteringLength); + if (clusteringBlockHeader != 0) { + throw new IOException("Clustering block upper bits (those not associated with keys) expected to be 0:" + clusteringBlockHeader); + } + } + + private static void skipClustering(RandomAccessReader dataReader, AbstractType[] types, int clusteringColumnsBound) throws IOException + { + long clusteringBlockHeader = 0; + for (int clusteringIndex = 0; clusteringIndex < clusteringColumnsBound; clusteringIndex++) + { + // struct clustering_block { + // varint clustering_block_header; + // simple_cell[] clustering_cells; + // }; + if (clusteringIndex % 32 == 0) + { + clusteringBlockHeader = dataReader.readUnsignedVInt(); + } + // skip value if present + if ((clusteringBlockHeader & 0b11) == 0) + { + AbstractType type = types[clusteringIndex]; + int len = type.isValueLengthFixed() ? type.valueLengthIfFixed() : dataReader.readUnsignedVInt32(); + dataReader.skipBytes(len); + } + clusteringBlockHeader = clusteringBlockHeader >>> 2; + } + if (clusteringBlockHeader != 0) { + throw new IOException("Clustering block upper bits (those not associated with keys) expected to be 0:" + clusteringBlockHeader); + } + } + + /** + * {@link UnfilteredSerializer#deserializeRowBody(DataInputPlus, SerializationHeader, DeserializationHelper, int, int, Row.Builder)} + */ + static void readLivenessInfo(RandomAccessReader dataReader, SerializationHeader serializationHeader, DeserializationHelper deserializationHelper, int flags, ReusableLivenessInfo livenessInfo) throws IOException + { + long timestamp = LivenessInfo.NO_TIMESTAMP; + int ttl = LivenessInfo.NO_TTL; + long localExpirationTime = LivenessInfo.NO_EXPIRATION_TIME; + if (UnfilteredSerializer.hasTimestamp(flags)) + { + // struct liveness_info { + // varint64 delta_timestamp; + // optional delta_ttl; + // optional delta_local_deletion_time; + //}; + timestamp = serializationHeader.readTimestamp(dataReader); + if (UnfilteredSerializer.hasTTL(flags)) + { + ttl = serializationHeader.readTTL(dataReader); + localExpirationTime = Cell.decodeLocalDeletionTime(serializationHeader.readLocalDeletionTime(dataReader), ttl, deserializationHelper); + } + } + livenessInfo.reset(timestamp, ttl, localExpirationTime); + } + + // SKIPPING + public int skipPartition() + { + if (state == PARTITION_END) + return continueReading(); + + if (state == PARTITION_START) + { + try + { + int partitionKeyLength = dataReader.readUnsignedShort(); + dataReader.skipBytes(partitionKeyLength); + + // PARTITION DELETION TIME + deletionTimeSerializer.skip(dataReader); + checkNextFlagsAfterPartitionStart(true); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + else if (!isState(state, STATIC_ROW_START | ROW_START | TOMBSTONE_START | PARTITION_END)) + { + throw new IllegalStateException("Unexpected state: " + state); + } + + while (!isState(state,PARTITION_START | DONE)) + { + switch (state) + { + case STATIC_ROW_START: + state = skipStaticRow(true); + break; + case ROW_START: + case TOMBSTONE_START: + state = skipUnfiltered(true); + break; + } + } + return state; + } + + public int skipStaticRow(boolean autoContinue) + { + if (state != State.STATIC_ROW_START) throw new IllegalStateException(); + + try + { + long rowSize = dataReader.readUnsignedVInt(); + dataReader.seek(dataReader.getPosition() + rowSize); + return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue); + } + catch (IOException e) + { + return corruptSSTable(e); + } + } + + public int skipUnfiltered(boolean autoContinue) + { + if (!isState(state, ROW_START | TOMBSTONE_START)) + throw new IllegalStateException(); + + AbstractType[] types = clusteringColumnTypes; + int clusteringColumnsBound = types.length; + // tombstone markers have `kind` & `clusteringColumnsBound` + try + { + if (!UnfilteredSerializer.isRow(basicUnfilteredFlags)) + { + dataReader.readByte();// byte kind = + clusteringColumnsBound = dataReader.readUnsignedShort(); + } + /** + * {@link org.apache.cassandra.db.ClusteringPrefix.Deserializer} + */ + skipClustering(dataReader, types, clusteringColumnsBound); + // same for row/tombstone + long rowSize = dataReader.readUnsignedVInt(); + dataReader.seek(dataReader.getPosition() + rowSize); + + return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue); + } + catch (Exception e) + { + return corruptSSTable(e); + } + } + + public int skipRowCells(long unfilteredDataStart, long unfilteredSize, boolean autoContinue) + { + if (!(isState(state,CELL_HEADER_START | CELL_VALUE_START | CELL_END))) throw new IllegalStateException(); + + try + { + dataReader.seek(unfilteredDataStart + unfilteredSize); + return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue); + } + catch (IOException e) + { + return corruptSSTable(e); + } + } + + public int continueReading() { + // TODO: can be optimized by pre-calculating next state when the flags are read + switch (state) + { + case PARTITION_END: + state = dataReader.isEOF() ? DONE : PARTITION_START; + break; + case UNFILTERED_END: + if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags)) + { + state = PARTITION_END; + } + else + { + state = UnfilteredSerializer.isRow(basicUnfilteredFlags) ? ROW_START : TOMBSTONE_START; + } + break; + case CELL_END: + if (cellCursor.hasNext()) + { + state = CELL_HEADER_START; + } + else + { + state = UNFILTERED_END; + } + break; + default: + throw new IllegalStateException("Cannot continue reading in current state: " + state); + } + return state; + } + + private int checkNextFlagsAfterPartitionStart(boolean autoContinue) throws IOException + { + long preFlagsPosition = dataReader.getPosition(); + basicUnfilteredFlags = dataReader.readUnsignedByte(); + if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags)) + { + state = !autoContinue ? PARTITION_END : + dataReader.isEOF() ? DONE : PARTITION_START; + } + else if (UnfilteredSerializer.isExtended(basicUnfilteredFlags)) + { + state = STATIC_ROW_START; + extendedFlags = dataReader.readUnsignedByte(); + validateStaticRowFlags(preFlagsPosition); + } + else + { + state = UnfilteredSerializer.isRow(basicUnfilteredFlags) ? ROW_START : TOMBSTONE_START; + } + return state; + } + + private void validateStaticRowFlags(long preFlagsPosition) + { + if (!UnfilteredSerializer.isStatic(extendedFlags)) + { + corruptSSTable("Row at: " + preFlagsPosition + " has extended flags but is not static, extendedFlags: " + extendedFlags); + } + if (!UnfilteredSerializer.isRow(basicUnfilteredFlags)) + { + corruptSSTable("Static row at: " + preFlagsPosition + " is not a row, flags: " + basicUnfilteredFlags); + } + if (!hasStaticColumns) + { + corruptSSTable("Row at: " + preFlagsPosition + " is static, but table has no static columns " + ssTableReader.metadata()); + } + if (UnfilteredSerializer.deletionIsShadowable(extendedFlags)) + { + throw new UnsupportedOperationException("Static row at: " + preFlagsPosition + " has deletionIsShadowable, which is deprecated since 4.0"); + } + } + + private int checkNextFlagsAfterStaticRowOrUnfilteredStart(boolean autoContinue) throws IOException + { + int flags = this.basicUnfilteredFlags = dataReader.readUnsignedByte(); + if (UnfilteredSerializer.isExtended(flags)) + { + corruptSSTable("Unexpected static row (flags=" + flags + ") mid-partition, at position: " + (dataReader.getPosition() - 1)); + } + + if (!autoContinue) { + return this.state = UNFILTERED_END; + } + else + { + return this.state = nextStateMidPartition(flags); + } + } + + private int checkNextFlagsAfterCellValuesEnd() throws IOException + { + int flags = this.basicUnfilteredFlags = dataReader.readUnsignedByte(); + if (UnfilteredSerializer.isExtended(flags)) + { + corruptSSTable("Unexpected static row (flags=" + flags + ") mid-partition, at position: " + (dataReader.getPosition() - 1)); + } + return this.state = CELL_END; + } + + private int corruptSSTable(Exception e) + { + ssTableReader.markSuspect(); + if (e instanceof CorruptSSTableException) + throw (CorruptSSTableException) e; + + throw new CorruptSSTableException(e, ssTableReader.getFilename()); + } + + protected int corruptSSTable(String message) + { + return corruptSSTable(new IllegalStateException(message)); + } + + private int nextStateMidPartition(int basicUnfilteredFlags) + { + if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags)) + { + return dataReader.isEOF() ? DONE : PARTITION_START; + } + else if (UnfilteredSerializer.isRow(basicUnfilteredFlags)) + { + return ROW_START; + } + else + { + return TOMBSTONE_START; + } + } + + public boolean isEOF() { + return state == DONE || dataReader.isEOF(); + } + + public int state() + { + return state; + } + + public long position() { + return dataReader.getFilePointer(); + } + + public long uncompressedLength() + { + return ssTableReader.uncompressedLength(); + } + + public SSTableReader ssTableReader() + { + return ssTableReader; + } + + public CellCursor cellCursor() + { + return cellCursor; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java new file mode 100644 index 0000000000..2a170acb73 --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/SSTableCursorWriter.java @@ -0,0 +1,689 @@ +/* + * 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.sstable; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.google.common.primitives.Ints; + +import org.agrona.collections.IntArrayList; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ClusteringPrefix; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.guardrails.Guardrails; +import org.apache.cassandra.db.guardrails.Threshold; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.SerializationHelper; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.io.FSWriteError; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SortedTableWriter; +import org.apache.cassandra.io.sstable.format.big.BigFormatPartitionWriter; +import org.apache.cassandra.io.sstable.format.big.BigTableWriter; +import org.apache.cassandra.io.sstable.format.big.RowIndexEntry; +import org.apache.cassandra.io.sstable.metadata.MetadataCollector; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.io.util.SequentialWriter; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.utils.BloomFilter; +import org.apache.cassandra.utils.ByteArrayUtil; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.concurrent.Ref; + +import static org.apache.cassandra.db.rows.UnfilteredSerializer.*; + +public class SSTableCursorWriter implements AutoCloseable +{ + private static final UnfilteredSerializer SERIALIZER = UnfilteredSerializer.serializer; + private static final ColumnMetadata[] EMPTY_COL_META = new ColumnMetadata[0]; + private final SortedTableWriter ssTableWriter; + private final SequentialWriter dataWriter; + private final SortedTableWriter.AbstractIndexWriter indexWriter; + private final DeletionTime.Serializer deletionTimeSerializer; + private final MetadataCollector metadataCollector; + private final SerializationHeader serializationHeader; + /** + * See: {@link BloomFilter#reusableIndexes} + */ + private final long[] reusableIndexes = new long[21]; + private final boolean hasStaticColumns; + + private long partitionStart; + // ROW contents, needed because of the order of writing and the var int fields + private int rowFlags; // discovered as we go along + private int rowExtendedFlags; + private final DataOutputBuffer rowHeaderBuffer = new DataOutputBuffer(); // holds the contents between FLAGS and SIZE + private final DataOutputBuffer rowBuffer = new DataOutputBuffer(); + private final ReusableDeletionTime openMarker = ReusableDeletionTime.live(); + + private final ColumnMetadata[] staticColumns; + private final ColumnMetadata[] regularColumns; + private final IntArrayList missingColumns = new IntArrayList(); + private ColumnMetadata[] columns; // points to static/regular + private int columnsWrittenCount = 0; + private int nextCellIndex = 0; + // Index info + private final DataOutputBuffer rowIndexEntries = new DataOutputBuffer(); + private final IntArrayList rowIndexEntriesOffsets = new IntArrayList(); + private final ClusteringDescriptor rowIndexEntryLastClustering; + private int indexBlockStartOffset; + private int rowIndexEntryOffset; + private final int indexBlockThreshold; + + private SSTableCursorWriter( + Descriptor desc, + SortedTableWriter ssTableWriter, + SequentialWriter dataWriter, + SortedTableWriter.AbstractIndexWriter indexWriter, + MetadataCollector metadataCollector, + SerializationHeader serializationHeader) + { + this.ssTableWriter = ssTableWriter; + this.dataWriter = dataWriter; + this.indexWriter = indexWriter; + this.deletionTimeSerializer = DeletionTime.getSerializer(desc.version); + this.metadataCollector = metadataCollector; + this.serializationHeader = serializationHeader; + hasStaticColumns = serializationHeader.hasStatic(); + staticColumns = hasStaticColumns ? serializationHeader.columns(true).toArray(EMPTY_COL_META) : EMPTY_COL_META; + regularColumns = serializationHeader.columns(false).toArray(EMPTY_COL_META); + this.indexBlockThreshold = DatabaseDescriptor.getColumnIndexSize(BigFormatPartitionWriter.DEFAULT_GRANULARITY); + rowIndexEntryLastClustering = new ClusteringDescriptor(serializationHeader.clusteringTypes().toArray(AbstractType[]::new)); + } + + public SSTableCursorWriter(SortedTableWriter ssTableWriter) + { + this(ssTableWriter.descriptor, + ssTableWriter, + ssTableWriter.dataWriter, + ssTableWriter.indexWriter, + ssTableWriter.metadataCollector, + ssTableWriter.partitionWriter.getHeader()); + } + + @Override + public void close() + { + SSTableReader finish = ssTableWriter.finish(false); + if (finish != null) { + Ref ref = finish.ref(); + if (ref != null) ref.close(); + } + ssTableWriter.close(); + } + + public long getPartitionStart() + { + return partitionStart; + } + + public long getPosition() + { + return dataWriter.position(); + } + + public int writePartitionStart(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime) throws IOException + { + rowIndexEntries.clear(); + rowIndexEntriesOffsets.clear(); + rowIndexEntryOffset = 0; + openMarker.resetLive(); + + partitionStart = dataWriter.position(); + writePartitionHeader(partitionKey, partitionKeyLength, partitionDeletionTime); + updateIndexBlockStartOffset(dataWriter.position()); + return indexBlockStartOffset; + } + + public void writePartitionEnd(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime, int headerLength) throws IOException + { + SERIALIZER.writeEndOfPartition(dataWriter); + long partitionEnd = dataWriter.position(); + long partitionSize = partitionEnd - partitionStart; + addPartitionMetadata(partitionKey, partitionKeyLength, partitionSize, partitionDeletionTime); + + /** {@link SortedTableWriter#endPartition(DecoratedKey, DeletionTime)} + lastWrittenKey = key; // tracked for verification, see {@link SortedTableWriter#verifyPartition(DecoratedKey)}, checking the key size and sorting + // first/last are retained for metadata {@link org.apache.cassandra.io.sstable.format.SSTableWriter#finalizeMetadata()}. They are also exposed via + // getters from the writer, but usage is unclear. + last = lastWrittenKey; + if (first == null) + first = lastWrittenKey; + // this is implemented differently for BIG/BTI + createRowIndexEntry(key, partitionLevelDeletion, partitionEnd - 1); + */ + appendBIGIndex(partitionKey, partitionKeyLength, partitionStart, headerLength, partitionDeletionTime, partitionEnd); + } + + private void appendBIGIndex(byte[] key, int keyLength, long partitionStart, int headerLength, DeletionTime partitionDeletionTime, long partitionEnd) throws IOException + { + /** + * {@link BigTableWriter#createRowIndexEntry(DecoratedKey, DeletionTime, long)} + * {@link BigTableWriter.IndexWriter#append(DecoratedKey, RowIndexEntry, long, ByteBuffer)} + * + */ + BigTableWriter.IndexWriter indexWriter = (BigTableWriter.IndexWriter) this.indexWriter; + SequentialWriter indexFileWriter = indexWriter.writer; + ((BloomFilter)indexWriter.bf).add(key, 0, keyLength, reusableIndexes); + long indexStart = indexFileWriter.position(); + try + { + ByteArrayUtil.writeWithShortLength(key, 0, keyLength, indexFileWriter); + + indexFileWriter.writeUnsignedVInt(partitionStart); + + // if the list of entries has one or fewer entries, no point in index entries. + /** See: {@link org.apache.cassandra.io.sstable.format.big.RowIndexEntry#create} */ + if (rowIndexEntriesOffsets.size() <= 1) + { + /** + * {@link RowIndexEntry#serialize(DataOutputPlus, ByteBuffer)} + */ + indexFileWriter.writeUnsignedVInt32(0); // size + } + else { + // add last block + long indexBlockSize = (partitionEnd - partitionStart - 1) - indexBlockStartOffset; + if (indexBlockSize != 0) { + addIndexBlock(partitionEnd - 1, indexBlockSize); + } + // if we have intermeddiate index info elements we also need to serialize the partitionDeletionTime + /** {@link RowIndexEntry.IndexedEntry#serialize(DataOutputPlus, ByteBuffer) */ + // size up to the offsets? + int endOfEntries = rowIndexEntries.getLength(); + // Write the headerLength, partitionDeletionTime and rowIndexEntriesOffsets.size() after the entries, + // just to calculate size. + rowIndexEntries.writeUnsignedVInt((long)headerLength); + deletionTimeSerializer.serialize(partitionDeletionTime, rowIndexEntries); + + rowIndexEntries.writeUnsignedVInt32(rowIndexEntriesOffsets.size()); // number of entries + + // bytes until offsets + int entriesAndOffsetsSize = rowIndexEntries.getLength() + rowIndexEntriesOffsets.size() * 4; + assert entriesAndOffsetsSize > 0; + indexFileWriter.writeUnsignedVInt32(entriesAndOffsetsSize); // size != 0 + // copy the header elements + indexFileWriter.write(rowIndexEntries.getData(), endOfEntries, rowIndexEntries.getLength() - endOfEntries); + indexFileWriter.write(rowIndexEntries.getData(), 0, endOfEntries); + for (int i = 0; i < rowIndexEntriesOffsets.size(); i++) + { + int offset = rowIndexEntriesOffsets.get(i); + indexFileWriter.writeInt(offset); + } + } + } + catch (IOException e) + { + throw new FSWriteError(e, indexFileWriter.getPath()); + } + indexWriter.summary.maybeAddEntry(key, 0, keyLength, indexStart); + } + + final long guardrailsPartitionSizeWarning = Guardrails.partitionSize.warnValue(null); + final long guardrailsPartitionTombstonesWarning = Guardrails.partitionTombstones.warnValue(null); + + /** + * update metadata like {@link SortedTableWriter#endPartition} and {@link SortedTableWriter#startPartition} + */ + private void addPartitionMetadata(byte[] partitionKey, int partitionKeyLength, long partitionSize, DeletionTime partitionDeletionTime) + { + if (partitionSize > guardrailsPartitionSizeWarning) + guardPartitionThreshold(Guardrails.partitionSize, partitionKey, partitionKeyLength, partitionSize); + + if (metadataCollector.totalTombstones > guardrailsPartitionTombstonesWarning) + guardPartitionThreshold(Guardrails.partitionTombstones, partitionKey, partitionKeyLength, metadataCollector.totalTombstones); + + metadataCollector.updatePartitionDeletion(partitionDeletionTime); + metadataCollector.addPartitionSizeInBytes(partitionSize); + metadataCollector.addKey(partitionKey, 0, partitionKeyLength); + metadataCollector.addCellPerPartitionCount(); + } + + private void guardPartitionThreshold(Threshold guardrail, byte[] partitionKey, int partitionKeyLength, long size) + { + if (guardrail.triggersOn(size, null)) + { + String message = String.format("%s.%s:%s on sstable %s", + ssTableWriter.metadata().keyspace, + ssTableWriter.metadata().name, + ssTableWriter.metadata().partitionKeyType.getString(ByteBuffer.wrap(partitionKey, 0, partitionKeyLength)), + ssTableWriter.getFilename()); + guardrail.guard(size, message, true, null); + } + } + + private void writePartitionHeader(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime) throws IOException + { + dataWriter.writeShort(partitionKeyLength); + dataWriter.write(partitionKey, 0, partitionKeyLength); + deletionTimeSerializer.serialize(partitionDeletionTime, dataWriter); + } + + public boolean writeEmptyStaticRow() throws IOException + { + if (!hasStaticColumns) + return false; + rowFlags = UnfilteredSerializer.EXTENSION_FLAG; + rowExtendedFlags = UnfilteredSerializer.IS_STATIC; + columns = staticColumns; + // TOD: we should be able to skip the use of the row buffers in this special case, maybe it doesn't matter + rowHeaderBuffer.clear(); + // NOTE: if we are to write this value (which is not used), this is where we should compute it. + rowHeaderBuffer.writeUnsignedVInt32(0); + rowBuffer.clear(); + columnsWrittenCount = 0; + missingColumns.clear(); + writeRowEnd(null, false); + + updateIndexBlockStartOffset(dataWriter.position()); + return true; + } + + public void writeRowStart(LivenessInfo livenessInfo, DeletionTime deletionTime, boolean isStatic) throws IOException + { + if (isStatic) { + rowFlags = UnfilteredSerializer.EXTENSION_FLAG; + rowExtendedFlags = UnfilteredSerializer.IS_STATIC; + columns = staticColumns; + } + else { + rowFlags = 0; + rowExtendedFlags = 0; + columns = regularColumns; + } + // NOTE: Data after this point needs a computed ahead of write size. This, combined with the cost of rewriting + // the size after the writing completes, means we have to buffer the row timestamps (most likely to differ in length) + // and the row columns data (will differ if they use their own timestamps, probably). Unfortunate. + // rest of header + rowHeaderBuffer.clear(); + missingColumns.clear(); + rowBuffer.clear(); + columnsWrittenCount = 0; + nextCellIndex = 0; + + // NOTE: if we are to write this value (which is not used), this is where we should compute it. + rowHeaderBuffer.writeUnsignedVInt32(0); + + // copy TS/TTL/deletion data + rowFlags |= writeRowTimeData(livenessInfo, deletionTime, rowHeaderBuffer); + } + + /** + * See {@link UnfilteredSerializer#serialize(Row, SerializationHelper, DataOutputPlus, long, int)} + */ + private int writeRowTimeData(LivenessInfo livenessInfo, DeletionTime deletionTime, DataOutputPlus writer) throws IOException + { + int flags = 0; + boolean writtenLivenessMetadata = false; + + if (!livenessInfo.isEmpty()) + { + flags |= HAS_TIMESTAMP; + serializationHeader.writeTimestamp(livenessInfo.timestamp(), writer); + metadataCollector.update(livenessInfo); + writtenLivenessMetadata = true; + } + if (livenessInfo.isExpiring()) + { + flags |= HAS_TTL; + serializationHeader.writeTTL(livenessInfo.ttl(), writer); + serializationHeader.writeLocalDeletionTime(livenessInfo.localExpirationTime(), writer); + if (!writtenLivenessMetadata) metadataCollector.update(livenessInfo); + } + if (!deletionTime.isLive()) + { + flags |= HAS_DELETION; + writeDeletionTime(deletionTime, writer); + } + + /** + * Metadata calls matching: {@link org.apache.cassandra.db.rows.Rows#collectStats} + * But the collection of data is conditional and the cell metadata is collected elsewhere. + */ + return flags; + } + + private void writeDeletionTime(DeletionTime deletionTime, DataOutputPlus writer) throws IOException + { + serializationHeader.writeDeletionTime(deletionTime, writer); + metadataCollector.update(deletionTime); + } + + public void writeCellHeader(int cellFlags, ReusableLivenessInfo cellLiveness, ColumnMetadata cellColumn) throws IOException + { + for (; nextCellIndex < columns.length; nextCellIndex++) { + if (columns[nextCellIndex].compareTo(cellColumn) == 0) + break; + missingColumns.addInt(nextCellIndex); + } + if (nextCellIndex == columns.length) + throw new IllegalStateException("Column not found: " + cellColumn +" or cell writes out of order, or bug."); + nextCellIndex++; + writeCellHeader(cellFlags, cellLiveness, rowBuffer); + } + + private void writeCellHeader(int cellFlags, ReusableLivenessInfo cellLiveness, DataOutputPlus writer) throws IOException + { + columnsWrittenCount++; + writer.writeByte(cellFlags); + if (!Cell.Serializer.useRowTimestamp(cellFlags)) { + long timestamp = cellLiveness.timestamp(); + serializationHeader.writeTimestamp(timestamp, writer); + } + if (!Cell.Serializer.useRowTTL(cellFlags)) { + boolean isDeleted = Cell.Serializer.isDeleted(cellFlags); + boolean isExpiring = Cell.Serializer.isExpiring(cellFlags); + if (isDeleted || isExpiring) { + // TODO: is this conversion from LET to LDT correct? + serializationHeader.writeLocalDeletionTime(cellLiveness.localExpirationTime(), writer); + } + if (isExpiring) { + serializationHeader.writeTTL(cellLiveness.ttl(), writer); + } + } + /** + * matching {@link org.apache.cassandra.db.rows.Cells#collectStats}; + */ + metadataCollector.updateCellLiveness(cellLiveness); + } + + public int writeCellValue(SSTableCursorReader cursor, byte[] copyColumnValueBuffer) throws IOException + { + return cursor.copyCellValue(rowBuffer, copyColumnValueBuffer); + } + + public void writeCellValue(DataOutputBuffer tempCellBuffer) throws IOException + { + rowBuffer.write(tempCellBuffer.getData(), 0, tempCellBuffer.getLength()); + } + + public void writeRowEnd(UnfilteredDescriptor rHeader, boolean updateClusteringMetadata) throws IOException + { + boolean isExtended = isExtended(rowFlags); + boolean isStatic = isExtended && UnfilteredSerializer.isStatic(rowExtendedFlags); + int columnsLength = columns.length; + if (columnsWrittenCount == columnsLength) + { + rowFlags |= HAS_ALL_COLUMNS; + } + else if (columnsWrittenCount == 0) { + // Same as Columns.serializer.serializeSubset(Columns.NONE, serializationHeader.columns(isStatic), rowHeaderBuffer) + if (columnsLength < 64) { + // all the bits are set, because all the columns are missing, value is always positive + rowHeaderBuffer.writeUnsignedVInt(-1L >>> (64 - columnsLength)); + } + else { + // no columns are present, nothing to write + rowHeaderBuffer.writeUnsignedVInt32(columnsLength); + } + } + else if (columnsWrittenCount < columnsLength) + { + for (; nextCellIndex < columnsLength; nextCellIndex++) + missingColumns.addInt(nextCellIndex); + + if (columnsLength < 64) { + // set a bit for every missing column + long mask = 0; + for (int missingIndex : missingColumns) { + mask |= (1L << missingIndex); + } + rowHeaderBuffer.writeUnsignedVInt(mask); + } + else { + encodeLargeColumnsSubset(); + } + } + long unfilteredStartPosition = dataWriter.position(); + /** See: {@link UnfilteredSerializer#serialize} */ + dataWriter.writeByte(rowFlags); + if (isExtended) + { + dataWriter.writeByte(rowExtendedFlags); + } + + if (!isStatic) + { + byte[] clustering = rHeader.clusteringBytes(); + int clusteringLength = rHeader.clusteringLength(); + dataWriter.write(clustering, 0, clusteringLength); + } + + // Now that we know the size, write it + the rest of the data + dataWriter.writeUnsignedVInt32(rowHeaderBuffer.getLength() + rowBuffer.getLength()); + + dataWriter.write(rowHeaderBuffer.getData(), 0, rowHeaderBuffer.getLength()); + dataWriter.write(rowBuffer.getData(), 0, rowBuffer.getLength()); + + long unfilteredEndPosition = getPosition(); + + /** + * Matching the: {@link org.apache.cassandra.db.rows.Rows#collectStats} along with above cell level metadata updates + */ + metadataCollector.updateColumnSetPerRow(columnsWrittenCount); + + if (isStatic) + { + updateIndexBlockStartOffset(dataWriter.position()); + } + else + { + updateMetadataAndIndexBlock(rHeader, unfilteredStartPosition, unfilteredEndPosition, updateClusteringMetadata); + } + } + + /** + * See: {@link org.apache.cassandra.io.sstable.format.SortedTableWriter#addRangeTomstoneMarker} + */ + public void writeRangeTombstone(UnfilteredDescriptor rangeTombstone, boolean updateClusteringMetadata) throws IOException + { + int tombstoneKind = rangeTombstone.clusteringKindEncoded(); + ClusteringPrefix.Kind kind = ClusteringPrefix.Kind.values()[tombstoneKind]; + long unfilteredStartPosition = getPosition(); + /** See: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serialize */ + dataWriter.writeByte((byte)IS_MARKER); + /** See: {@link org.apache.cassandra.db.ClusteringBoundOrBoundary.Serializer#serialize} */ + dataWriter.writeByte(tombstoneKind); + dataWriter.writeShort(rangeTombstone.clusteringColumnsBound()); + + int clusteringLength = rangeTombstone.clusteringLength(); + if (clusteringLength != 0) + { + byte[] clustering = rangeTombstone.clusteringBytes(); + dataWriter.write(clustering, 0, clusteringLength); + } + rowHeaderBuffer.clear(); + // TODO: previousUnfilteredSize + rowHeaderBuffer.writeUnsignedVInt32(0); + + if (kind.isBoundary()) + { + writeDeletionTime(rangeTombstone.deletionTime(), rowHeaderBuffer); + writeDeletionTime(rangeTombstone.deletionTime2(), rowHeaderBuffer); + openMarker.reset(rangeTombstone.deletionTime2()); + } + else + { + writeDeletionTime(rangeTombstone.deletionTime(), rowHeaderBuffer); + if (kind.isOpen(false)) + openMarker.reset(rangeTombstone.deletionTime()); + else + openMarker.resetLive(); + } + + dataWriter.writeUnsignedVInt32(rowHeaderBuffer.getLength()); + dataWriter.write(rowHeaderBuffer.getData(), 0, rowHeaderBuffer.getLength()); + + long unfilteredEndPosition = getPosition(); + + /** {@link org.apache.cassandra.io.sstable.format.big.BigFormatPartitionWriter#addUnfiltered(Unfiltered)} */ + // if we hit the index block size that we have to index after, go ahead and index it. + updateMetadataAndIndexBlock(rangeTombstone, unfilteredStartPosition, unfilteredEndPosition, updateClusteringMetadata); + } + + private void updateMetadataAndIndexBlock( + UnfilteredDescriptor unfilteredDescriptor, + long unfilteredStartPosition, + long unfilteredEndPosition, + boolean updateClusteringMetadata) throws IOException + { + if (updateClusteringMetadata) updateClusteringMetadata(unfilteredDescriptor); + // write the first clustering into rowIndexEntries buffer (we will need it unless we never write the first entry) + if (unfilteredStartPosition == indexBlockStartOffset || (rowIndexEntryOffset == rowIndexEntries.position())) { + writeClusteringToRowIndexEntries(unfilteredDescriptor); + } + else + { + rowIndexEntryLastClustering.copy(unfilteredDescriptor); + } + /** {@link BigFormatPartitionWriter#addUnfiltered(Unfiltered)} */ + // if we hit the index block size that we have to index after, go ahead and index it. + long indexBlockSize = currentOffsetInPartition(unfilteredEndPosition) - indexBlockStartOffset; + if (indexBlockSize >= this.indexBlockThreshold) + addIndexBlock(unfilteredEndPosition, indexBlockSize); + } + + public void updateClusteringMetadata(UnfilteredDescriptor unfilteredDescriptor) + { + metadataCollector.updateClusteringValues(unfilteredDescriptor); + } + + /** + * See: + * {@link BigFormatPartitionWriter#addIndexBlock()} + * - {@link org.apache.cassandra.io.sstable.IndexInfo.Serializer#serialize(org.apache.cassandra.io.sstable.IndexInfo, org.apache.cassandra.io.util.DataOutputPlus)} + */ + private void addIndexBlock(long endOfRowPosition, long indexBlockSize) throws IOException + { + if (rowIndexEntriesOffsets.isEmpty() && rowIndexEntryOffset != 0) { + throw new IllegalStateException(); + } + + // serialize the index info + /** {@link org.apache.cassandra.io.sstable.IndexInfo.Serializer#serialize(org.apache.cassandra.io.sstable.IndexInfo, org.apache.cassandra.io.util.DataOutputPlus)}*/ + rowIndexEntriesOffsets.addInt(rowIndexEntryOffset); + + // first clustering is already in, write last entry + if (rowIndexEntryLastClustering.clusteringLength() == 0) { + // first entry is the last entry, copy it + byte[] entriesData = rowIndexEntries.getData(); + long endOfFirstEntry = rowIndexEntries.position(); + rowIndexEntries.write(entriesData, rowIndexEntryOffset, (int) (endOfFirstEntry - rowIndexEntryOffset)); + } + else + { + writeClusteringToRowIndexEntries(rowIndexEntryLastClustering); + rowIndexEntryLastClustering.resetClustering(); + } + rowIndexEntries.writeUnsignedVInt((long)indexBlockStartOffset); + rowIndexEntries.writeVInt(indexBlockSize - IndexInfo.Serializer.WIDTH_BASE); + + boolean isDeleteTimePresent = !openMarker.isLive(); + rowIndexEntries.writeBoolean(isDeleteTimePresent); + if (isDeleteTimePresent) + deletionTimeSerializer.serialize(openMarker, rowIndexEntries); + // next block starts + rowIndexEntryOffset = Ints.checkedCast(rowIndexEntries.position()); + updateIndexBlockStartOffset(endOfRowPosition); + } + + private void updateIndexBlockStartOffset(long endOfRowPosition) + { + indexBlockStartOffset = (int) (endOfRowPosition - partitionStart); + } + + private void writeClusteringToRowIndexEntries(ClusteringDescriptor clustering) throws IOException + { + ClusteringPrefix.Kind kind = clustering.clusteringKind(); + rowIndexEntries.writeByte(kind.ordinal()); + if (kind != ClusteringPrefix.Kind.CLUSTERING) + rowIndexEntries.writeShort(clustering.clusteringColumnsBound()); + rowIndexEntries.write(clustering.clusteringBytes(), 0, clustering.clusteringLength()); + } + + private long currentOffsetInPartition(long position) + { + return position - partitionStart; + } + + private void encodeLargeColumnsSubset() throws IOException + { + // no columns are present, nothing to write + rowHeaderBuffer.writeUnsignedVInt32(missingColumns.size()); + if (missingColumns.size() > columns.length / 2) + { + // write present columns + int presentIndex = 0; + int missingIndex = 0; + for (int i = 0; i < missingColumns.size(); i++) + { + missingIndex = missingColumns.get(i); + for (; presentIndex < missingIndex; presentIndex++) + rowHeaderBuffer.writeUnsignedVInt32(presentIndex); + presentIndex = missingIndex + 1; + } + if (missingIndex < columns.length-1) { + for (; presentIndex < missingIndex; presentIndex++) + rowHeaderBuffer.writeUnsignedVInt32(presentIndex); + } + } + else + { + // write missing columns + for (int missingIndex : missingColumns) { + rowHeaderBuffer.writeUnsignedVInt32(missingIndex); + } + } + } + + public void setLast(ByteBuffer key) + { + IPartitioner partitioner = ssTableWriter.getPartitioner(); + DecoratedKey last = partitioner.decorateKey(ByteBufferUtil.clone(key)); + ssTableWriter.setLast(last); + } + + public void setFirst(ByteBuffer key) + { + IPartitioner partitioner = ssTableWriter.getPartitioner(); + DecoratedKey first = partitioner.decorateKey(ByteBufferUtil.clone(key)); + ssTableWriter.setFirst(first); + ssTableWriter.setLast(first); + } + + public IPartitioner partitioner() + { + return ssTableWriter.getPartitioner(); + } + + public DeletionTime openMarker() { + return openMarker; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java index d90cc898bc..59b52ca77a 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleUnsortedWriter.java @@ -68,7 +68,7 @@ class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter private final BlockingQueue writeQueue = newBlockingQueue(0); private final DiskWriter diskWriter = new DiskWriter(); - SSTableSimpleUnsortedWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) + public SSTableSimpleUnsortedWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) { this(null, directory, metadata, columns, maxSSTableSizeInMiB); } @@ -198,7 +198,7 @@ class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter } } - static class SyncException extends RuntimeException + public static class SyncException extends RuntimeException { SyncException(IOException ioe) { diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java index 05120fa65d..7b5fb8a89e 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableSimpleWriter.java @@ -64,7 +64,7 @@ class SSTableSimpleWriter extends AbstractSSTableSimpleWriter * @param maxSSTableSizeInMiB defines the max SSTable size if the value is positive. * Any non-positive value indicates the sstable size is unlimited. */ - protected SSTableSimpleWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) + public SSTableSimpleWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) { this(null, directory, metadata, columns, maxSSTableSizeInMiB); } diff --git a/src/java/org/apache/cassandra/io/sstable/UnfilteredDescriptor.java b/src/java/org/apache/cassandra/io/sstable/UnfilteredDescriptor.java new file mode 100644 index 0000000000..df04c4ab8c --- /dev/null +++ b/src/java/org/apache/cassandra/io/sstable/UnfilteredDescriptor.java @@ -0,0 +1,226 @@ +/* + * 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.sstable; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.cassandra.db.Columns; +import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime; +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.db.SerializationHeader; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.io.util.RandomAccessReader; + +public class UnfilteredDescriptor extends ClusteringDescriptor +{ + private final ReusableLivenessInfo rowLivenessInfo = new ReusableLivenessInfo(); + private final ReusableDeletionTime deletionTime = ReusableDeletionTime.live(); + private final ReusableDeletionTime deletionTime2 = ReusableDeletionTime.live(); + + private long position; + private int flags; + private int extendedFlags; + + private long unfilteredSize; + private long unfilteredDataStart; + private long prevUnfilteredSize; + Columns rowColumns; + + public UnfilteredDescriptor(AbstractType[] clusteringTypes) + { + super(clusteringTypes); + } + + void loadTombstone(RandomAccessReader dataReader, + SerializationHeader serializationHeader, + int flags) throws IOException + { + this.flags = flags; + this.extendedFlags = 0; + rowColumns = null; + byte clusteringKind = dataReader.readByte(); + if (clusteringKind == STATIC_CLUSTERING_KIND || clusteringKind == ROW_CLUSTERING_KIND) { + // STATIC_CLUSTERING or CLUSTERING -> no deletion info, should not happen + throw new IllegalStateException(); + } + + int columnsBound = dataReader.readUnsignedShort(); + loadClustering(dataReader, clusteringKind, columnsBound); + unfilteredSize = dataReader.readUnsignedVInt(); + prevUnfilteredSize = dataReader.readUnsignedVInt(); // debug only, unused otherwise + if (clusteringKind == EXCL_END_INCL_START_BOUNDARY_CLUSTERING_KIND || + clusteringKind == INCL_END_EXCL_START_BOUNDARY_CLUSTERING_KIND) + { + // boundary + // CLOSE + serializationHeader.readDeletionTime(dataReader, deletionTime); + // OPEN + serializationHeader.readDeletionTime(dataReader, deletionTime2); + } + else + { + // bound + // CLOSE|OPEN + serializationHeader.readDeletionTime(dataReader, deletionTime); + } + } + + void loadRow(RandomAccessReader dataReader, + SerializationHeader serializationHeader, + DeserializationHelper deserializationHelper, + int flags) throws IOException { + // body = whatever is covered by size, so inclusive of the prev_row_size inclusive of flags + position = dataReader.getPosition() - 1; + this.flags = flags; + this.extendedFlags = 0; + + loadClustering(dataReader, ROW_CLUSTERING_KIND, this.clusteringTypes.length); + + rowColumns = serializationHeader.columns(false); + + loadCommonRowFields(dataReader, serializationHeader, deserializationHelper, flags); + } + + void loadStaticRow(RandomAccessReader dataReader, + SerializationHeader serializationHeader, + DeserializationHelper deserializationHelper, + int flags, + int extendedFlags) throws IOException { + // body = whatever is covered by size, so inclusive of the prev_row_size inclusive of flags + position = dataReader.getPosition() - 2; + this.flags = flags; + this.extendedFlags = extendedFlags; + // no clustering + loadClustering(dataReader, STATIC_CLUSTERING_KIND, 0); + rowColumns = serializationHeader.columns(true); + + loadCommonRowFields(dataReader, serializationHeader, deserializationHelper, flags); + } + + private void loadCommonRowFields(RandomAccessReader dataReader, + SerializationHeader serializationHeader, + DeserializationHelper deserializationHelper, + int flags) throws IOException + { + unfilteredSize = dataReader.readUnsignedVInt(); + unfilteredDataStart = dataReader.getPosition(); + prevUnfilteredSize = dataReader.readUnsignedVInt(); // debug only, unused otherwise + + SSTableCursorReader.readLivenessInfo(dataReader, + serializationHeader, + deserializationHelper, + flags, + rowLivenessInfo); + + if (UnfilteredSerializer.hasDeletion(flags)) + { + // struct delta_deletion_time { + // varint delta_marked_for_delete_at; + // varint delta_local_deletion_time; + //}; + serializationHeader.readDeletionTime(dataReader, deletionTime); + } + else + { + deletionTime.resetLive(); + } + if (!UnfilteredSerializer.hasAllColumns(flags)) + { + // TODO: re-implement GC free + rowColumns = Columns.serializer.deserializeSubset(rowColumns, dataReader); + } + } + + public void resetUnfiltered() + { + resetClustering(); + position = 0; + flags = 0; + extendedFlags = 0; + unfilteredSize = 0; + unfilteredDataStart = 0; + prevUnfilteredSize = 0; + rowColumns = null; + } + + public long position() + { + return position; + } + + public ReusableLivenessInfo livenessInfo() + { + return rowLivenessInfo; + } + + public ReusableDeletionTime deletionTime() + { + return deletionTime; + } + + public ReusableDeletionTime openDeletionTime() + { + return isBoundary() ? deletionTime2 : isEndBound() ? null : deletionTime; + } + + public ReusableDeletionTime deletionTime2() + { + return deletionTime2; + } + + public int flags() + { + return flags; + } + + public long size() + { + return unfilteredSize; + } + + public long dataStart() + { + return unfilteredDataStart; + } + + public Columns rowColumns() + { + return rowColumns; + } + + @Override + public String toString() + { + return "UnfilteredDescriptor{" + + "rowLivenessInfo=" + rowLivenessInfo + + ", deletionTime=" + deletionTime + + ", position=" + position + + ", flags=" + flags + + ", extFlags=" + extendedFlags + + ", unfilteredSize=" + unfilteredSize + + ", prevUnfilteredSize=" + prevUnfilteredSize + + ", unfilteredDataStart=" + unfilteredDataStart + + ", rowColumns=" + rowColumns + + ", clusteringTypes=" + Arrays.toString(clusteringTypes()) + + '}'; + } +} diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java index bcd607dc25..93915b1ba2 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableReader.java @@ -926,7 +926,16 @@ public abstract class SSTableReader extends SSTable implements UnfilteredSource, /** * Returns a {@link KeyReader} over all keys in the sstable. */ - public abstract KeyReader keyReader() throws IOException; + public final KeyReader keyReader() throws IOException { + return keyReader(false); + } + + /** + * Returns a {@link KeyReader} over all keys in the sstable. + * + * @param detailed should the iterator also provide details per partition entry(e.g. row entry details) + */ + public abstract KeyReader keyReader(boolean detailed) throws IOException; /** * Returns a {@link KeyReader} over all keys in the sstable after a given key. diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java index 28035a85da..0b54fec2c5 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableScanner.java @@ -280,4 +280,12 @@ implements ISSTableScanner } } } + + @Override + public boolean isFullRange() + { + return dataRange == null || (dataRange.startKey().equals(sstable.getFirst()) && + dataRange.stopKey().equals(sstable.getLast()) && + dataRange.isUnrestricted(sstable.metadata())); + } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java index 69145e1e53..cf40f4e34b 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableSimpleScanner.java @@ -129,6 +129,13 @@ implements ISSTableScanner return ImmutableSet.of(sstable); } + @Override + public boolean isFullRange() + { + // hasNext will init start and end + return hasNext() && currentStartPosition == 0 && currentEndPosition == sstable.uncompressedLength(); + } + public TableMetadata metadata() { return sstable.metadata(); diff --git a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java index c09459b37d..8543da23e2 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SSTableWriter.java @@ -77,8 +77,8 @@ public abstract class SSTableWriter extends SSTable implements Transactional protected boolean isTransient; protected long maxDataAge = -1; protected final long keyCount; - protected final MetadataCollector metadataCollector; - protected final SerializationHeader header; + public final MetadataCollector metadataCollector; + public final SerializationHeader header; protected final List observers; protected final MmappedRegionsCache mmappedRegionsCache; protected final TransactionalProxy txnProxy = txnProxy(); @@ -334,7 +334,7 @@ public abstract class SSTableWriter extends SSTable implements Transactional } } - protected Map finalizeMetadata() + protected final Map finalizeMetadata() { return metadataCollector.finalizeMetadata(getPartitioner().getClass().getCanonicalName(), metadata().params.bloomFilterFpChance, @@ -595,4 +595,12 @@ public abstract class SSTableWriter extends SSTable implements Transactional return new SSTableZeroCopyWriter(this, txn, owner); } } + + public void setFirst(DecoratedKey key) { + first = key; + } + + public void setLast(DecoratedKey key) { + last = key; + } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java index 46b65140c5..5322cb4cc1 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTablePartitionWriter.java @@ -43,11 +43,11 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable private final SerializationHelper helper; private final Version version; - private long previousRowStart; - private long initialPosition; + private long previousRowStartOffset; + private long partitionStartPosition; private long headerLength; - protected long startPosition; + protected long indexBlockStartOffset; protected int written; protected ClusteringPrefix firstClustering; @@ -78,9 +78,9 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable protected void reset() { - this.initialPosition = writer.position(); - this.startPosition = -1; - this.previousRowStart = 0; + this.partitionStartPosition = writer.position(); + this.indexBlockStartOffset = -1; + this.previousRowStartOffset = 0; this.written = 0; this.firstClustering = null; this.lastClustering = null; @@ -106,7 +106,7 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable if (!header.hasStatic()) { - this.headerLength = writer.position() - initialPosition; + this.headerLength = writer.position() - partitionStartPosition; state = State.AWAITING_ROWS; return; } @@ -121,7 +121,7 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable UnfilteredSerializer.serializer.serializeStaticRow(staticRow, helper, writer, version.correspondingMessagingVersion()); - this.headerLength = writer.position() - initialPosition; + this.headerLength = writer.position() - partitionStartPosition; state = State.AWAITING_ROWS; } @@ -129,21 +129,21 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable { checkState(state == State.AWAITING_ROWS); - long pos = currentPosition(); + long offset = currentOffsetInPartition(); if (firstClustering == null) { - // Beginning of an index block. Remember the start and position + // Beginning of an index block. Remember the start clustering and position firstClustering = unfiltered.clustering(); - startOpenMarker = openMarker; - startPosition = pos; + startOpenMarker = openMarker; // first entry is always LIVE (for BTI format) + indexBlockStartOffset = offset; } long unfilteredPosition = writer.position(); - unfilteredSerializer.serialize(unfiltered, helper, writer, pos - previousRowStart, version.correspondingMessagingVersion()); + unfilteredSerializer.serialize(unfiltered, helper, writer, offset - previousRowStartOffset, version.correspondingMessagingVersion()); lastClustering = unfiltered.clustering(); - previousRowStart = pos; + previousRowStartOffset = offset; ++written; if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER) @@ -159,19 +159,30 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable state = State.COMPLETED; - long endPosition = currentPosition(); + long partitionLength = currentOffsetInPartition(); unfilteredSerializer.writeEndOfPartition(writer); - return endPosition; + return partitionLength; } - protected long currentPosition() + protected long currentOffsetInPartition() { - return writer.position() - initialPosition; + return writer.position() - partitionStartPosition; } - public long getInitialPosition() + public long getPartitionStartPosition() { - return initialPosition; + return partitionStartPosition; + } + + /** Some bullshit access for now */ + public SerializationHeader getHeader() + { + return header; + } + + public SerializationHelper getHelper() + { + return helper; } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java index eb6231f9e7..50c9744856 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableVerifier.java @@ -175,7 +175,7 @@ public abstract class SortedTableVerifier imp { try { - outputHandler.debug("Deserializing bloom filter for %s", sstable); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing bloom filter for %s", sstable); deserializeBloomFilter(sstable); } catch (Throwable t) @@ -217,7 +217,7 @@ public abstract class SortedTableVerifier imp protected int verifyOwnedRanges() { List> ownedRanges = Collections.emptyList(); - outputHandler.debug("Checking that all tokens are owned by the current node"); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Checking that all tokens are owned by the current node"); try (KeyIterator iter = sstable.keyIterator()) { ownedRanges = Range.normalize(tokenLookup.apply(cfs.metadata.keyspace)); @@ -287,7 +287,7 @@ public abstract class SortedTableVerifier imp throw new CompactionInterruptedException(verifyInfo.getCompactionInfo()); long rowStart = dataFile.getFilePointer(); - outputHandler.debug("Reading row at %d", rowStart); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Reading row at %d", rowStart); DecoratedKey key = null; try @@ -332,8 +332,11 @@ public abstract class SortedTableVerifier imp long dataSize = nextRowPositionFromIndex - dataStartFromIndex; // avoid an NPE if key is null - String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey()); - outputHandler.debug("row %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSize)); + if (outputHandler.isDebugEnabled()) + { + String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey()); + outputHandler.debug("row %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSize)); + } try { @@ -352,7 +355,7 @@ public abstract class SortedTableVerifier imp prevKey = key; - outputHandler.debug("Row %s at %s valid, moving to next row at %s ", goodRows, rowStart, nextRowPositionFromIndex); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Row %s at %s valid, moving to next row at %s ", goodRows, rowStart, nextRowPositionFromIndex); dataFile.seek(nextRowPositionFromIndex); } catch (Throwable th) @@ -374,7 +377,7 @@ public abstract class SortedTableVerifier imp { try { - outputHandler.debug("Deserializing index for %s", sstable); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing index for %s", sstable); deserializeIndex(sstable); } catch (Throwable t) @@ -384,7 +387,7 @@ public abstract class SortedTableVerifier imp } } - private void deserializeIndex(SSTableReader sstable) throws IOException + protected void deserializeIndex(SSTableReader sstable) throws IOException { try (KeyReader it = sstable.keyReader()) { diff --git a/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java index 5ccaf26710..d600ee665d 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/SortedTableWriter.java @@ -79,9 +79,9 @@ public abstract class SortedTableWriter

o.startPartition(key, partitionWriter.getInitialPosition(), partitionWriter.getInitialPosition())); + notifyObservers(o -> o.startPartition(key, partitionWriter.getPartitionStartPosition(), partitionWriter.getPartitionStartPosition())); } protected void onStaticRow(Row row) @@ -331,7 +333,7 @@ public abstract class SortedTableWriter

components; - protected final IFilter bf; + public final IFilter bf; protected AbstractIndexWriter(Builder b) { diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java index 801982d5ec..fcbcc18c06 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigFormatPartitionWriter.java @@ -48,11 +48,11 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter @VisibleForTesting public static final int DEFAULT_GRANULARITY = 64 * 1024; - // used, if the row-index-entry reaches config column_index_cache_size - private DataOutputBuffer buffer; - // used to track the size of the serialized size of row-index-entry (unused for buffer) + // used, if the row-index-entry reaches config switchIndexInfoToBufferThreshold + private DataOutputBuffer rowIndexEntryBuffer; + // used to track the total serialized size of indexSamples (unused for buffer) private int indexSamplesSerializedSize; - // used, until the row-index-entry reaches config column_index_cache_size + // used, until the row-index-entry reaches switchIndexInfoToBufferThreshold (from config column_index_cache_size, or default 64k) private final List indexSamples = new ArrayList<>(); private DataOutputBuffer reusableBuffer; @@ -62,8 +62,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter private final ISerializer idxSerializer; - private final int cacheSizeThreshold; - private final int indexSize; + /** Beyond this limit we switch from storing IndexInfo in the list to directly serializing them into a buffer */ + private final int switchIndexInfoToBufferThreshold; + /** If a partition grows beyond this size we store inter-partition index data in IndexInfo */ + private final int indexBlockThreshold; BigFormatPartitionWriter(SerializationHeader header, SequentialWriter writer, @@ -82,8 +84,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter { super(header, writer, version); this.idxSerializer = indexInfoSerializer; - this.cacheSizeThreshold = cacheSizeThreshold; - this.indexSize = indexSize; + this.switchIndexInfoToBufferThreshold = cacheSizeThreshold; + this.indexBlockThreshold = indexSize; } public void reset() @@ -93,9 +95,9 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter this.indexSamplesSerializedSize = 0; this.indexSamples.clear(); - if (this.buffer != null) - this.reusableBuffer = this.buffer; - this.buffer = null; + if (this.rowIndexEntryBuffer != null) + this.reusableBuffer = this.rowIndexEntryBuffer; + this.rowIndexEntryBuffer = null; } public int getColumnIndexCount() @@ -105,12 +107,12 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter public ByteBuffer buffer() { - return buffer != null ? buffer.buffer() : null; + return rowIndexEntryBuffer != null ? rowIndexEntryBuffer.buffer() : null; } public List indexSamples() { - if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) <= cacheSizeThreshold) + if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) <= switchIndexInfoToBufferThreshold) { return indexSamples; } @@ -129,8 +131,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter { IndexInfo cIndexInfo = new IndexInfo(firstClustering, lastClustering, - startPosition, - currentPosition() - startPosition, + indexBlockStartOffset, + currentOffsetInPartition() - indexBlockStartOffset, !openMarker.isLive() ? openMarker : null); // indexOffsets is used for both shallow (ShallowIndexedEntry) and non-shallow IndexedEntry. @@ -156,8 +158,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter else { indexOffsets[columnIndexCount] = - buffer != null - ? Ints.checkedCast(buffer.position()) + rowIndexEntryBuffer != null + ? Ints.checkedCast(rowIndexEntryBuffer.position()) : indexSamplesSerializedSize; } } @@ -165,16 +167,20 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter // First, we collect the IndexInfo objects until we reach Config.column_index_cache_size in an ArrayList. // When column_index_cache_size is reached, we switch to byte-buffer mode. - if (buffer == null) + if (rowIndexEntryBuffer == null) { indexSamplesSerializedSize += idxSerializer.serializedSize(cIndexInfo); - if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) > cacheSizeThreshold) + if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.INT_SIZE > switchIndexInfoToBufferThreshold) { - buffer = reuseOrAllocateBuffer(); + rowIndexEntryBuffer = reuseOrAllocateBuffer(); + // serialize pre-existing samples for (IndexInfo indexSample : indexSamples) { - idxSerializer.serialize(indexSample, buffer); + /** {@link IndexInfo.Serializer#serialize} */ + idxSerializer.serialize(indexSample, rowIndexEntryBuffer); } + // release pre-existing samples + indexSamples.clear(); } else { @@ -182,9 +188,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter } } // don't put an else here since buffer may be allocated in preceding if block - if (buffer != null) + if (rowIndexEntryBuffer != null) { - idxSerializer.serialize(cIndexInfo, buffer); + /** {@link IndexInfo.Serializer#serialize} */ + idxSerializer.serialize(cIndexInfo, rowIndexEntryBuffer); } firstClustering = null; @@ -201,7 +208,7 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter return buffer; } // don't use the standard RECYCLER as that only recycles up to 1MB and requires proper cleanup - return new DataOutputBuffer(cacheSizeThreshold * 2); + return new DataOutputBuffer(switchIndexInfoToBufferThreshold * 2); } @Override @@ -210,7 +217,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter super.addUnfiltered(unfiltered); // if we hit the column index size that we have to index after, go ahead and index it. - if (currentPosition() - startPosition >= indexSize) + long sizeSinceLastIndexBlock = currentOffsetInPartition() - indexBlockStartOffset; + if (sizeSinceLastIndexBlock >= this.indexBlockThreshold) addIndexBlock(); } @@ -231,10 +239,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter // we have to write the offsts to these here. The offsets have already been collected // in indexOffsets[]. buffer is != null, if it exceeds Config.column_index_cache_size. // In the other case, when buffer==null, the offsets are serialized in RowIndexEntry.IndexedEntry.serialize(). - if (buffer != null) + if (rowIndexEntryBuffer != null) { for (int i = 0; i < columnIndexCount; i++) - buffer.writeInt(indexOffsets[i]); + rowIndexEntryBuffer.writeInt(indexOffsets[i]); } // we should always have at least one computed index block, but we only write it out if there is more than that. @@ -245,8 +253,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter public int indexInfoSerializedSize() { - return buffer != null - ? buffer.buffer().limit() + return rowIndexEntryBuffer != null + ? rowIndexEntryBuffer.buffer().limit() : indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0); } diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java index 8f4dd50d61..24b53ae94e 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigSSTableReaderLoadingBuilder.java @@ -178,7 +178,7 @@ public class BigSSTableReaderLoadingBuilder extends SortedTableReaderLoadingBuil checkNotNull(serializationHeader); RowIndexEntry.IndexSerializer serializer = new RowIndexEntry.Serializer(descriptor.version, serializationHeader, tableMetrics); - return BigTableKeyReader.create(indexFile, serializer); + return BigTableKeyReader.create(indexFile, serializer, false); } /** diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableKeyReader.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableKeyReader.java index 04b07af2ce..d3fba91046 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableKeyReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableKeyReader.java @@ -36,19 +36,23 @@ public class BigTableKeyReader implements KeyReader private final RandomAccessReader indexFileReader; private final IndexSerializer rowIndexEntrySerializer; private final long initialPosition; - + private final boolean detailed; private ByteBuffer key; private long dataPosition; private long keyPosition; + /** only if detailed */ + private RowIndexEntry rowIndexEntry; private BigTableKeyReader(FileHandle indexFile, RandomAccessReader indexFileReader, - IndexSerializer rowIndexEntrySerializer) + IndexSerializer rowIndexEntrySerializer, + boolean detailed) { this.indexFile = indexFile; this.indexFileReader = indexFileReader; this.rowIndexEntrySerializer = rowIndexEntrySerializer; this.initialPosition = indexFileReader.getFilePointer(); + this.detailed = detailed; } public static BigTableKeyReader create(RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException @@ -58,7 +62,7 @@ public class BigTableKeyReader implements KeyReader public static BigTableKeyReader create(FileHandle indexFile, RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException { - BigTableKeyReader iterator = new BigTableKeyReader(indexFile, indexFileReader, serializer); + BigTableKeyReader iterator = new BigTableKeyReader(indexFile, indexFileReader, serializer, false); try { iterator.advance(); @@ -72,7 +76,7 @@ public class BigTableKeyReader implements KeyReader } @SuppressWarnings({ "resource", "RedundantSuppression" }) // iFile and reader are closed in the BigTableKeyReader#close method - public static BigTableKeyReader create(FileHandle indexFile, IndexSerializer serializer) throws IOException + public static BigTableKeyReader create(FileHandle indexFile, IndexSerializer serializer, boolean detailed) throws IOException { FileHandle iFile = null; RandomAccessReader reader = null; @@ -81,7 +85,7 @@ public class BigTableKeyReader implements KeyReader { iFile = indexFile.sharedCopy(); reader = iFile.createReader(); - iterator = new BigTableKeyReader(iFile, reader, serializer); + iterator = new BigTableKeyReader(iFile, reader, serializer, detailed); iterator.advance(); return iterator; } @@ -116,7 +120,14 @@ public class BigTableKeyReader implements KeyReader { keyPosition = indexFileReader.getFilePointer(); key = ByteBufferUtil.readWithShortLength(indexFileReader); - dataPosition = rowIndexEntrySerializer.deserializePositionAndSkip(indexFileReader); + if (detailed) { + rowIndexEntry = rowIndexEntrySerializer.deserialize(indexFileReader); + dataPosition = rowIndexEntry.getPosition(); + } + else + { + dataPosition = rowIndexEntrySerializer.deserializePositionAndSkip(indexFileReader); + } return true; } else @@ -152,6 +163,15 @@ public class BigTableKeyReader implements KeyReader return dataPosition; } + public RowIndexEntry rowIndexEntry() { + assert detailed; + return rowIndexEntry; + } + + public FileHandle indexFile() { + return indexFile; + } + public long indexPosition() { return indexFileReader.getFilePointer(); diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java index d44bd7f714..2f31d4c358 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableReader.java @@ -141,9 +141,9 @@ public class BigTableReader extends SSTableReaderWithFilter implements IndexSumm } @Override - public KeyReader keyReader() throws IOException + public KeyReader keyReader(boolean detailed) throws IOException { - return BigTableKeyReader.create(ifile, rowIndexEntrySerializer); + return BigTableKeyReader.create(ifile, rowIndexEntrySerializer, detailed); } @Override diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java index 83243529c4..a275239629 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java @@ -188,6 +188,7 @@ public class BigTableScanner extends SSTableScanner implem { try { - outputHandler.debug("Deserializing index summary for %s", sstable); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing index summary for %s", sstable); deserializeIndexSummary(sstable); } catch (Throwable t) @@ -99,6 +107,74 @@ public class BigTableVerifier extends SortedTableVerifier implem super.verifyIndex(); } + @Override + protected void deserializeIndex(SSTableReader sstable) throws IOException + { + DeserializationHelper deserializationHelper = new DeserializationHelper(sstable.metadata(), + sstable.descriptor.version.correspondingMessagingVersion(), + DeserializationHelper.Flag.LOCAL); + ClusteringComparator comparator = sstable.metadata().comparator; + try (BigTableKeyReader it = (BigTableKeyReader)sstable.keyReader(true)) + { + if (it.isExhausted()) + return; + ByteBuffer key; + boolean isFirst = true; + do + { + key = it.key(); + if (outputHandler.isDebugEnabled()) outputHandler.debug("Key %s", sstable.metadata().partitionKeyType.getString(key)); + + if (isFirst && !Objects.equals(key, sstable.getFirst().getKey())) + throw new CorruptSSTableException(new IOException("First partition does not match index"), it.toString()); + else + isFirst = false; + RowIndexEntry rowIndexEntry = it.rowIndexEntry(); + if (outputHandler.isDebugEnabled()) outputHandler.debug("rowIndexEntry %s", rowIndexEntry.toString()); + + long partitionBase = it.dataPosition(); + int blockCount = rowIndexEntry.blockCount(); + if (blockCount > 0) + { + long expectedNextOffset = 0; + RowIndexEntry.IndexInfoRetriever indexInfoRetriever = rowIndexEntry.openWithIndex(it.indexFile()); + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + IndexInfo indexInfo = indexInfoRetriever.columnsIndex(blockIndex); + if (outputHandler.isDebugEnabled()) outputHandler.debug("indexInfo %s", indexInfo.toString(sstable.metadata())); + + long dataFileOffset = partitionBase + indexInfo.offset; + if (expectedNextOffset != 0) + { + if (expectedNextOffset != indexInfo.offset) + throw new CorruptSSTableException(new IOException("Row entry indexInfo offset + width should match next block offset:" + indexInfo.offset + " expected: " + expectedNextOffset), it.toString()); + } + expectedNextOffset = indexInfo.offset + indexInfo.width; + dataFile.seek(dataFileOffset); + + Unfiltered unfiltered = UnfilteredSerializer.serializer.deserialize(dataFile, + sstable.header, + deserializationHelper, + BTreeRow.sortedBuilder()); + if (!Objects.equals(indexInfo.firstName, unfiltered.clustering())) + throw new CorruptSSTableException(new IOException("Unfiltered clustering in index does not match data:{info=" + indexInfo.toString(sstable.metadata()) + ", row:" + unfiltered.clustering().toString(sstable.metadata()) + "}"), it.toString()); + + if (comparator.compare(indexInfo.firstName, indexInfo.lastName) > 0) + throw new CorruptSSTableException(new IOException("First name is > Last name:{info=" + indexInfo.toString(sstable.metadata()) + "}"), it.toString()); + + } + } + } + // The verifier checks that the partition key/position in the index and data match, here we waant to verify + // the entries clustering keys match. + while (it.advance()); // no-op, just check if index is readable + + if (!Objects.equals(key, sstable.getLast().getKey())) + throw new CorruptSSTableException(new IOException("Last partition does not match index"), it.toString()); + } + dataFile.reset(); + } + private void logDuplicates(DecoratedKey key, Row first, int duplicateRows, long minTimestamp, long maxTimestamp) { String keyString = sstable.metadata().partitionKeyType.getString(key.getKey()); diff --git a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java index 6cf01e7fbc..4f04eee100 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableWriter.java @@ -87,7 +87,7 @@ public class BigTableWriter extends SortedTableWriter o.startPartition(key, partitionWriter.getInitialPosition(), indexWriter.writer.position())); + notifyObservers(o -> o.startPartition(key, partitionWriter.getPartitionStartPosition(), indexWriter.writer.position())); } @Override @@ -97,7 +97,7 @@ public class BigTableWriter extends SortedTableWriter 1; if (size <= DatabaseDescriptor.getColumnIndexCacheSize()) { return new IndexedEntry(position, in, deletionTime, headerLength, columnsIndexCount, @@ -663,6 +667,21 @@ public class RowIndexEntry extends AbstractRowIndexEntry in.readUnsignedVInt(); } + + @Override + public String toString() + { + return "IndexedEntry{" + + "position=" + position + + ", deletionTime=" + deletionTime + + ", headerLength=" + headerLength + + ", columnsIndex=" + Arrays.toString(columnsIndex) + + ", offsets=" + Arrays.toString(offsets) + + ", indexedPartSize=" + indexedPartSize + + ", idxInfoSerializer=" + idxInfoSerializer + + ", version=" + version + + '}'; + } } /** @@ -678,6 +697,7 @@ public class RowIndexEntry extends AbstractRowIndexEntry BASE_SIZE = ObjectSizes.measure(new ShallowIndexedEntry(0, 0, DeletionTime.LIVE, 0, 10, 0, null, BigFormat.getInstance().getLatestVersion())); } + // only for cache serialization private final long indexFilePosition; private final DeletionTime deletionTime; @@ -701,8 +721,6 @@ public class RowIndexEntry extends AbstractRowIndexEntry { super(dataFilePosition); - assert columnIndexCount > 1; - this.indexFilePosition = indexFilePosition; this.headerLength = headerLength; this.deletionTime = deletionTime; @@ -810,6 +828,23 @@ public class RowIndexEntry extends AbstractRowIndexEntry in.readUnsignedVInt(); } + + @Override + public String toString() + { + return "ShallowIndexedEntry{" + + "position=" + position + + ", indexFilePosition=" + indexFilePosition + + ", deletionTime=" + deletionTime + + ", headerLength=" + headerLength + + ", columnsIndexCount=" + columnsIndexCount + + ", indexedPartSize=" + indexedPartSize + + ", offsetsOffset=" + offsetsOffset + + ", idxInfoSerializer=" + idxInfoSerializer + + ", fieldsSerializedSize=" + fieldsSerializedSize + + ", version=" + version + + '}'; + } } private static final class ShallowInfoRetriever extends FileIndexInfoRetriever @@ -900,4 +935,12 @@ public class RowIndexEntry extends AbstractRowIndexEntry super(message); } } + + @Override + public String toString() + { + return "RowIndexEntry{" + + "position=" + position + + '}'; + } } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormatPartitionWriter.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormatPartitionWriter.java index ccf69f7378..6bb024ab7e 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormatPartitionWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiFormatPartitionWriter.java @@ -79,7 +79,7 @@ class BtiFormatPartitionWriter extends SortedTablePartitionWriter super.addUnfiltered(unfiltered); // if we hit the column index size that we have to index after, go ahead and index it. - if (currentPosition() - startPosition >= rowIndexBlockSize) + if (currentOffsetInPartition() - indexBlockStartOffset >= rowIndexBlockSize) addIndexBlock(); } @@ -112,7 +112,7 @@ class BtiFormatPartitionWriter extends SortedTablePartitionWriter protected void addIndexBlock() throws IOException { - IndexInfo cIndexInfo = new IndexInfo(startPosition, startOpenMarker); + IndexInfo cIndexInfo = new IndexInfo(indexBlockStartOffset, startOpenMarker); rowTrie.add(firstClustering, lastClustering, cIndexInfo); firstClustering = null; ++rowIndexBlockCount; diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java index 791bcb9d18..1eee4515e3 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableReader.java @@ -317,7 +317,7 @@ public class BtiTableReader extends SSTableReaderWithFilter } @Override - public PartitionIterator keyReader() throws IOException + public PartitionIterator keyReader(boolean detailed) throws IOException { return PartitionIterator.create(partitionIndex, metadata().partitioner, rowIndexFile, dfile, descriptor.version); } diff --git a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java index 33dec69874..e88f7228f2 100644 --- a/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java +++ b/src/java/org/apache/cassandra/io/sstable/format/bti/BtiTableWriter.java @@ -73,7 +73,7 @@ public class BtiTableWriter extends SortedTableWriter commitLogIntervals = IntervalSet.empty(); protected final MinMaxLongTracker timestampTracker = new MinMaxLongTracker(); @@ -122,6 +124,8 @@ public class MetadataCollector implements PartitionStatisticsCollector * be a corresponding start bound that is smaller). */ private ClusteringPrefix minClustering = ClusteringBound.MAX_START; + private final ClusteringDescriptor minClusteringDescriptor; + /** * The largest clustering prefix for any {@link Unfiltered} in the sstable. * @@ -129,6 +133,7 @@ public class MetadataCollector implements PartitionStatisticsCollector * be a corresponding end bound that is bigger). */ private ClusteringPrefix maxClustering = ClusteringBound.MIN_END; + private final ClusteringDescriptor maxClusteringDescriptor; protected boolean hasLegacyCounterShards = false; private boolean hasPartitionLevelDeletions = false; @@ -159,6 +164,9 @@ public class MetadataCollector implements PartitionStatisticsCollector { this.comparator = comparator; this.originatingHostId = originatingHostId; + AbstractType[] clusteringTypes = comparator.subtypes().toArray(AbstractType[]::new); + this.minClusteringDescriptor = new ClusteringDescriptor(clusteringTypes).resetMaxStart(); + this.maxClusteringDescriptor = new ClusteringDescriptor(clusteringTypes).resetMinEnd(); } public MetadataCollector(Iterable sstables, ClusteringComparator comparator) @@ -185,6 +193,14 @@ public class MetadataCollector implements PartitionStatisticsCollector return this; } + public MetadataCollector addKey(byte[] key, int offset, int length) + { + long hashed = MurmurHash.hash2_64(key, offset, length, 0); + cardinality.offerHashed(hashed); + totalTombstones = 0; + return this; + } + public MetadataCollector addPartitionSizeInBytes(long partitionSize) { estimatedPartitionSize.add(partitionSize); @@ -236,6 +252,15 @@ public class MetadataCollector implements PartitionStatisticsCollector updateTombstoneCount(); } + /** + * Cell level stats, if we accept that LDT and LET are the same... + */ + public void updateCellLiveness(LivenessInfo newInfo) + { + ++currentPartitionCells; + update(newInfo); + } + public void updatePartitionDeletion(DeletionTime dt) { if (!dt.isLive()) @@ -299,6 +324,32 @@ public class MetadataCollector implements PartitionStatisticsCollector return this; } + public void updateClusteringValues(ClusteringDescriptor newClustering) { + // In a SSTable, every opening marker will be closed, so the start of a range tombstone marker will never be + // the maxClustering (the corresponding close might though) and there is no point in doing the comparison + // (and vice-versa for the close). By the same reasoning, a boundary will never be either the min or max + // clustering, and we can save on comparisons. + if (newClustering == null || newClustering.clusteringKind().isBoundary()) + return; + + // In case of monotonically growing stream of clusterings, we will usually require only one comparison + // because if we detected X is greater than the current MAX, then it cannot be lower than the current MIN + // at the same time. The only case when we need to update MIN when the current MAX was detected to be updated + // is the case when MIN was not yet initialized and still point the ClusteringBound.MAX_START + if (ClusteringComparator.compare(newClustering, maxClusteringDescriptor) > 0) + { + maxClusteringDescriptor.copy(newClustering); + if (minClusteringDescriptor.isMaxStart()) // min is unset + { + minClusteringDescriptor.copy(newClustering); + } + } + else if (ClusteringComparator.compare(newClustering, minClusteringDescriptor) < 0) + { + minClusteringDescriptor.copy(newClustering); + } + } + public void updateClusteringValues(Clustering clustering) { if (clustering == Clustering.STATIC_CLUSTERING) @@ -361,6 +412,13 @@ public class MetadataCollector implements PartitionStatisticsCollector Map components = new EnumMap<>(MetadataType.class); components.put(MetadataType.VALIDATION, new ValidationMetadata(partitioner, bloomFilterFPChance)); + Slice coveredClustering; + if (!minClusteringDescriptor.isMaxStart()) // min is end only if the descriptors are unused + { + minClustering = minClusteringDescriptor.toClusteringPrefix(comparator.subtypes()); + maxClustering = maxClusteringDescriptor.toClusteringPrefix(comparator.subtypes()); + } + coveredClustering = Slice.make(minClustering.retainable().asStartBound(), maxClustering.retainable().asEndBound()); components.put(MetadataType.STATS, new StatsMetadata(estimatedPartitionSize, estimatedCellPerPartitionCount, commitLogIntervals, @@ -374,7 +432,7 @@ public class MetadataCollector implements PartitionStatisticsCollector estimatedTombstoneDropTime.build(), sstableLevel, comparator.subtypes(), - Slice.make(minClustering.retainable().asStartBound(), maxClustering.retainable().asEndBound()), + coveredClustering, hasLegacyCounterShards, repairedAt, totalColumnsSet, diff --git a/src/java/org/apache/cassandra/io/util/ResizableByteBuffer.java b/src/java/org/apache/cassandra/io/util/ResizableByteBuffer.java new file mode 100644 index 0000000000..35a1e63217 --- /dev/null +++ b/src/java/org/apache/cassandra/io/util/ResizableByteBuffer.java @@ -0,0 +1,151 @@ +/* + * 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.ByteBuffer; + +import org.apache.cassandra.utils.vint.VIntCoding; +import org.jctools.util.Pow2; + +public class ResizableByteBuffer +{ + private int length = 0; + private byte[] bytes = new byte[64]; + private ByteBuffer buffer = ByteBuffer.wrap(bytes); + + /** + * Loads the length (UnsignedShort) from the data reader, and then loads the new data into the buffer. + * + * @param dataReader data source + * @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise + * @throws IOException + */ + public final boolean loadShortLength(RandomAccessReader dataReader) throws IOException + { + int newLength = dataReader.readUnsignedShort(); + return load(dataReader, newLength); + } + + /** + * Loads the new data (of the given length) into the buffer. Buffer is rewritten. + * + * @param dataReader data source + * @param newLength new data length + * @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise + * @throws IOException + */ + public final boolean load(RandomAccessReader dataReader, int newLength) throws IOException + { + boolean resize = maybeResizeForOverwrite(newLength); + dataReader.readFully(bytes, 0, newLength); + setLength(newLength); + return resize; + } + + public final void resetBuffer() { + setLength(0); + } + + protected final byte[] bytes() + { + return bytes; + } + + protected final ByteBuffer buffer() + { + return buffer; + } + + protected final int length() + { + return length; + } + + protected final void overwrite(byte[] newBytes, int newLength) + { + maybeResizeForOverwrite(newLength); + System.arraycopy(newBytes, 0, bytes, 0, newLength); + setLength(newLength); + } + + private void setLength(int newLength) + { + int currentLength = length; + if (newLength != currentLength) + { + length = newLength; + buffer.limit(newLength); + } + } + + /** + * Loads new partial data (of the given length) into the buffer. New data is appended. + * + * @param dataReader data source + * @param partLength new data length + * @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise + * @throws IOException + */ + public final boolean loadPart(RandomAccessReader dataReader, int partLength) throws IOException + { + int newLength = length + partLength; + boolean resize = maybeResizeForAppend(newLength); + dataReader.readFully(bytes, length, partLength); + setLength(newLength); + return resize; + } + + public final boolean writeUnsignedVInt(long value) + { + int currentPosition = length; + int newLength = currentPosition + 9; + boolean resize = maybeResizeForAppend(newLength); + VIntCoding.writeUnsignedVInt(value, buffer.position(currentPosition).limit(newLength)); + setLength(buffer.position()); + buffer.position(0); + return resize; + } + + private boolean maybeResizeForOverwrite(int newLength) + { + if (newLength > bytes.length) + { + bytes = new byte[Pow2.roundToPowerOfTwo(newLength)]; + buffer = ByteBuffer.wrap(bytes); + return true; + } + return false; + } + + private boolean maybeResizeForAppend(int newLength) + { + if (newLength > bytes.length) + { + int currLength = length; + byte[] currBytes = bytes; + bytes = new byte[Pow2.roundToPowerOfTwo(newLength)]; + if (currLength != 0) + System.arraycopy(currBytes,0 , bytes, 0, currLength); + buffer = ByteBuffer.wrap(bytes); + return true; + } + return false; + } +} diff --git a/src/java/org/apache/cassandra/tools/JsonTransformer.java b/src/java/org/apache/cassandra/tools/JsonTransformer.java index 9ffa6be00a..cd960cb4ca 100644 --- a/src/java/org/apache/cassandra/tools/JsonTransformer.java +++ b/src/java/org/apache/cassandra/tools/JsonTransformer.java @@ -436,7 +436,14 @@ public final class JsonTransformer else { AbstractType type = column.cellValueType(); - json.writeRawValue(type.toJSONString(clustering.get(i), clustering.accessor(), ProtocolVersion.CURRENT)); + try + { + json.writeRawValue(type.toJSONString(clustering.get(i), clustering.accessor(), ProtocolVersion.CURRENT)); + } + catch (Exception e) + { + json.writeString("unsupported conversion to JSON"); + } } } json.writeEndArray(); @@ -552,7 +559,13 @@ public final class JsonTransformer else { json.writeFieldName("value"); - json.writeRawValue(cellType.toJSONString(cell.value(), cell.accessor(), ProtocolVersion.CURRENT)); + try + { + json.writeRawValue(cellType.toJSONString(cell.value(), cell.accessor(), ProtocolVersion.CURRENT)); + } + catch (Exception e) { + json.writeString("unsupported conversion to JSON"); + } } if (liveInfo.isEmpty() || cell.timestamp() != liveInfo.timestamp()) { diff --git a/src/java/org/apache/cassandra/utils/BloomFilter.java b/src/java/org/apache/cassandra/utils/BloomFilter.java index a95d131a39..2575b4b3ad 100644 --- a/src/java/org/apache/cassandra/utils/BloomFilter.java +++ b/src/java/org/apache/cassandra/utils/BloomFilter.java @@ -101,6 +101,22 @@ public class BloomFilter extends WrappedSharedCloseable implements IFilter return indexes; } + private long[] indexes(byte[] key, int offset, int length) + { + // we use the same array both for storing the hash result, and for storing the indexes we return, + // so that we do not need to allocate two arrays. + long[] indexes = reusableIndexes.get(); + return indexes(key, offset, length, indexes); + } + + private long[] indexes(byte[] key, int offset, int length, long[] indexes) + { + MurmurHash.hash3_x64_128(key, offset, length, 0, indexes); + + setIndexes(indexes[1], indexes[0], hashCount, bitset.capacity(), indexes); + return indexes; + } + @Inline private void setIndexes(long base, long inc, int count, long max, long[] results) { @@ -121,6 +137,24 @@ public class BloomFilter extends WrappedSharedCloseable implements IFilter } } + public void add(byte[] key, int offset, int length) + { + long[] indexes = indexes(key, offset, length); + for (int i = 0; i < hashCount; i++) + { + bitset.set(indexes[i]); + } + } + + public void add(byte[] key, int offset, int length, long[] indexes) + { + indexes(key, offset, length, indexes); + for (int i = 0; i < hashCount; i++) + { + bitset.set(indexes[i]); + } + } + @Override public final boolean isPresent(FilterKey key) { diff --git a/src/java/org/apache/cassandra/utils/ByteArrayUtil.java b/src/java/org/apache/cassandra/utils/ByteArrayUtil.java index 13fb44c3bb..31f9434816 100644 --- a/src/java/org/apache/cassandra/utils/ByteArrayUtil.java +++ b/src/java/org/apache/cassandra/utils/ByteArrayUtil.java @@ -46,6 +46,11 @@ public class ByteArrayUtil return FastByteOperations.compareUnsigned(o1, off1, len, o2, off2, len); } + public static int compareUnsigned(byte[] o1, int off1, int len1, byte[] o2, int off2, int len2) + { + return FastByteOperations.compareUnsigned(o1, off1, len1, o2, off2, len2); + } + public static byte[] bytes(byte b) { return new byte[] {b}; @@ -234,6 +239,16 @@ public class ByteArrayUtil out.write(buffer); } + public static void writeWithShortLength(byte[] buffer, int offset, int length, DataOutput out) throws IOException + { + assert length <= buffer.length; + assert offset <= length; + assert length <= FBUtilities.MAX_UNSIGNED_SHORT + : String.format("Attempted serializing to buffer exceeded maximum of %s bytes: %s", FBUtilities.MAX_UNSIGNED_SHORT, length); + out.writeShort(length); + out.write(buffer, offset, length); + } + public static void writeWithVIntLength(byte[] bytes, DataOutputPlus out) throws IOException { out.writeUnsignedVInt32(bytes.length); diff --git a/src/java/org/apache/cassandra/utils/MurmurHash.java b/src/java/org/apache/cassandra/utils/MurmurHash.java index 80cf5cd39f..e5d488d663 100644 --- a/src/java/org/apache/cassandra/utils/MurmurHash.java +++ b/src/java/org/apache/cassandra/utils/MurmurHash.java @@ -93,6 +93,66 @@ public class MurmurHash return h; } + /** + * We can go all in and do a unified unsafe implementation to serve arrays/direct memory and layer the byte buffer + * types on top. Left for later. + */ + public static long hash2_64(byte[] key, int offset, int length, long seed) + { + long m64 = 0xc6a4a7935bd1e995L; + int r64 = 47; + + long h64 = (seed & 0xffffffffL) ^ (m64 * length); + + int lenLongs = length >> 3; + + for (int i = 0; i < lenLongs; ++i) + { + int i_8 = i << 3; + + long k64 = ((long) key[offset+i_8+0] & 0xff) + (((long) key[offset+i_8+1] & 0xff)<<8) + + (((long) key[offset+i_8+2] & 0xff)<<16) + (((long) key[offset+i_8+3] & 0xff)<<24) + + (((long) key[offset+i_8+4] & 0xff)<<32) + (((long) key[offset+i_8+5] & 0xff)<<40) + + (((long) key[offset+i_8+6] & 0xff)<<48) + (((long) key[offset+i_8+7] & 0xff)<<56); + + k64 *= m64; + k64 ^= k64 >>> r64; + k64 *= m64; + + h64 ^= k64; + h64 *= m64; + } + + int rem = length & 0x7; + + switch (rem) + { + case 0: + break; + case 7: + h64 ^= (long) key[offset + length - rem + 6] << 48; + case 6: + h64 ^= (long) key[offset + length - rem + 5] << 40; + case 5: + h64 ^= (long) key[offset + length - rem + 4] << 32; + case 4: + h64 ^= (long) key[offset + length - rem + 3] << 24; + case 3: + h64 ^= (long) key[offset + length - rem + 2] << 16; + case 2: + h64 ^= (long) key[offset + length - rem + 1] << 8; + case 1: + h64 ^= (long) key[offset + length - rem]; + h64 *= m64; + } + + h64 ^= h64 >>> r64; + h64 *= m64; + h64 ^= h64 >>> r64; + + return h64; + } + public static long hash2_64(ByteBuffer key, int offset, int length, long seed) { long m64 = 0xc6a4a7935bd1e995L; @@ -153,12 +213,22 @@ public class MurmurHash { int i_8 = index << 3; int blockOffset = offset + i_8; - return ((long) key.get(blockOffset + 0) & 0xff) + (((long) key.get(blockOffset + 1) & 0xff) << 8) + + return ((long) key.get(blockOffset + 0) & 0xff) + (((long) key.get(blockOffset + 1) & 0xff) << 8) + (((long) key.get(blockOffset + 2) & 0xff) << 16) + (((long) key.get(blockOffset + 3) & 0xff) << 24) + (((long) key.get(blockOffset + 4) & 0xff) << 32) + (((long) key.get(blockOffset + 5) & 0xff) << 40) + (((long) key.get(blockOffset + 6) & 0xff) << 48) + (((long) key.get(blockOffset + 7) & 0xff) << 56); } + protected static long getBlock(byte[] key, int offset, int index) + { + int i_8 = index << 3; + int blockOffset = offset + i_8; + return ((long) key[blockOffset + 0] & 0xff) + (((long) key[blockOffset + 1] & 0xff) << 8) + + (((long) key[blockOffset + 2] & 0xff) << 16) + (((long) key[blockOffset + 3] & 0xff) << 24) + + (((long) key[blockOffset + 4] & 0xff) << 32) + (((long) key[blockOffset + 5] & 0xff) << 40) + + (((long) key[blockOffset + 6] & 0xff) << 48) + (((long) key[blockOffset + 7] & 0xff) << 56); + } + protected static long rotl64(long v, int n) { return ((v << n) | (v >>> (64 - n))); @@ -251,6 +321,82 @@ public class MurmurHash result[1] = h2; } + public static void hash3_x64_128(byte[] key, int offset, int length, long seed, long[] result) + { + final int nblocks = length >> 4; // Process as 128-bit blocks. + + long h1 = seed; + long h2 = seed; + + long c1 = 0x87c37b91114253d5L; + long c2 = 0x4cf5ad432745937fL; + + //---------- + // body + + for(int i = 0; i < nblocks; i++) + { + long k1 = getBlock(key, offset, i * 2 + 0); + long k2 = getBlock(key, offset, i * 2 + 1); + + k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; + + h1 = rotl64(h1,27); h1 += h2; h1 = h1*5+0x52dce729; + + k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; + + h2 = rotl64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5; + } + + //---------- + // tail + + // Advance offset to the unprocessed tail of the data. + offset += nblocks * 16; + + long k1 = 0; + long k2 = 0; + + switch(length & 15) + { + case 15: k2 ^= ((long) key[offset+14]) << 48; + case 14: k2 ^= ((long) key[offset+13]) << 40; + case 13: k2 ^= ((long) key[offset+12]) << 32; + case 12: k2 ^= ((long) key[offset+11]) << 24; + case 11: k2 ^= ((long) key[offset+10]) << 16; + case 10: k2 ^= ((long) key[offset+9]) << 8; + case 9: k2 ^= ((long) key[offset+8]) << 0; + k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; + + case 8: k1 ^= ((long) key[offset+7]) << 56; + case 7: k1 ^= ((long) key[offset+6]) << 48; + case 6: k1 ^= ((long) key[offset+5]) << 40; + case 5: k1 ^= ((long) key[offset+4]) << 32; + case 4: k1 ^= ((long) key[offset+3]) << 24; + case 3: k1 ^= ((long) key[offset+2]) << 16; + case 2: k1 ^= ((long) key[offset+1]) << 8; + case 1: k1 ^= ((long) key[offset]); + k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= length; h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix(h1); + h2 = fmix(h2); + + h1 += h2; + h2 += h1; + + result[0] = h1; + result[1] = h2; + } + protected static long invRotl64(long v, int n) { return ((v >>> n) | (v << (64 - n))); diff --git a/src/java/org/apache/cassandra/utils/OutputHandler.java b/src/java/org/apache/cassandra/utils/OutputHandler.java index 76eb34558f..e299b86669 100644 --- a/src/java/org/apache/cassandra/utils/OutputHandler.java +++ b/src/java/org/apache/cassandra/utils/OutputHandler.java @@ -36,7 +36,7 @@ public interface OutputHandler void debug(String msg); default void debug(String msg, Object ... args) { - debug(String.format(msg, args)); + if (isDebugEnabled()) debug(String.format(msg, args)); } // called when the user needs to be warn @@ -55,6 +55,8 @@ public interface OutputHandler warn(String.format(msg, args)); } + boolean isDebugEnabled(); + class LogOutput implements OutputHandler { private static Logger logger = LoggerFactory.getLogger(LogOutput.class); @@ -66,7 +68,7 @@ public interface OutputHandler public void debug(String msg) { - logger.trace(msg); + logger.debug(msg); } public void warn(String msg) @@ -78,6 +80,18 @@ public interface OutputHandler { logger.warn(msg, th); } + + public boolean isDebugEnabled() { return logger.isDebugEnabled();} + } + + class NullOutput implements OutputHandler + { + public void output(String msg) {} + public void debug(String msg, Object ... args) {} + public void debug(String msg) {} + public void warn(String msg) {} + public void warn(Throwable th, String msg) {} + public boolean isDebugEnabled() { return false;} } class SystemOutput implements OutputHandler @@ -120,5 +134,10 @@ public interface OutputHandler if (printStack && th != null) th.printStackTrace(warnOut); } + + public boolean isDebugEnabled() + { + return debug; + } } } \ No newline at end of file diff --git a/src/java/org/apache/cassandra/utils/vint/VIntCoding.java b/src/java/org/apache/cassandra/utils/vint/VIntCoding.java index a444f4147b..4a8b77bc76 100644 --- a/src/java/org/apache/cassandra/utils/vint/VIntCoding.java +++ b/src/java/org/apache/cassandra/utils/vint/VIntCoding.java @@ -230,6 +230,7 @@ public class VIntCoding { return getUnsignedVInt(input, readerIndex, input.limit()); } + public static long getUnsignedVInt(ByteBuffer input, int readerIndex, int readerLimit) { if (readerIndex < 0) diff --git a/test/bin/jmh b/test/bin/jmh index ad8f44bd3c..53cf166de9 100755 --- a/test/bin/jmh +++ b/test/bin/jmh @@ -136,6 +136,6 @@ CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/test/conf/" CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/classes/" CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/lib/jars/*" -exec $NUMACTL "$JAVA" -cp "$CLASSPATH" org.openjdk.jmh.Main -jvmArgs="$cassandra_parms $JVM_OPTS" "$@" +exec $NUMACTL "$JAVA" -Xmx8g -XX:+UseParallelGC -cp "$CLASSPATH" org.openjdk.jmh.Main -jvmArgs="-Djmh.shutdownTimeout=120 $cassandra_parms $JVM_OPTS" "$@" # vi:ai sw=4 ts=4 tw=0 et diff --git a/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java index 5727a020b6..529c9f3d50 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/FailingRepairTest.java @@ -338,6 +338,12 @@ public class FailingRepairTest extends TestBaseImpl implements Serializable return Collections.emptySet(); } + @Override + public boolean isFullRange() + { + return false; + } + public TableMetadata metadata() { return null; diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java index d6a1fe3adf..47e044bf14 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java @@ -299,7 +299,7 @@ public abstract class AccordMigrationReadRaceTestBase extends AccordTestBase @Test public void testRangeRouting() throws Throwable { - String cql = "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(pk) > " + Murmur3Partitioner.MINIMUM.token; + String cql = "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(pk) > " + Murmur3Partitioner.MINIMUM.getLongValue(); testSplitAndRetry(cql, this::loadData, result -> { assertThat(result).isDeepEqualTo(dataFlat); }); diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java index ee89e982de..202fdb2c87 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java @@ -120,7 +120,7 @@ public class InteropTokenRangeTest extends TestBaseImpl { NavigableSet set = new TreeSet<>(); while (result.hasNext()) - set.add(Murmur3Partitioner.instance.getToken(result.next().get("pk")).token); + set.add(Murmur3Partitioner.instance.getToken(result.next().get("pk")).getLongValue()); return set; } diff --git a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java index 72da2a8d6c..2d27394ac3 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/cql3/SingleNodeTokenConflictTest.java @@ -405,10 +405,10 @@ public class SingleNodeTokenConflictTest extends StatefulASTBase for (ByteBuffer bb : values) { var token = Murmur3Partitioner.instance.getToken(bb); - if (token.token > Long.MIN_VALUE + 1) - neighbors.add(keyForToken(token.token - 1)); - if (token.token < Long.MAX_VALUE) - neighbors.add(keyForToken(token.token + 1)); + if (token.getLongValue() > Long.MIN_VALUE + 1) + neighbors.add(keyForToken(token.getLongValue() - 1)); + if (token.getLongValue() < Long.MAX_VALUE) + neighbors.add(keyForToken(token.getLongValue() + 1)); } return neighbors.build(); } diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java index 3a7059c91f..6ddb44f1be 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java @@ -1005,9 +1005,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase SinglePartitionReadCommand sprc = (SinglePartitionReadCommand) command; Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) sprc.partitionKey().getToken(); - assert node.cluster.state.get().isReadReplicaFor(requestToken.token, node.matcher) : + assert node.cluster.state.get().isReadReplicaFor(requestToken.getLongValue(), node.matcher) : String.format("Node %s is not a read replica for %s. Replicas: %s", - node, requestToken.token, node.cluster.state.get().readReplicasFor(requestToken.token)); + node, requestToken.getLongValue(), node.cluster.state.get().readReplicasFor(requestToken.getLongValue())); } @Override @@ -1048,7 +1048,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase Murmur3Partitioner.LongToken leftToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().left.getToken(); Murmur3Partitioner.LongToken rightToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().right.getToken(); - assert node.cluster.state.get().isReadReplicaFor(leftToken.token, rightToken.token, node.matcher); + assert node.cluster.state.get().isReadReplicaFor(leftToken.getLongValue(), rightToken.getLongValue(), node.matcher); } @Override @@ -1085,8 +1085,8 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase { Mutation command = request.payload; Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) command.key().getToken(); - assert node.cluster.state.get().isWriteTargetFor(requestToken.token, node.matcher) : String.format("Node %s is not a write target for %s. Write placements: %s", - node.node.idx(), requestToken, node.cluster.state.get().writePlacementsFor(requestToken.token)); + assert node.cluster.state.get().isWriteTargetFor(requestToken.getLongValue(), node.matcher) : String.format("Node %s is not a write target for %s. Write placements: %s", + node.node.idx(), requestToken, node.cluster.state.get().writePlacementsFor(requestToken.getLongValue())); } public Message respondTo(Message request) @@ -1145,7 +1145,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase public static long token(Object... pk) { - return ((Murmur3Partitioner.LongToken) DatabaseDescriptor.getPartitioner().decorateKey(toBytes(pk)).getToken()).token; + return ((Murmur3Partitioner.LongToken) DatabaseDescriptor.getPartitioner().decorateKey(toBytes(pk)).getToken()).getLongValue(); } public static ByteBuffer toBytes(Object... pk) diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java index 720d57f919..362b94fd1e 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/GossipDeadlockTest.java @@ -71,7 +71,7 @@ public class GossipDeadlockTest extends TestBaseImpl ExecutorPlus e = ExecutorFactory.Global.executorFactory().pooled("BounceMove", 2); long startToken = cluster.get(2).callOnInstance(() -> { NodeId nodeId = ClusterMetadata.current().myNodeId(); - return ((Murmur3Partitioner.LongToken)ClusterMetadata.current().tokenMap.tokens(nodeId).get(0)).token; + return ((Murmur3Partitioner.LongToken)ClusterMetadata.current().tokenMap.tokens(nodeId).get(0)).getLongValue(); }); AtomicBoolean stop = new AtomicBoolean(false); Future moves = e.submit(() -> { diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java index 44fdc7d701..f9ae0e3abe 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java @@ -555,7 +555,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase Assert.assertEquals(replication, sut.rf.asKeyspaceParams().replication); Assert.assertEquals(modelState.simulatedPlacements.nodes.stream().map(Node::token).collect(Collectors.toSet()), - actualMetadata.tokenMap.tokens().stream().map(t -> ((LongToken) t).token).collect(Collectors.toSet())); + actualMetadata.tokenMap.tokens().stream().map(t -> ((LongToken) t).getLongValue()).collect(Collectors.toSet())); for (Map.Entry e : actualMetadata.placements.asMap().entrySet()) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/streaming/LCSStreamingKeepLevelTest.java b/test/distributed/org/apache/cassandra/distributed/test/streaming/LCSStreamingKeepLevelTest.java index 4319768169..0c6d90834b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/streaming/LCSStreamingKeepLevelTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/streaming/LCSStreamingKeepLevelTest.java @@ -66,8 +66,8 @@ public class LCSStreamingKeepLevelTest extends TestBaseImpl { populate(cluster); - long tokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(3).getToken()).token; - long prevTokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(2).getToken()).token; + long tokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(3).getToken()).getLongValue(); + long prevTokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(2).getToken()).getLongValue(); // move node 4 to the middle point between its current position and the previous node long newToken = (tokenVal + prevTokenVal) / 2; cluster.get(4).nodetoolResult("move", String.valueOf(newToken)).asserts().success(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSPlacementAfterMoveTest.java b/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSPlacementAfterMoveTest.java index 99b33984e5..389e3acc6d 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSPlacementAfterMoveTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/tcm/CMSPlacementAfterMoveTest.java @@ -46,7 +46,7 @@ public class CMSPlacementAfterMoveTest extends TestBaseImpl long node1Token = cluster.get(1).callOnInstance(() -> { ClusterMetadata metadata = ClusterMetadata.current(); ImmutableList tokens = ClusterMetadata.current().tokenMap.tokens(metadata.myNodeId()); - return ((Murmur3Partitioner.LongToken) tokens.get(0)).token; + return ((Murmur3Partitioner.LongToken) tokens.get(0)).getLongValue(); }); long newNode4Token = node1Token + 100; // token after node1s token should be in cms cluster.get(4).nodetoolResult("move", String.valueOf(newNode4Token)); diff --git a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java index 952663c50e..ee8adceb60 100644 --- a/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java +++ b/test/distributed/org/apache/cassandra/io/sstable/format/ForwardingSSTableReader.java @@ -238,9 +238,9 @@ public abstract class ForwardingSSTableReader extends SSTableReader } @Override - public KeyReader keyReader() throws IOException + public KeyReader keyReader(boolean detailed) throws IOException { - return delegate.keyReader(); + return delegate.keyReader(detailed); } public KeyReader keyReader(PartitionPosition key) throws IOException diff --git a/test/distributed/org/apache/cassandra/service/accord/AccordJournalBurnTest.java b/test/distributed/org/apache/cassandra/service/accord/AccordJournalBurnTest.java index e074ed950e..2d27acbc4c 100644 --- a/test/distributed/org/apache/cassandra/service/accord/AccordJournalBurnTest.java +++ b/test/distributed/org/apache/cassandra/service/accord/AccordJournalBurnTest.java @@ -304,7 +304,7 @@ public class AccordJournalBurnTest extends BurnTestBase while (ci.hasNext()) writer.append(ci.next()); - ci.setTargetDirectory(writer.getSStableDirectory().path()); + ci.setTargetDirectory(writer.getSStableDirectoryPath()); // point of no return newSStables = writer.finish(); } diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java new file mode 100644 index 0000000000..90a86c09ef --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionTest.java @@ -0,0 +1,251 @@ +/* + * 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.harry.test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import accord.utils.Invariants; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.ThrowingRunnable; +import org.apache.cassandra.io.sstable.HarrySSTableWriter; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; + +public class HarryCompactionTest extends CQLTester +{ + private static final AtomicInteger idGen = new AtomicInteger(0); + public static final int TEST_REPS = 100; + public static final int PARTITIONS_RANGE = 100; + public static final int COLUMNS_RANGE = 10; + private final Generator staticColumnsGenerator = BitSet.generator(3); + private final Generator regularColumnsGenerator = BitSet.generator(9); + + private String keyspace; + private String table; + private String qualifiedTable; + private File dataDir; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + public void perTestSetup() throws IOException + { + keyspace = "cql_keyspace" + idGen.incrementAndGet(); + table = "table" + idGen.incrementAndGet(); + qualifiedTable = keyspace + '.' + table; + dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table); + assert dataDir.tryCreateDirectories(); + + ServerTestUtils.prepareServerNoRegister(); + StorageService.instance.initServer(); + requireNetwork(); + } + + private final Generator simple_schema = rng -> { + return new SchemaSpec(rng.next(), + 1000, + keyspace, + table, + Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), + ColumnSpec.pk("pk2", ColumnSpec.int64Type)), + Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true), + ColumnSpec.ck("ck2", ColumnSpec.int64Type, true)), + Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType), + ColumnSpec.regularColumn("r2", ColumnSpec.int64Type), + ColumnSpec.regularColumn("r3", ColumnSpec.int8Type), + ColumnSpec.regularColumn("r4", ColumnSpec.doubleType), + ColumnSpec.regularColumn("r5", ColumnSpec.floatType), + ColumnSpec.regularColumn("r6", ColumnSpec.int32Type), + ColumnSpec.regularColumn("r7", ColumnSpec.booleanType), + ColumnSpec.regularColumn("r8", ColumnSpec.int16Type), + ColumnSpec.regularColumn("r9", ColumnSpec.textType)), + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); + }; + + @Test + public void testFlushAndCompact1() throws IOException { + testFlushAndCompact(1); + } + + @Test + public void testFlushAndCompact2() throws IOException { + testFlushAndCompact(2); + } + + @Test + public void testFlushAndCompact3() throws IOException { + testFlushAndCompact(3); + } + + @Test + public void testFlushAndCompact4() throws IOException { + testFlushAndCompact(4); + } + + @Test + public void testFlushAndCompact5() throws IOException + { + testFlushAndCompact(5); + } + + public void testFlushAndCompact(int flushcount) throws IOException + { + for (int i = 0; i < TEST_REPS; i++) + testFlushAndCompactOnce(flushcount); + } + + public void testFlushAndCompactOnce(int flushcount) throws IOException + { + perTestSetup(); + withRandom( rng -> { + + SchemaSpec schema = simple_schema.generate(rng); + schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + history.customThrowing(() -> { + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + }, "disable compaction"); + + AtomicReference sstableWriter = new AtomicReference<>(); + ThrowingRunnable flushAndChangeWriter = () -> { + HarrySSTableWriter prev = sstableWriter.get(); + if (prev != null) + { + prev.close(); + StorageService.instance.bulkLoad(dataDir.absolutePath()); + dataDir.forEach(file -> file.delete()); + } + + Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder() + .forTable(schema.compile()) + .inDirectory(dataDir) + .build()) == prev); + }; + flushAndChangeWriter.run(); + + for (int sstablesFlushed = 0; sstablesFlushed < flushcount; sstablesFlushed++) + { + for (int i = 0; i < PARTITIONS_RANGE; i++) + { + for (int j = 0; j < COLUMNS_RANGE; j++) + { + history.insert(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * COLUMNS_RANGE)); // some overlap, but not all + history.deleteRow(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * COLUMNS_RANGE)); // some overlap, but not all + history.deleteColumns(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 20), + regularColumnsGenerator.generate(rng), + staticColumnsGenerator.generate(rng)); + } + history.deletePartition(rng.nextInt(0, 2 * PARTITIONS_RANGE)); // some overlap, but not all + } + + history.customThrowing(flushAndChangeWriter, "flush sstable" + sstablesFlushed); + } + + history.customThrowing(() -> { + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.forceMajorCompaction(); + }, "major compaction"); + + for (int i = 0; i < 2*PARTITIONS_RANGE; i++) + history.selectPartition(i); + + replay(schema, history, sstableWriter::get); + }); + } + + public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + CQLVisitExecutor executor = create(schema, historyBuilder, writer); + for (Visit visit : historyBuilder) + executor.execute(visit); + } + + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + DataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, tracker, + new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + statement -> { + if (logger.isTraceEnabled()) + logger.trace(statement.toString()); + return execute(statement.cql(), statement.bindings()); + }) + { + @Override + protected void executeMutatingVisit(Visit visit, CompiledStatement statement) + { + try + { + writer.get().addRow(statement.cql(), statement.bindings()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) + { + super.executeValidatingVisit(visit, selects, compiledStatement); + } + + @Override + public void execute(Visit visit) + { + if (visit.visitedPartitions.size() > 1) + throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); + + super.execute(visit); + } + }; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java new file mode 100644 index 0000000000..4855bd3de6 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/HarryCompactionWithRangeDeletionsTest.java @@ -0,0 +1,256 @@ +/* + * 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.harry.test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import accord.utils.Invariants; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.ThrowingRunnable; +import org.apache.cassandra.io.sstable.HarrySSTableWriter; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; + +public class HarryCompactionWithRangeDeletionsTest extends CQLTester +{ + private static final AtomicInteger idGen = new AtomicInteger(0); + public static final int TEST_REPS = 100; + public static final int PARTITIONS_RANGE = 100; + public static final int ROWS_RANGE = 10; + public static final int STATIC_COLS = 3; + public static final int REG_COLS = 9; + private final Generator staticColumnsGenerator = BitSet.generator(STATIC_COLS); + private final Generator regularColumnsGenerator = BitSet.generator(REG_COLS); + + private String keyspace; + private String table; + private String qualifiedTable; + private File dataDir; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + public void perTestSetup() throws IOException + { + keyspace = "cql_keyspace" + idGen.incrementAndGet(); + table = "table" + idGen.incrementAndGet(); + qualifiedTable = keyspace + '.' + table; + dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table); + assert dataDir.tryCreateDirectories(); + + ServerTestUtils.prepareServerNoRegister(); + StorageService.instance.initServer(); + requireNetwork(); + } + + private final Generator schemaSpecGenerator = rng -> { + return new SchemaSpec(rng.next(), + 1000, + keyspace, + table, + Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), + ColumnSpec.pk("pk2", ColumnSpec.int64Type)), + Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true), + ColumnSpec.ck("ck2", ColumnSpec.int64Type, true)), + Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType), + ColumnSpec.regularColumn("r2", ColumnSpec.int64Type), + ColumnSpec.regularColumn("r3", ColumnSpec.int8Type), + ColumnSpec.regularColumn("r4", ColumnSpec.doubleType), + ColumnSpec.regularColumn("r5", ColumnSpec.floatType), + ColumnSpec.regularColumn("r6", ColumnSpec.int32Type), + ColumnSpec.regularColumn("r7", ColumnSpec.booleanType), + ColumnSpec.regularColumn("r8", ColumnSpec.int16Type), + ColumnSpec.regularColumn("r9", ColumnSpec.textType)), + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); + }; + + @Test + public void testFlushAndCompact1() throws IOException { + testFlushAndCompact(1); + } + + @Test + public void testFlushAndCompact2() throws IOException { + testFlushAndCompact(2); + } + + @Test + public void testFlushAndCompact3() throws IOException { + testFlushAndCompact(3); + } + + @Test + public void testFlushAndCompact4() throws IOException { + testFlushAndCompact(4); + } + + @Test + public void testFlushAndCompact5() throws IOException + { + testFlushAndCompact(5); + } + + public void testFlushAndCompact(int flushcount) throws IOException + { + for (int i = 0; i < TEST_REPS; i++) + testFlushAndCompactOnce(flushcount); + } + + public void testFlushAndCompactOnce(int flushcount) throws IOException + { + perTestSetup(); + withRandom(205413964293041L, rng -> { + + SchemaSpec schema = schemaSpecGenerator.generate(rng); + schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + history.customThrowing(() -> { + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + }, "disable compaction"); + + AtomicReference sstableWriter = new AtomicReference<>(); + ThrowingRunnable flushAndChangeWriter = () -> { + HarrySSTableWriter prev = sstableWriter.get(); + if (prev != null) + { + prev.close(); + StorageService.instance.bulkLoad(dataDir.absolutePath()); + dataDir.forEach(file -> file.delete()); + } + + Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder() + .forTable(schema.compile()) + .inDirectory(dataDir) + .build()) == prev); + }; + flushAndChangeWriter.run(); + + for (int sstablesFlushed = 0; sstablesFlushed < flushcount; sstablesFlushed++) + { + for (int i = 0; i < PARTITIONS_RANGE; i++) + { + for (int j = 0; j < ROWS_RANGE; j++) + { + history.insert(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * ROWS_RANGE)); // some overlap, but not all + } + } + int lowerBoundRowIdx = rng.nextInt(ROWS_RANGE); + int upperBoundRowIdx = rng.nextInt(lowerBoundRowIdx, 2 * ROWS_RANGE); + history.deleteRowRange(rng.nextInt(0, 2 * PARTITIONS_RANGE), + lowerBoundRowIdx, + upperBoundRowIdx, + rng.nextInt(REG_COLS), + rng.nextBoolean(), + rng.nextBoolean()); + + history.customThrowing(flushAndChangeWriter, "flush sstable" + sstablesFlushed); + } + + history.customThrowing(() -> { + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.forceMajorCompaction(); + }, "major compaction"); + + for (int i = 0; i < 2 * PARTITIONS_RANGE; i++) + history.selectPartition(i); + + replay(schema, history, sstableWriter::get); + }); + } + + public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + CQLVisitExecutor executor = create(schema, historyBuilder, writer); + for (Visit visit : historyBuilder) + executor.execute(visit); + } + + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + DataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, tracker, + new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + statement -> { + if (logger.isTraceEnabled()) + logger.trace(statement.toString()); + return execute(statement.cql(), statement.bindings()); + }) + { + @Override + protected void executeMutatingVisit(Visit visit, CompiledStatement statement) + { + try + { + writer.get().addRow(statement.cql(), statement.bindings()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) + { + super.executeValidatingVisit(visit, selects, compiledStatement); + } + + @Override + public void execute(Visit visit) + { + if (visit.visitedPartitions.size() > 1) + throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); + + super.execute(visit); + } + }; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java b/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java new file mode 100644 index 0000000000..9b3b8e818c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/test/HarrySSTableWriterTest.java @@ -0,0 +1,190 @@ +/* + * 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.harry.test; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import accord.utils.Invariants; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.harry.ColumnSpec; +import org.apache.cassandra.harry.SchemaSpec; +import org.apache.cassandra.harry.dsl.HistoryBuilder; +import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor; +import org.apache.cassandra.harry.execution.CQLVisitExecutor; +import org.apache.cassandra.harry.execution.CompiledStatement; +import org.apache.cassandra.harry.execution.DataTracker; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.op.Operations; +import org.apache.cassandra.harry.op.Visit; +import org.apache.cassandra.harry.util.ThrowingRunnable; +import org.apache.cassandra.io.sstable.HarrySSTableWriter; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.service.StorageService; + +import static org.apache.cassandra.harry.checker.TestHelper.withRandom; + +public class HarrySSTableWriterTest extends CQLTester +{ + private static final AtomicInteger idGen = new AtomicInteger(0); + private static final int NUMBER_WRITES_IN_RUNNABLE = 10; + + private String keyspace; + private String table; + private String qualifiedTable; + private File dataDir; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + @Before + public void perTestSetup() throws IOException + { + keyspace = "cql_keyspace" + idGen.incrementAndGet(); + table = "table" + idGen.incrementAndGet(); + qualifiedTable = keyspace + '.' + table; + dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table); + assert dataDir.tryCreateDirectories(); + + ServerTestUtils.prepareServerNoRegister(); + StorageService.instance.initServer(); + requireNetwork(); + } + + private final Generator simple_schema = rng -> { + return new SchemaSpec(rng.next(), + 1000, + keyspace, + table, + Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType), + ColumnSpec.pk("pk2", ColumnSpec.int64Type)), + Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false), + ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)), + Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType), + ColumnSpec.regularColumn("r2", ColumnSpec.int64Type), + ColumnSpec.regularColumn("r3", ColumnSpec.asciiType)), + Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType), + ColumnSpec.staticColumn("s2", ColumnSpec.int64Type), + ColumnSpec.staticColumn("s3", ColumnSpec.asciiType))); + }; + + @Test + public void generateSSTableTest() + { + withRandom(rng -> { + + SchemaSpec schema = simple_schema.generate(rng); + schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace)); + createTable(schema.compile()); + + HistoryBuilder history = new HistoryBuilder(schema.valueGenerators); + for (int i = 0; i < 100; i++) + history.insert(1); + + AtomicReference sstableWriter = new AtomicReference<>(); + ThrowingRunnable resetWriter = () -> { + HarrySSTableWriter prev = sstableWriter.get(); + if (prev != null) + { + prev.close(); + StorageService.instance.bulkLoad(dataDir.absolutePath()); + } + + Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder() + .forTable(schema.compile()) + .inDirectory(dataDir) + .build()) == prev); + }; + resetWriter.run(); + + for (int i = 0; i < 100; i++) + { + for (int j = 0; j < 10; j++) + history.insert(i, j); + } + + history.customThrowing(resetWriter, "flush sstable"); + + for (int i = 0; i < 100; i++) + history.selectPartition(i); + + replay(schema, history, sstableWriter::get); + }); + } + + public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + CQLVisitExecutor executor = create(schema, historyBuilder, writer); + for (Visit visit : historyBuilder) + executor.execute(visit); + } + + public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier writer) + { + DataTracker tracker = new DataTracker.SequentialDataTracker(); + return new CQLTesterVisitExecutor(schema, tracker, + new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder), + statement -> { + if (logger.isTraceEnabled()) + logger.trace(statement.toString()); + return execute(statement.cql(), statement.bindings()); + }) + { + @Override + protected void executeMutatingVisit(Visit visit, CompiledStatement statement) + { + try + { + writer.get().addRow(statement.cql(), statement.bindings()); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + } + + @Override + protected void executeValidatingVisit(Visit visit, List selects, CompiledStatement compiledStatement) + { + super.executeValidatingVisit(visit, selects, compiledStatement); + } + + @Override + public void execute(Visit visit) + { + if (visit.visitedPartitions.size() > 1) + throw new IllegalStateException("SSTable Generator does not support batch statements and transactions"); + + super.execute(visit); + } + }; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java b/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java new file mode 100644 index 0000000000..db1bef6622 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/io/sstable/HarrySSTableWriter.java @@ -0,0 +1,656 @@ +/* + * 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.sstable; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.NavigableSet; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.cql3.ColumnSpecification; +import org.apache.cassandra.cql3.QueryOptions; +import org.apache.cassandra.cql3.QueryProcessor; +import org.apache.cassandra.cql3.UpdateParameters; +import org.apache.cassandra.cql3.functions.types.TypeCodec; +import org.apache.cassandra.cql3.statements.ModificationStatement; +import org.apache.cassandra.cql3.statements.schema.CreateIndexStatement; +import org.apache.cassandra.cql3.statements.schema.CreateTableStatement; +import org.apache.cassandra.cql3.statements.schema.CreateTypeStatement; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.Slice; +import org.apache.cassandra.db.Slices; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.SyntaxException; +import org.apache.cassandra.index.sai.StorageAttachedIndexGroup; +import org.apache.cassandra.io.sstable.format.SSTableFormat; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.KeyspaceParams; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; +import org.apache.cassandra.schema.SchemaTransformation; +import org.apache.cassandra.schema.SchemaTransformations; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.schema.Tables; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; +import org.apache.cassandra.schema.Views; +import org.apache.cassandra.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.transformations.AlterSchema; +import org.apache.cassandra.transport.ProtocolVersion; +import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.JavaDriverUtils; + +import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; + +public class HarrySSTableWriter implements Closeable +{ + public static final ByteBuffer UNSET_VALUE = ByteBufferUtil.UNSET_BYTE_BUFFER; + + static + { + CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.setBoolean(true); + DatabaseDescriptor.clientInitialization(false); + // Partitioner is not set in client mode. + if (DatabaseDescriptor.getPartitioner() == null) + DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance); + ClusterMetadataService.initializeForClients(); + } + + private final AbstractSSTableSimpleWriter writer; + + private HarrySSTableWriter(AbstractSSTableSimpleWriter writer) + { + this.writer = writer; + } + + public static Builder builder() + { + return new Builder(); + } + + public HarrySSTableWriter addRow(String cql, Object... values) throws IOException + { + ModificationStatement statement = prepare(cql); + List boundNames = statement.getBindVariables(); + // TODO: avoid materializing this + List> typeCodecs = boundNames.stream() + .map(bn -> JavaDriverUtils.codecFor(JavaDriverUtils.driverType(bn.type))) + .collect(Collectors.toList()); + + int size = Math.min(values.length, boundNames.size()); + List rawValues = new ArrayList<>(size); + for (int i = 0; i < size; i++) + { + Object value = values[i]; + rawValues.add(serialize(value, typeCodecs.get(i), boundNames.get(i))); + } + + return rawAddRow(statement, rawValues, boundNames); + } + + private ModificationStatement prepare(String cql) + { + ModificationStatement.Parsed statement = QueryProcessor.parseStatement(cql, + ModificationStatement.Parsed.class, + "INSERT/UPDATE/DELETE"); + ClientState state = ClientState.forInternalCalls(); + ModificationStatement preparedModificationStatement = statement.prepare(state); + preparedModificationStatement.validate(state); + + if (preparedModificationStatement.hasConditions()) + throw new IllegalArgumentException("Conditional statements are not supported"); + if (preparedModificationStatement.isCounter()) + throw new IllegalArgumentException("Counter modification statements are not supported"); + if (preparedModificationStatement.getBindVariables().isEmpty()) + throw new IllegalArgumentException("Provided preparedModificationStatement statement has no bind variables"); + + return preparedModificationStatement; + } + + /** + * Adds a new row to the writer given already serialized values. + *

+ * This is a shortcut for {@code rawAddRow(Arrays.asList(values))}. + * + * @param values the row values (corresponding to the bind variables of the + * modification statement used when creating by this writer) as binary. + * @return this writer. + */ + public HarrySSTableWriter rawAddRow(ModificationStatement modificationStatement, List values, List boundNames) throws InvalidRequestException, IOException + { + if (values.size() != boundNames.size()) + throw new InvalidRequestException(String.format("Invalid number of arguments, expecting %d values but got %d", boundNames.size(), values.size())); + + QueryOptions options = QueryOptions.forInternalCalls(null, values); + ClientState state = ClientState.forInternalCalls(); + List keys = modificationStatement.buildPartitionKeyNames(options, state); + + long now = currentTimeMillis(); + // Note that we asks indexes to not validate values (the last 'false' arg below) because that triggers a 'Keyspace.open' + // and that forces a lot of initialization that we don't want. + UpdateParameters params = new UpdateParameters(modificationStatement.metadata, + ClientState.forInternalCalls(), + options, + modificationStatement.getTimestamp(TimeUnit.MILLISECONDS.toMicros(now), options), + options.getNowInSec((int) TimeUnit.MILLISECONDS.toSeconds(now)), + modificationStatement.getTimeToLive(options), + Collections.emptyMap()); + + try + { + if (modificationStatement.hasSlices()) + { + Slices slices = modificationStatement.createSlices(options); + + for (ByteBuffer key : keys) + { + for (Slice slice : slices) + modificationStatement.addUpdateForKey(writer.getUpdateFor(key), slice, params); + } + } + else + { + NavigableSet> clusterings = modificationStatement.createClustering(options, state); + + for (ByteBuffer key : keys) + { + for (Clustering clustering : clusterings) + modificationStatement.addUpdateForKey(writer.getUpdateFor(key), clustering, params); + } + } + return this; + } + catch (SSTableSimpleUnsortedWriter.SyncException e) + { + // If we use a BufferedWriter and had a problem writing to disk, the IOException has been + // wrapped in a SyncException (see BufferedWriter below). We want to extract that IOE. + throw (IOException) e.getCause(); + } + } + + /** + * Close this writer. + *

+ * This method should be called, otherwise the produced sstables are not + * guaranteed to be complete (and won't be in practice). + */ + public void close() throws IOException + { + writer.close(); + } + + private ByteBuffer serialize(Object value, TypeCodec codec, ColumnSpecification columnSpecification) + { + if (value == null || value == UNSET_VALUE) + return (ByteBuffer) value; + + try + { + return codec.serialize(value, ProtocolVersion.CURRENT); + } + catch (ClassCastException cce) + { + // For backwards-compatibility with consumers that may be passing + // an Integer for a Date field, for example. + return ((AbstractType) columnSpecification.type).decompose(value); + } + } + + /** + * A Builder for a CQLSSTableWriter object. + */ + public static class Builder + { + private static final Logger logger = LoggerFactory.getLogger(Builder.class); + private static final long DEFAULT_BUFFER_SIZE_IN_MIB_FOR_UNSORTED = 128L; + + protected SSTableFormat format = null; + + private final List typeStatements; + private final List indexStatements; + + private File directory; + private CreateTableStatement.Raw schemaStatement; + private IPartitioner partitioner; + private boolean sorted = false; + private long maxSSTableSizeInMiB = -1L; + private boolean buildIndexes = true; + private Consumer> sstableProducedListener; + private boolean openSSTableOnProduced = false; + + protected Builder() + { + this.typeStatements = new ArrayList<>(); + this.indexStatements = new ArrayList<>(); + } + + /** + * The directory where to write the sstables. + *

+ * This is a mandatory option. + * + * @param directory the directory to use, which should exists and be writable. + * @return this builder. + * @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable. + */ + public Builder inDirectory(String directory) + { + return inDirectory(new File(directory)); + } + + /** + * The directory where to write the sstables (mandatory option). + *

+ * This is a mandatory option. + * + * @param directory the directory to use, which should exists and be writable. + * @return this builder. + * @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable. + */ + public Builder inDirectory(File directory) + { + if (!directory.exists()) + throw new IllegalArgumentException(directory + " doesn't exists"); + if (!directory.isWritable()) + throw new IllegalArgumentException(directory + " exists but is not writable"); + + this.directory = directory; + return this; + } + + public Builder withType(String typeDefinition) throws SyntaxException + { + typeStatements.add(QueryProcessor.parseStatement(typeDefinition, CreateTypeStatement.Raw.class, "CREATE TYPE")); + return this; + } + + /** + * The schema (CREATE TABLE statement) for the table for which sstable are to be created. + *

+ * Please note that the provided CREATE TABLE statement must use a fully-qualified + * table name, one that include the keyspace name. + *

+ * This is a mandatory option. + * + * @param schema the schema of the table for which sstables are to be created. + * @return this builder. + * @throws IllegalArgumentException if {@code schema} is not a valid CREATE TABLE statement + * or does not have a fully-qualified table name. + */ + public Builder forTable(String schema) + { + this.schemaStatement = QueryProcessor.parseStatement(schema, CreateTableStatement.Raw.class, "CREATE TABLE"); + return this; + } + + /** + * The schema (CREATE INDEX statement) for index to be created for the table. Only SAI indexes are supported. + * + * @param indexes CQL statements representing SAI indexes to be created. + * @return this builder + */ + public Builder withIndexes(String... indexes) + { + for (String index : indexes) + indexStatements.add(QueryProcessor.parseStatement(index, CreateIndexStatement.Raw.class, "CREATE INDEX")); + + return this; + } + + /** + * The partitioner to use. + *

+ * By default, {@code Murmur3Partitioner} will be used. If this is not the partitioner used + * by the cluster for which the SSTables are created, you need to use this method to + * provide the correct partitioner. + * + * @param partitioner the partitioner to use. + * @return this builder. + */ + public Builder withPartitioner(IPartitioner partitioner) + { + this.partitioner = partitioner; + return this; + } + + /** + * Defines the maximum SSTable size in mebibytes when using the sorted writer. + * By default, i.e. not specified, there is no maximum size limit for the produced SSTable + * + * @param size the maximum sizein mebibytes of each individual SSTable allowed + * @return this builder + */ + public Builder withMaxSSTableSizeInMiB(int size) + { + if (size <= 0) + { + logger.warn("A non-positive value for maximum SSTable size is specified, " + + "which disables the size limiting effectively. Please supply a positive value in order " + + "to enforce size limiting for the produced SSTables."); + } + this.maxSSTableSizeInMiB = size; + return this; + } + + /** + * The size of the buffer to use. + *

+ * This defines how much data will be buffered before being written as + * a new SSTable. This corresponds roughly to the data size that will have the created + * sstable. + *

+ * The default is 128MiB, which should be reasonable for a 1GiB heap. If you experience + * OOM while using the writer, you should lower this value. + * + * @param size the size to use in MiB. + * @return this builder. + * @deprecated This method is deprecated in favor of the new withMaxSSTableSizeInMiB(int size) + */ + @Deprecated(since = "5.0") + public Builder withBufferSizeInMiB(int size) + { + return withMaxSSTableSizeInMiB(size); + } + + /** + * The size of the buffer to use. + *

+ * This defines how much data will be buffered before being written as + * a new SSTable. This corresponds roughly to the data size that will have the created + * sstable. + *

+ * The default is 128MiB, which should be reasonable for a 1GiB heap. If you experience + * OOM while using the writer, you should lower this value. + * + * @param size the size to use in MiB. + * @return this builder. + * @deprecated This method is deprecated in favor of the new withBufferSizeInMiB(int size). See CASSANDRA-17675 + */ + @Deprecated(since = "4.1") + public Builder withBufferSizeInMB(int size) + { + return withBufferSizeInMiB(size); + } + + /** + * Creates a CQLSSTableWriter that expects sorted inputs. + *

+ * If this option is used, the resulting writer will expect rows to be + * added in SSTable sorted order (and an exception will be thrown if that + * is not the case during modification). The SSTable sorted order means that + * rows are added such that their partition key respect the partitioner + * order. + *

+ * You should thus only use this option is you know that you can provide + * the rows in order, which is rarely the case. If you can provide the + * rows in order however, using this sorted might be more efficient. + *

+ * Note that if used, some option like withBufferSizeInMiB will be ignored. + * + * @return this builder. + */ + public Builder sorted() + { + this.sorted = true; + return this; + } + + /** + * Whether indexes should be built and serialized to disk along data. Defaults to true. + * + * @param buildIndexes true if indexes should be built, false otherwise + * @return this builder + */ + public Builder withBuildIndexes(boolean buildIndexes) + { + this.buildIndexes = buildIndexes; + return this; + } + + /** + * Set the listener to receive notifications on sstable produced + *

+ * Note that if listener is registered, the sstables are opened into {@link SSTableReader}. + * The consumer is responsible for releasing the {@link SSTableReader} + * + * @param sstableProducedListener receives the produced sstables + * @return this builder + */ + public Builder withSSTableProducedListener(Consumer> sstableProducedListener) + { + this.sstableProducedListener = sstableProducedListener; + return this; + } + + /** + * Whether the produced sstable should be open or not. + * By default, the writer does not open the produced sstables + * + * @return this builder + */ + public Builder openSSTableOnProduced() + { + this.openSSTableOnProduced = true; + return this; + } + + public HarrySSTableWriter build() + { + if (directory == null) + throw new IllegalStateException("No ouptut directory specified, you should provide a directory with inDirectory()"); + if (schemaStatement == null) + throw new IllegalStateException("Missing schema, you should provide the schema for the SSTable to create with forTable()"); + + Preconditions.checkState(Sets.difference(SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES, Schema.instance.getKeyspaces()).isEmpty(), + "Local keyspaces were not loaded. If this is running as a client, please make sure to add %s=true system property.", + CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.getKey()); + + // Assign the default max SSTable size if not defined in builder + if (isMaxSSTableSizeUnset()) + { + maxSSTableSizeInMiB = sorted ? -1L : DEFAULT_BUFFER_SIZE_IN_MIB_FOR_UNSORTED; + } + + synchronized (HarrySSTableWriter.class) + { + String keyspaceName = schemaStatement.keyspace(); + String tableName = schemaStatement.table(); + + Schema.instance.submit(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(keyspaceName, + KeyspaceParams.simple(1), + Tables.none(), + Views.none(), + Types.none(), + UserFunctions.none()), true)); + + KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspaceName, + KeyspaceParams.simple(1), + Tables.none(), + Views.none(), + Types.none(), + UserFunctions.none()); + + TableMetadata tableMetadata = Schema.instance.getTableMetadata(keyspaceName, tableName); + if (tableMetadata == null) + { + Types types = createTypes(keyspaceName); + Schema.instance.submit(SchemaTransformations.addTypes(types, true)); + tableMetadata = createTable(types, ksm.userFunctions); + Schema.instance.submit(SchemaTransformations.addTable(tableMetadata, true)); + + if (buildIndexes && !indexStatements.isEmpty()) + { + // we need to commit keyspace metadata first so applyIndexes sees that keyspace from TCM + commitKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(tableMetadata))); + applyIndexes(keyspaceName); + } + + KeyspaceMetadata keyspaceMetadata = ClusterMetadata.current().schema.getKeyspaceMetadata(keyspaceName); + tableMetadata = keyspaceMetadata.tables.getNullable(tableName); + + Schema.instance.submit(SchemaTransformations.addTable(tableMetadata, true)); + } + + KeyspaceMetadata keyspaceMetadata = ClusterMetadata.current().schema.getKeyspaceMetadata(keyspaceName); + Keyspace keyspace = Keyspace.mockKS(keyspaceMetadata); + Directories directories = new Directories(tableMetadata, Collections.singleton(new Directories.DataDirectory(new File(directory.toPath())))); + ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(keyspace, + tableName, + tableMetadata, + directories, + false, + false); + + keyspace.initCfCustom(cfs); + + // this is the empty directory / leftover from times we initialized ColumnFamilyStore + // it will automatically create directories for keyspace and table on disk after initialization + // we set that directory to the destination of generated SSTables so we just remove empty directories here + try + { + new File(directory, keyspaceName).deleteRecursive(); + } + catch (UncheckedIOException ex) + { + if (!(ex.getCause() instanceof NoSuchFileException)) + { + throw ex; + } + } + + TableMetadataRef ref = tableMetadata.ref; + AbstractSSTableSimpleWriter writer = sorted + ? new SSTableSimpleWriter(directory, ref, cfs.metadata.get().regularAndStaticColumns(), maxSSTableSizeInMiB) + : new SSTableSimpleUnsortedWriter(directory, ref, cfs.metadata.get().regularAndStaticColumns(), maxSSTableSizeInMiB); + + if (format != null) + writer.setSSTableFormatType(format); + + if (buildIndexes && !indexStatements.isEmpty() && cfs != null) + { + StorageAttachedIndexGroup saiGroup = StorageAttachedIndexGroup.getIndexGroup(cfs); + if (saiGroup != null) + writer.addIndexGroup(saiGroup); + } + + if (sstableProducedListener != null) + writer.setSSTableProducedListener(sstableProducedListener); + + writer.setShouldOpenProducedSSTable(openSSTableOnProduced); + + return new HarrySSTableWriter(writer); + } + } + + private boolean isMaxSSTableSizeUnset() + { + return maxSSTableSizeInMiB <= 0; + } + + private Types createTypes(String keyspace) + { + Types.RawBuilder builder = Types.rawBuilder(keyspace); + for (CreateTypeStatement.Raw st : typeStatements) + st.addToRawBuilder(builder); + return builder.build(); + } + + /** + * Applies any provided index definitions to the target table + * + * @param keyspaceName name of the keyspace to apply indexes for + * @return table metadata reflecting applied indexes + */ + private void applyIndexes(String keyspaceName) + { + ClientState state = ClientState.forInternalCalls(); + + for (CreateIndexStatement.Raw statement : indexStatements) + { + Keyspaces keyspaces = statement.prepare(state).apply(ClusterMetadata.current()); + commitKeyspaceMetadata(keyspaces.getNullable(keyspaceName)); + } + } + + private void commitKeyspaceMetadata(KeyspaceMetadata keyspaceMetadata) + { + SchemaTransformation schemaTransformation = new SchemaTransformation() + { + @Override + public Keyspaces apply(ClusterMetadata metadata) + { + return metadata.schema.getKeyspaces().withAddedOrUpdated(keyspaceMetadata); + } + + @Override + public boolean compatibleWith(ClusterMetadata metadata) + { + return true; + } + }; + ClusterMetadataService.instance().commit(new AlterSchema(schemaTransformation)); + } + + /** + * Creates the table according to schema statement + * + * @param types types this table should be created with + */ + private TableMetadata createTable(Types types, UserFunctions functions) + { + ClientState state = ClientState.forInternalCalls(); + CreateTableStatement statement = schemaStatement.prepare(state); + statement.validate(ClientState.forInternalCalls()); + + TableMetadata.Builder builder = statement.builder(types, functions); + if (partitioner != null) + builder.partitioner(partitioner); + + return builder.build(); + } + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java b/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java index edbe249b2e..8205fd02b3 100644 --- a/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java +++ b/test/microbench/org/apache/cassandra/test/microbench/CompactionBench.java @@ -23,64 +23,87 @@ import java.io.IOException; import java.util.List; import java.util.concurrent.*; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.service.snapshot.SnapshotManager; +import org.apache.cassandra.tcm.ClusterMetadataService; import org.openjdk.jmh.annotations.*; @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) @Threads(1) @State(Scope.Benchmark) public class CompactionBench extends CQLTester { - static String keyspace; - String table; - String writeStatement; - String readStatement; - ColumnFamilyStore cfs; - List snapshotFiles; - List liveFiles; + protected static String keyspace; + protected String table; + protected String writeStatement; + protected ColumnFamilyStore cfs; + protected List snapshotFiles; + + @Param("2") + protected int sstableCount = 2; + + @Param("50000") + protected int rowCount = 50000; + + @Param("NONE") + protected String overlap = "NONE"; + + @Param("true") + protected boolean isCursor = true; @Setup(Level.Trial) public void setup() throws Throwable { CQLTester.prepareServer(); + DatabaseDescriptor.setCursorCompactionEnabled(isCursor); + DatabaseDescriptor.setCompactionThroughputMebibytesPerSec(10*1024); // no rate limiting + createSStables(); + takeSnapshot(); + } + + protected void createSStables() + { keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, commentid bigint, PRIMARY KEY(userid, picid))"); execute("use "+keyspace+";"); writeStatement = "INSERT INTO "+table+"(userid,picid,commentid)VALUES(?,?,?)"; - readStatement = "SELECT * from "+table+" limit 100"; Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); cfs.disableAutoCompaction(); - //Warm up - System.err.println("Writing 50k"); - for (long i = 0; i < 50000; i++) - execute(writeStatement, i, i, i ); + for (int j = 0; j < sstableCount; j++) + { + int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount; + int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount; + for (long i = 0; i < rowCount; i++) + { + execute(writeStatement, (pPrefix + i), (rPrefix + i), j * rowCount + i); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + } - cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); - - System.err.println("Writing 50k again..."); - for (long i = 0; i < 50000; i++) - execute(writeStatement, i, i, i ); - - cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); - + private void takeSnapshot() + { SnapshotManager.instance.takeSnapshot("originals", cfs.getKeyspaceTableName()); snapshotFiles = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).snapshots("originals").listFiles(); + long[] sum = new long[1]; + snapshotFiles.forEach(f -> sum[0] += f.length()); + System.out.println("Total input size: " + sum[0]); } @TearDown(Level.Trial) @@ -95,28 +118,36 @@ public class CompactionBench extends CQLTester System.err.println("Thread "+t.getName()); } + CommitLog.instance.shutdownBlocking(); + ClusterMetadataService.instance().log().close(); + CQLTester.tearDownClass(); CQLTester.cleanup(); } @TearDown(Level.Invocation) - public void resetSnapshot() + public void resetSnapshot() throws IOException, InterruptedException { cfs.truncateBlocking(); List directories = cfs.getDirectories().getCFDirectories(); - - for (File file : directories) + // Sometimes deletes are unreliable... + int deleted = 0; + do { - for (File f : file.tryList()) + deleted = 0; + for (File file : directories) { - if (f.isDirectory()) - continue; - - FileUtils.delete(f); + for (File f : file.tryList()) + { + if (f.isDirectory()) + continue; + f.tryDelete(); + deleted++; + } } - } - + Thread.sleep(10); + } while (deleted != 0); for (File file : snapshotFiles) FileUtils.createHardLink(file, new File(new File(file.toPath().getParent().getParent().getParent()), file.name())); diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionLargeCellBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionLargeCellBench.java new file mode 100644 index 0000000000..0a17d7ae95 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionLargeCellBench.java @@ -0,0 +1,78 @@ +/* + * 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.test.microbench.sstable; + + +import java.nio.ByteBuffer; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.test.microbench.CompactionBench; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@Threads(1) +@State(Scope.Benchmark) +public class CompactionLargeCellBench extends CompactionBench +{ + @Param("128") + int blobSize = 128; + + protected void createSStables() + { + keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, b blob, PRIMARY KEY(userid, picid))"); + execute("use "+keyspace+";"); + writeStatement = "INSERT INTO "+table+"(userid,picid,b)VALUES(?,?,?)"; + + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + byte[] blob = new byte[blobSize]; + for (int j = 0; j < sstableCount; j++) + { + int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount; + int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount; + for (long i = 0; i < rowCount; i++) + { + ThreadLocalRandom.current().nextBytes(blob); + execute(writeStatement, (pPrefix + i), (rPrefix + i), ByteBuffer.wrap(blob)); + } + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionStringsBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionStringsBench.java new file mode 100644 index 0000000000..ee1af71e4d --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionStringsBench.java @@ -0,0 +1,92 @@ +/* + * 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.test.microbench.sstable; + + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.test.microbench.CompactionBench; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@Threads(1) +@State(Scope.Benchmark) +public class CompactionStringsBench extends CompactionBench +{ + @Param("128") + int stringSizeMin = 128; + @Param("256") + int stringSizeMax = 256; + + protected void createSStables() + { + keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + table = createTable(keyspace, "CREATE TABLE %s ( pk ascii, ck ascii, c ascii, PRIMARY KEY(pk, ck))"); + execute("use "+keyspace+";"); + writeStatement = "INSERT INTO "+table+"(pk,ck,c)VALUES(?,?,?)"; + + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + for (int j = 0; j < sstableCount; j++) + { + Random r = new Random(42L); + int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount; + int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount; + for (long i = 0; i < rowCount; i++) + { + execute(writeStatement, (pPrefix + i) + getNextString(r), (rPrefix + i) + getNextString(r), getNextString(r)); + } + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + } + + private String getNextString(Random r) + { + int rangeDelta = stringSizeMax - stringSizeMin; + int rangeRandom = rangeDelta > 0 ? r.nextInt(rangeDelta) : 0; + byte[] blob = new byte[stringSizeMin + rangeRandom]; + r.nextBytes(blob); + for (int i = 0; i < blob.length; i++) + { + if (blob[i] < 0) + blob[i] = 0; + } + String nextString = new String(blob); + return nextString; + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionWideRowBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionWideRowBench.java new file mode 100644 index 0000000000..3b0e0745e8 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/CompactionWideRowBench.java @@ -0,0 +1,104 @@ +/* + * 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.test.microbench.sstable; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.test.microbench.CompactionBench; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1) +@Threads(1) +@State(Scope.Benchmark) +public class CompactionWideRowBench extends CompactionBench +{ + @Param("1") + int rowPerPkCount = 1; + + @Param("1") + int ckCount = 1; + + @Param("1") + int colCount = 1; + + protected void createSStables() + { + keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String tableCreate = "CREATE TABLE %s ( userid bigint"; + for (int i=0;i k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + int pkCount = rowCount/rowPerPkCount; + for (int j = 0; j < sstableCount; j++) + { + int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount; + int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount; + for (long pkIndex = 0; pkIndex < pkCount; pkIndex++) + { + for (long rowIndex = 0; rowIndex < rowPerPkCount; rowIndex++) + { + values[0] = (pPrefix + pkIndex); + Arrays.fill(values, 1, ckCount + 1, (rPrefix + rowIndex)); + Arrays.fill(values, 1 + ckCount, values.length, j * rowCount + pkIndex); + execute(writeStatement, values); + } + } + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractBench.java new file mode 100644 index 0000000000..836394deb6 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractBench.java @@ -0,0 +1,111 @@ +/* + * 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.test.microbench.sstable; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(value = 1) +@State(Scope.Benchmark) +public class SSTableAbstractBench extends CQLTester +{ + ColumnFamilyStore cfs; + String keyspace; + + @Param("50000") + int rowCount = 50000; + private String table; + + // TODO: elaborate data setup with multiple schemas + @Setup(Level.Trial) + public void setup() throws Throwable + { + prepareServer(); + beforeTest(); + + setupTable(); + setupData(); + } + + protected void setupTable() + { + keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid1 bigint, picid2 bigint, commentid bigint, " + + "PRIMARY KEY(userid, picid1, picid2))"); + execute("use "+keyspace+";"); + + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + } + + protected void setupData() + { + String writeStatement = "INSERT INTO " + table + "(userid,picid1,picid2,commentid)VALUES(?,?,?,?)"; + for (long i = 0; i < rowCount; i++) + insertForIndex(writeStatement, i); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + + protected UntypedResultSet insertForIndex(String writeStatement, long i) + { + return execute(writeStatement, i, i, i, i); + } + + @TearDown(Level.Trial) + public void teardown() throws IOException, ExecutionException, InterruptedException + { + CommitLog.instance.shutdownBlocking(); + ClusterMetadataService.instance().log().close(); + CQLTester.tearDownClass(); + CQLTester.cleanup(); + } + + public SSTableReader getReader() throws IOException + { + return cfs.getLiveSSTables().stream().filter(s -> s.getKeyspaceName().equals(keyspace)).findFirst().orElse(null); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractPipeBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractPipeBench.java new file mode 100644 index 0000000000..98c6cbb19a --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableAbstractPipeBench.java @@ -0,0 +1,56 @@ +/* + * 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.test.microbench.sstable; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import org.apache.cassandra.db.Directories; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.service.snapshot.SnapshotManager; +import org.apache.cassandra.tools.Util; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + + +@State(Scope.Benchmark) +public class SSTableAbstractPipeBench extends SSTableAbstractBench +{ + Descriptor desc; + TableMetadata metadata; + File tmpDir; + List snapshotFiles; + + @Setup(Level.Trial) + public void setupSnapshots() throws Throwable + { + SnapshotManager.instance.takeSnapshot("originals", cfs.getKeyspaceTableName()); + + snapshotFiles = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).snapshots("originals").listFiles(); + + desc = Descriptor.fromFileWithComponent(snapshotFiles.get(0), false).left; + metadata = Util.metadataFromSSTable(desc); + tmpDir = Files.createTempDirectory("sstable-copy").toFile(); + System.err.println("Writing to : " + tmpDir); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java new file mode 100644 index 0000000000..a2b8b5661b --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableCursorPipeUtil.java @@ -0,0 +1,150 @@ +/* + * 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.test.microbench.sstable; + +import java.io.IOException; + +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.io.sstable.PartitionDescriptor; +import org.apache.cassandra.db.ReusableLivenessInfo; +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.io.sstable.SSTableCursorWriter; +import org.apache.cassandra.io.sstable.UnfilteredDescriptor; + +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.*; + +public class SSTableCursorPipeUtil +{ + + public static void copySSTable(SSTableCursorReader reader, SSTableCursorWriter writer) throws Throwable + { + PartitionDescriptor pHeader = new PartitionDescriptor(reader.ssTableReader().getPartitioner().createReusableKey(0)); + UnfilteredDescriptor unfilteredDescriptor = new UnfilteredDescriptor(reader.ssTableReader().header.clusteringTypes().toArray(AbstractType[]::new)); + int readerState = PARTITION_START; + boolean first = true; + + while (readerState != DONE) + { + readerState = reader.readPartitionHeader(pHeader); + if (first) + { + first = false; + writer.setFirst(pHeader.keyBuffer()); + } + readerState = copyPartition(reader, writer, pHeader, unfilteredDescriptor, readerState); + } + writer.setLast(pHeader.keyBuffer()); + } + + public static int copyPartition(SSTableCursorReader reader, SSTableCursorWriter writer, PartitionDescriptor pHeader, UnfilteredDescriptor unfilteredDescriptor, int readerState) throws IOException + { + if (readerState != STATIC_ROW_START && + readerState != ROW_START && + readerState != TOMBSTONE_START && + readerState != PARTITION_END) + throw new IllegalStateException(); + + final byte[] keyBytes = pHeader.keyBytes(); + final int keyLength = pHeader.keyLength(); + final DeletionTime pDeletionTime = pHeader.deletionTime(); + + int headerLength = writer.writePartitionStart(keyBytes, keyLength, pDeletionTime); + int unfilteredCounter = 0; + while (readerState != PARTITION_END) + { + switch (readerState) + { + case STATIC_ROW_START: + readerState = copyStaticRow(reader, writer, unfilteredDescriptor); + headerLength = (int) (writer.getPosition() - writer.getPartitionStart()); + break; + case ROW_START: + readerState = copyRow(reader, writer, unfilteredDescriptor, unfilteredCounter++); + break; + case TOMBSTONE_START: + readerState = copyRangeTombstone(reader, writer, unfilteredDescriptor, unfilteredCounter++); + } + } + writer.writePartitionEnd(keyBytes, keyLength, pDeletionTime, headerLength); + if (unfilteredCounter > 1) { + writer.updateClusteringMetadata(unfilteredDescriptor); + } + return reader.continueReading(); + } + + public static int copyStaticRow(SSTableCursorReader reader, SSTableCursorWriter writer, UnfilteredDescriptor unfilteredDescriptor) throws IOException + { + int readerState = reader.readStaticRowHeader(unfilteredDescriptor); + return copyRowAfterDescriptor(reader, writer, unfilteredDescriptor, readerState, true, false); + } + + public static int copyRow(SSTableCursorReader reader, SSTableCursorWriter writer, UnfilteredDescriptor unfilteredDescriptor, int unfilteredIndex) throws IOException + { + int readerState = reader.readRowHeader(unfilteredDescriptor); + return copyRowAfterDescriptor(reader, writer, unfilteredDescriptor, readerState, false, unfilteredIndex == 0); + } + + public static int copyRangeTombstone(SSTableCursorReader reader, SSTableCursorWriter writer, UnfilteredDescriptor unfilteredDescriptor, int unfilteredIndex) throws IOException + { + int readerState = reader.readTombstoneMarker(unfilteredDescriptor); + writer.writeRangeTombstone(unfilteredDescriptor, unfilteredIndex == 0); + return readerState; + } + + private final static byte[] copyColumnValueBuffer = new byte[4096]; // used to copy cell contents (maybe piecemeal if very large, since we don't have a direct read option) + + public static int copyRowAfterDescriptor(SSTableCursorReader reader, SSTableCursorWriter writer, UnfilteredDescriptor unfilteredDescriptor, int readerState, boolean isStatic, boolean updateClusteringMetadata) throws IOException + { + writer.writeRowStart(unfilteredDescriptor.livenessInfo(), unfilteredDescriptor.deletionTime(), isStatic); + + // Copy cells + while (readerState != UNFILTERED_END) + { + if (readerState != CELL_HEADER_START) + throw new IllegalStateException("Unexpected reader state: " + readerState); + readerState = reader.readCellHeader(); + SSTableCursorReader.CellCursor cellCursor = reader.cellCursor(); + + /** + * {@link Cell.Serializer#serialize} + */ + int cellFlags = cellCursor.cellFlags; + ReusableLivenessInfo cellLiveness = cellCursor.cellLiveness; + writer.writeCellHeader(cellFlags, cellLiveness, cellCursor.cellColumn); + if (readerState == CELL_VALUE_START) + { + readerState = writer.writeCellValue(reader, copyColumnValueBuffer); + } + else if (Cell.Serializer.hasValue(cellFlags)) + { + throw new IllegalStateException("Flags and state contradict"); + } + if (readerState != CELL_END) + throw new IllegalStateException("Expect CELL_END after cell read. State: " + readerState); + + readerState = reader.continueReading(); + } + + writer.writeRowEnd(unfilteredDescriptor, updateClusteringMetadata); + + return reader.continueReading(); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeBench.java new file mode 100644 index 0000000000..e69900007e --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeBench.java @@ -0,0 +1,67 @@ +/* + * 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.test.microbench.sstable; + +import java.io.File; +import java.util.stream.Stream; + +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.SSTableWriter; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.tools.Util; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + + +@State(Scope.Benchmark) +public class SSTablePipeBench extends SSTableAbstractPipeBench +{ + @TearDown(Level.Invocation) + public void closeReaderAndDeleteOutput() + { + for (File file : tmpDir.listFiles()) + { + file.delete(); + } + } + + @Benchmark + public void readAndWrite() throws Throwable + { + SSTableReader ssTableReader = SSTableReader.openNoValidation(null, desc, TableMetadataRef.forOfflineTools(metadata)); + try (SSTableWriter ssTableWriter = CompactionManager.createWriter(cfs, new org.apache.cassandra.io.util.File(tmpDir), -1, -1, null, false, ssTableReader, LifecycleTransaction.offline(OperationType.COMPACTION));) + { + final ISSTableScanner currentScanner = ssTableReader.getScanner(); + Stream partitions = Util.iterToStream(currentScanner); + partitions.forEach(unfilteredRowIterator -> { + ssTableWriter.append(unfilteredRowIterator); + }); + ssTableWriter.finish(false); + } + ssTableReader.ref().close(); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeCursorBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeCursorBench.java new file mode 100644 index 0000000000..83fc957ac8 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTablePipeCursorBench.java @@ -0,0 +1,57 @@ +/* + * 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.test.microbench.sstable; + +import java.io.File; + +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.io.sstable.SSTableCursorWriter; +import org.apache.cassandra.db.compaction.CompactionManager; +import org.apache.cassandra.db.compaction.OperationType; +import org.apache.cassandra.db.lifecycle.LifecycleTransaction; +import org.apache.cassandra.io.sstable.format.SortedTableWriter; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + + +@State(Scope.Benchmark) +public class SSTablePipeCursorBench extends SSTableAbstractPipeBench +{ + @TearDown(Level.Invocation) + public void closeReaderAndDeleteOutput() throws Exception + { + for (File file : tmpDir.listFiles()) + { + file.delete(); + } + } + + @Benchmark + public void readAndWrite() throws Throwable + { + try(SSTableCursorReader cursorReader = SSTableCursorReader.fromDescriptor(desc); + SortedTableWriter ssTableWriter = (SortedTableWriter) CompactionManager.createWriter(cfs, new org.apache.cassandra.io.util.File(tmpDir), 0, 0, null, false, cursorReader.ssTableReader(), LifecycleTransaction.offline(OperationType.COMPACTION)); + SSTableCursorWriter cursorWriter = new SSTableCursorWriter(ssTableWriter);){ + SSTableCursorPipeUtil.copySSTable(cursorReader, cursorWriter); + } + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableRawVisitorBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableRawVisitorBench.java new file mode 100644 index 0000000000..5d4db56cc5 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableRawVisitorBench.java @@ -0,0 +1,310 @@ +/* + * 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.test.microbench.sstable; + +import java.io.IOException; + +import com.google.common.collect.ImmutableList; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.rows.UnfilteredSerializer; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.sstable.format.Version; +import org.apache.cassandra.io.util.RandomAccessReader; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.utils.vint.VIntCoding; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Benchmark) +public class SSTableRawVisitorBench extends SSTableAbstractBench +{ + private Version version; + private TableMetadata metadata; + private ImmutableList clusteringColumns; + private int clusteringColumnCount; + private AbstractType[] clusteringColumnTypes; + private boolean hasUIntDeletionTime; + + private SSTableReader ssTableReader; + + @Setup(Level.Invocation) + public void prepareReader() throws IOException + { + ssTableReader = getReader(); + TableMetadata metadata = ssTableReader.metadata(); + version = ssTableReader.descriptor.version; + hasUIntDeletionTime = version.hasUIntDeletionTime(); + clusteringColumns = metadata.clusteringColumns(); + clusteringColumnCount = clusteringColumns.size(); + clusteringColumnTypes = new AbstractType[clusteringColumnCount]; + for (int i = 0; i< clusteringColumnTypes.length; i++) { + clusteringColumnTypes[i] = clusteringColumns.get(i).type; + } + } + + @TearDown(Level.Invocation) + public void closeReader() { + ssTableReader.ref().close(); + } + + long[] counters = new long[4]; + @Benchmark + public void countPartitionsAndUnfiltered() throws IOException + { + for (int i = 0; i < counters.length; i++) + { + counters[i] = 0; + } + try (RandomAccessReader randomAccessReader = ssTableReader.openDataReader()) { + long length = randomAccessReader.length(); + long nextPartition = 0; + do + { + nextPartition = readPartition(randomAccessReader, nextPartition, counters); + counters[0]++; + } while (!randomAccessReader.isEOF() && nextPartition < length); + } + } + + + // struct partition { + // struct partition_header header + // optional row + // struct unfiltered unfiltereds[]; + //}; + private long readPartition(RandomAccessReader randomAccessReader, long nextPartition, long[] counters) throws IOException + { + int cursor = (int) nextPartition; + int headerPosition = cursor; + // struct partition_header header { + // be16 key_length; e.g. 8 if long + // byte key[key_length]; + // struct deletion_time deletion_time { + // be32 local_deletion_time; + // be64 marked_for_delete_at; + // }; + // }; + int keyLength = randomAccessReader.readUnsignedShort(); + // TODO: print key according to metadata (need the type for formatting) + int keyPosition = (cursor += 2); + randomAccessReader.skipBytes(keyLength); + cursor += keyLength; + + // PARTITION DELETION TIME + int deletionTimePosition = cursor; + int deletionTimeSize = 12; + if (hasUIntDeletionTime) { + byte flags = randomAccessReader.readByte(); + if ((IS_LIVE_DELETION & flags) != 0) { + deletionTimeSize = 1; + // no delete times + } + else { + long position = randomAccessReader.getPosition(); + randomAccessReader.seek(position - 1); + long markedForDeleteAt = randomAccessReader.readLong(); + int localDeletionTime = randomAccessReader.readInt(); + } + } + else + { + int localDeletionTime = randomAccessReader.readInt(); + long markedForDeleteAt = randomAccessReader.readLong(); + } + // read the rows until END_OF_PARTITION + int nextUnfilteredPosition = (cursor += deletionTimeSize); + byte nextUnfilteredFlags = randomAccessReader.readByte(); + while (!UnfilteredSerializer.isEndOfPartition(nextUnfilteredFlags)) { + nextUnfilteredPosition = readUnfiltered(randomAccessReader, nextUnfilteredFlags, nextUnfilteredPosition, counters); + nextUnfilteredFlags = randomAccessReader.readByte(); + } + return nextUnfilteredPosition + 1; + } + // struct row { + // byte flags; + // optional extended_flags; // only present for static rows + // optional clustering_blocks { + // varint clustering_block_header; + // simple_cell[] clustering_cells; + // }; // only present for non-static rows + // varint row_body_size; + // varint prev_unfiltered_size; // for backward traversing + // optional liveness_info; + // optional deletion_time; + // optional missing_columns; + // cell[] cells; + // }; // Has IS_STATIC flag set + private int readUnfiltered(RandomAccessReader randomAccessReader, byte flags, final int unfilteredStartPosition, long[] counters) throws IOException + { + if (UnfilteredSerializer.isEndOfPartition(flags)) throw new IllegalStateException(); + + int cursor = unfilteredStartPosition + 1; + boolean isRow = UnfilteredSerializer.isRow(flags); + boolean isTombstoneMarker = UnfilteredSerializer.isTombstoneMarker(flags); + boolean isStatic = false; + boolean deletionIsShadowable = false; + if (UnfilteredSerializer.isExtended(flags)) { + byte extendedFlags = randomAccessReader.readByte(); cursor++; + + isStatic = UnfilteredSerializer.isStatic(extendedFlags); + deletionIsShadowable = UnfilteredSerializer.deletionIsShadowable(extendedFlags); + + } + if ((isStatic && !isRow) || (isStatic && isTombstoneMarker)) throw new IllegalStateException(); + + if (isStatic) { // this should only apply to first row read + // static row + long rowSize = randomAccessReader.readUnsignedVInt(); + randomAccessReader.skipBytes((int)rowSize); + + cursor += VIntCoding.computeUnsignedVIntSize(rowSize) + rowSize; + // TODO: handle row contents + + counters[1]++; + } + else if (isRow) + { + final int rowClusteringStart = cursor; + // READ CLUSTERING, repeated for tombstone, will de-dup later + long clusteringBlockHeader = 0; + AbstractType[] types = clusteringColumnTypes; + for (int clusteringIndex = 0; clusteringIndex < types.length; clusteringIndex++) + { + // struct clustering_block { + // varint clustering_block_header; + // simple_cell[] clustering_cells; + // }; + if (clusteringIndex % 32 == 0) { + // TODO: ideally we'd like to get the size while reading rather than have to compute it + clusteringBlockHeader = randomAccessReader.readUnsignedVInt(); + cursor += VIntCoding.computeUnsignedVIntSize(clusteringBlockHeader); + } + AbstractType type = types[clusteringIndex]; + if (isNull(clusteringBlockHeader, clusteringIndex)) { + // handle null + } else if (isEmpty(clusteringBlockHeader, clusteringIndex)) { + // handle empty + } else if (type.isValueLengthFixed()) { + // handle value (TODO: add some JSON sonversion without Strings) + int length = type.valueLengthIfFixed(); + cursor += length; + randomAccessReader.skipBytes(length); + } else { + int length = randomAccessReader.readUnsignedVInt32(); + cursor += VIntCoding.computeUnsignedVIntSize(length); + if (length < 0) + throw new IllegalStateException("Corrupt (negative) value length encountered"); + // handle value (TODO: add some JSON sonversion without Strings) + cursor += length; + randomAccessReader.skipBytes(length); + } + } + // READ CLUSTERING DONE + final int rowBodyStart = cursor; + + long rowSize = randomAccessReader.readUnsignedVInt(); + randomAccessReader.skipBytes((int)rowSize); + cursor += VIntCoding.computeUnsignedVIntSize(rowSize) + rowSize; + // TODO: handle row contents + counters[2]++; + } + else if (isTombstoneMarker) { + // struct range_tombstone_marker { + // byte flags = IS_MARKER; + // byte kind_ordinal; + // be16 bound_values_count; + // struct clustering_block[] clustering_blocks; + // varint marker_body_size; + // varint prev_unfiltered_size; + // }; + byte kind = randomAccessReader.readByte(); + cursor++; + + int clusteringColumnsBound = randomAccessReader.readUnsignedShort(); + cursor+=2; + + // READ CLUSTERING, repeated for row, will de-dup later + long clusteringBlockHeader = 0; + AbstractType[] types = clusteringColumnTypes; + for (int clusteringIndex = 0; clusteringIndex < clusteringColumnsBound; clusteringIndex++) + { + // struct clustering_block { + // varint clustering_block_header; + // simple_cell[] clustering_cells; + // }; + if (clusteringIndex % 32 == 0) { + // TODO: ideally we'd like to get the size while reading rather than have to compute it + clusteringBlockHeader = randomAccessReader.readUnsignedVInt(); + cursor += VIntCoding.computeUnsignedVIntSize(clusteringBlockHeader); + } + AbstractType type = types[clusteringIndex]; + if (isNull(clusteringBlockHeader, clusteringIndex)) { + // handle null + } else if (isEmpty(clusteringBlockHeader, clusteringIndex)) { + // handle empty + } else if (type.isValueLengthFixed()) { + // handle value (TODO: add some JSON sonversion without Strings) + cursor += type.valueLengthIfFixed(); + } else { + int length = randomAccessReader.readUnsignedVInt32(); + //cursor += VIntCoding.computeUnsignedVIntSize(length); + if (length < 0) + throw new IllegalStateException("Corrupt (negative) value length encountered"); + // handle value (TODO: add some JSON sonversion without Strings) + cursor += length; + } + } + // READ CLUSTERING DONE + long length = randomAccessReader.readUnsignedVInt(); + cursor += VIntCoding.computeUnsignedVIntSize(length); + length = randomAccessReader.readUnsignedVInt(); + cursor += VIntCoding.computeUnsignedVIntSize(length); + counters[3]++; + } + + return cursor; + } + + // TODO: C&P from Clustering + // ---Clustering + // no need to do modulo arithmetic for i, since the left-shift execute on the modulus of RH operand by definition + private static boolean isNull(long header, int i) + { + long mask = 1L << (i * 2) + 1; + return (header & mask) != 0; + } + + // no need to do modulo arithmetic for i, since the left-shift execute on the modulus of RH operand by definition + private static boolean isEmpty(long header, int i) + { + long mask = 1L << (i * 2); + return (header & mask) != 0; + } + // ---Clustering + + // TODO: C&P from DeletionTime + // We use the sign bit to signal LIVE DeletionTimes + private final static int IS_LIVE_DELETION = 0b1000_0000; +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingBench.java new file mode 100644 index 0000000000..d6a820ee4d --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingBench.java @@ -0,0 +1,78 @@ +/* + * 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.test.microbench.sstable; + +import java.io.IOException; +import java.util.stream.Stream; + +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.tools.Util; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Benchmark) +public class SSTableReadingBench extends SSTableAbstractBench +{ + private SSTableReader ssTableReader; + + @Setup(Level.Invocation) + public void prepareReader() throws IOException + { + ssTableReader = super.getReader(); + } + + @TearDown(Level.Invocation) + public void closeReader() { + ssTableReader.ref().close(); + } + + long[] counters = new long[4]; + @Benchmark + public void countPartitionsAndUnfiltered() + { + for (int i = 0; i < counters.length; i++) + { + counters[i] = 0; + } + final ISSTableScanner currentScanner = ssTableReader.getScanner(); + Stream partitions = Util.iterToStream(currentScanner); + partitions.forEach(unfilteredRowIterator -> { + counters[0]++; + Row staticRow = unfilteredRowIterator.staticRow(); + if (staticRow != null) { + counters[1]++; + } + unfilteredRowIterator.forEachRemaining(unfiltered -> { + if (unfiltered.isRow()) { + counters[2]++; + } + else { + counters[3]++; + } + }); + }); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingCursorBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingCursorBench.java new file mode 100644 index 0000000000..e5c5d957cc --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingCursorBench.java @@ -0,0 +1,73 @@ +/* + * 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.test.microbench.sstable; + +import java.io.IOException; + +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +@State(Scope.Benchmark) +public class SSTableReadingCursorBench extends SSTableAbstractBench +{ + private SSTableReader ssTableReader; + private SSTableCursorReader cursor; + + @Setup(Level.Invocation) + public void prepareReader() throws IOException + { + ssTableReader = super.getReader(); + cursor = new SSTableCursorReader(ssTableReader); + } + + @TearDown(Level.Invocation) + public void closeReader() throws Exception + { + cursor.close(); + ssTableReader.ref().close(); + } + + long[] counters = new long[4]; + + + @Benchmark + public void readPartitionAndUnfiltered() throws IOException + { + SSTableReadingFileCursorBench.readPartitionAndUnfiltered(counters, cursor); + } + + @Benchmark + public void readPartitionSkipUnfiltered() throws IOException + { + SSTableReadingFileCursorBench.readPartitionSkipUnfiltered(counters, cursor); + } + + @Benchmark + public void skipPartition() throws IOException + { + SSTableReadingFileCursorBench.skipPartition(counters, cursor); + } + +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileBench.java new file mode 100644 index 0000000000..dab8faa36b --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileBench.java @@ -0,0 +1,120 @@ +/* + * 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.test.microbench.sstable; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.io.util.File; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableMetadataRef; +import org.apache.cassandra.tools.Util; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(value = 1) +@State(Scope.Benchmark) +public class SSTableReadingFileBench +{ + static + { + DatabaseDescriptor.toolInitialization(!TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST.getBoolean()); + } + @Param("test/data/compaction/oa-70-big-Data.db") + String sstableFileName; + private Descriptor desc; + private SSTableReader ssTableReader; + + @Setup(Level.Trial) + public void loadDescriptor() throws FileNotFoundException + { + File ssTableFile = new File(sstableFileName); + + if (!ssTableFile.exists()) + { + throw new FileNotFoundException("Cannot find file " + ssTableFile.absolutePath()); + } + desc = Descriptor.fromFileWithComponent(ssTableFile, false).left; + } + + @Setup(Level.Invocation) + public void prepareReader() throws IOException + { + TableMetadata metadata = Util.metadataFromSSTable(desc); + ssTableReader = SSTableReader.openNoValidation(null, desc, TableMetadataRef.forOfflineTools(metadata)); + } + + @TearDown(Level.Invocation) + public void closeReader() { + ssTableReader.ref().close(); + } + + long[] counters = new long[4]; + @Benchmark + public void countPartitionsAndUnfiltered() + { + for (int i = 0; i < counters.length; i++) + { + counters[i] = 0; + } + final ISSTableScanner currentScanner = ssTableReader.getScanner(); + Stream partitions = Util.iterToStream(currentScanner); + partitions.forEach(unfilteredRowIterator -> { + counters[0]++; + Row staticRow = unfilteredRowIterator.staticRow(); + if (staticRow != null) { + counters[1]++; + } + unfilteredRowIterator.forEachRemaining(unfiltered -> { + if (unfiltered.isRow()) { + counters[2]++; + } + else + { + counters[3]++; + } + }); + }); + } +} diff --git a/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java new file mode 100644 index 0000000000..a7d2ebf444 --- /dev/null +++ b/test/microbench/org/apache/cassandra/test/microbench/sstable/SSTableReadingFileCursorBench.java @@ -0,0 +1,218 @@ +/* + * 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.test.microbench.sstable; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.io.sstable.PartitionDescriptor; +import org.apache.cassandra.io.sstable.UnfilteredDescriptor; +import org.apache.cassandra.io.sstable.SSTableCursorReader; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.io.sstable.Descriptor; +import org.apache.cassandra.io.util.File; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.*; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(value = 1) +@State(Scope.Benchmark) +public class SSTableReadingFileCursorBench +{ + static + { + if(!DatabaseDescriptor.isDaemonInitialized()) + DatabaseDescriptor.toolInitialization(!TEST_UTIL_ALLOW_TOOL_REINIT_FOR_TEST.getBoolean()); + } + + @Param("test/data/compaction/oa-70-big-Data.db") + String sstableFileName; + private Descriptor desc; + private SSTableCursorReader cursor; + + @Setup(Level.Trial) + public void loadDescriptor() throws FileNotFoundException + { + File ssTableFile = new File(sstableFileName); + + if (!ssTableFile.exists()) + { + throw new FileNotFoundException("Cannot find file " + ssTableFile.absolutePath()); + } + desc = Descriptor.fromFileWithComponent(ssTableFile, false).left; + } + + @Setup(Level.Invocation) + public void prepareReader() throws IOException + { + cursor = SSTableCursorReader.fromDescriptor(desc); + } + + @TearDown(Level.Invocation) + public void closeReader() throws Exception + { + cursor.close(); + } + + long[] counters = new long[4]; + + @Benchmark + public void readPartitionAndUnfiltered() throws IOException + { + readPartitionAndUnfiltered(counters, cursor); + } + + static void readPartitionAndUnfiltered(long[] counters, SSTableCursorReader cursor) throws IOException + { + Arrays.fill(counters, 0); + int state = PARTITION_START; + PartitionDescriptor pHeader = new PartitionDescriptor(cursor.ssTableReader().getPartitioner().createReusableKey(0)); + UnfilteredDescriptor rHeader = new UnfilteredDescriptor(cursor.ssTableReader().header.clusteringTypes().toArray(AbstractType[]::new)); + while (state != DONE) { + state = cursor.readPartitionHeader(pHeader); + counters[0]++; + state = readThroughPartition(counters, cursor, state, rHeader); + } + } + + private static int readThroughPartition(long[] counters, SSTableCursorReader cursor, int state, UnfilteredDescriptor unfilteredDescriptor) throws IOException + { + while (state != PARTITION_END) { + switch (state) { + case STATIC_ROW_START: + counters[1]++; + state = readThroughStaticRow(cursor, unfilteredDescriptor); + break; + case ROW_START: + counters[2]++; + state = readThroughRow(cursor, unfilteredDescriptor); + break; + case TOMBSTONE_START: + counters[3]++; + state = cursor.readTombstoneMarker(unfilteredDescriptor); + break; + } + } + return cursor.continueReading(); + } + + static int readThroughStaticRow(SSTableCursorReader cursor, UnfilteredDescriptor unfilteredDescriptor) throws IOException + { + int state = cursor.readStaticRowHeader(unfilteredDescriptor); + while (state != UNFILTERED_END) { + state = readThroughCell(cursor); + } + return cursor.continueReading(); + } + + static int readThroughRow(SSTableCursorReader cursor, UnfilteredDescriptor unfilteredDescriptor) throws IOException + { + int state = cursor.readRowHeader(unfilteredDescriptor); + while (state != UNFILTERED_END) { + state = readThroughCell(cursor); + } + return cursor.continueReading(); + } + + private static int readThroughCell(SSTableCursorReader cursor) throws IOException + { + int state = cursor.readCellHeader(); + if (state == CELL_VALUE_START) + { + state = cursor.skipCellValue(); + } + if (state == CELL_END) + state = cursor.continueReading(); + return state; + } + + @Benchmark + public void readPartitionSkipUnfiltered() throws IOException + { + readPartitionSkipUnfiltered(counters, cursor); + } + + static void readPartitionSkipUnfiltered(long[] counters, SSTableCursorReader cursor) throws IOException + { + for (int i = 0; i < counters.length; i++) + { + counters[i] = 0; + } + int state = PARTITION_START; + PartitionDescriptor pHeader = new PartitionDescriptor(cursor.ssTableReader().getPartitioner().createReusableKey(0)); + while (state != DONE) { + state = cursor.readPartitionHeader(pHeader); + counters[0]++; + if (state == PARTITION_END) continue; + while (state != DONE && state != PARTITION_START) { + switch (state) { + case STATIC_ROW_START: + counters[1]++; + state = cursor.skipStaticRow(true); + break; + case ROW_START: + counters[2]++; + state = cursor.skipUnfiltered(true); + break; + case TOMBSTONE_START: + counters[3]++; + state = cursor.skipUnfiltered(true); + break; + } + } + } + } + + @Benchmark + public void skipPartition() throws IOException + { + skipPartition(counters, cursor); + } + + static void skipPartition(long[] counters, SSTableCursorReader cursor) throws IOException + { + for (int i = 0; i < counters.length; i++) + { + counters[i] = 0; + } + while (cursor.skipPartition() != DONE) { + counters[0]++; + } + } +} diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java index 6ae41ab74e..2f47f8cc1e 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java @@ -220,8 +220,8 @@ public class KeyspaceActions extends ClusterActions { int primaryKey = primaryKeys[i]; LongToken token = Murmur3Partitioner.instance.getToken(Int32Type.instance.decompose(primaryKey)); - List readReplicas = readPlacements.replicasFor(token.token); - List writeReplicas = writePlacements.replicasFor(token.token); + List readReplicas = readPlacements.replicasFor(token.getLongValue()); + List writeReplicas = writePlacements.replicasFor(token.getLongValue()); replicasForKey[i] = readReplicas.stream().mapToInt(r -> r.node().idx()).toArray(); Set pendingReplicas = new HashSet<>(writeReplicas); diff --git a/test/unit/org/apache/cassandra/cql3/CQLTester.java b/test/unit/org/apache/cassandra/cql3/CQLTester.java index 4ac41a608b..192b6a059e 100644 --- a/test/unit/org/apache/cassandra/cql3/CQLTester.java +++ b/test/unit/org/apache/cassandra/cql3/CQLTester.java @@ -2251,6 +2251,9 @@ public abstract class CQLTester Assert.assertEquals(String.format("expected %d rows but received %d", expectedCount, actualRowCount), expectedCount, actualRowCount); } + public static void assertRows(UntypedResultSet result, Object[]... rows) { + assertRows(result, List.of(rows)); + } public abstract static class CellValidator { public abstract ByteBuffer expected(); @@ -2378,21 +2381,21 @@ public abstract class CQLTester }; } - public static void assertRows(UntypedResultSet result, Object[]... rows) + public static void assertRows(UntypedResultSet result, List rows) { if (result == null) { - if (rows.length > 0) - Assert.fail(String.format("No rows returned by query but %d expected", rows.length)); + if (rows.size() > 0) + Assert.fail(String.format("No rows returned by query but %d expected", rows.size())); return; } List meta = result.metadata(); Iterator iter = result.iterator(); int i = 0; - while (iter.hasNext() && i < rows.length) + while (iter.hasNext() && i < rows.size()) { - Object[] expected = rows[i]; + Object[] expected = rows.get(i); UntypedResultSet.Row actual = iter.next(); Assert.assertEquals(String.format("Invalid number of (expected) values provided for row %d", i), expected == null ? 1 : expected.length, meta.size()); @@ -2445,10 +2448,11 @@ public abstract class CQLTester } logger.info("Extra row num {}: {}", i, str); } - Assert.fail(String.format("Got more rows than expected. Expected %d but got %d.\nExpected: %s\nActual: %s", rows.length, i, toString(rows), result.toStringUnsafe())); + Assert.fail(String.format("Got more rows than expected. Expected %d but got %d.", rows.size(), i)); + Assert.fail(String.format("Got more rows than expected. Expected %d but got %d.\nExpected: %s\nActual: %s", rows.size(), i, toString(rows), result.toStringUnsafe())); } - Assert.assertTrue(String.format("Got %s rows than expected. Expected %d but got %d", rows.length>i ? "less" : "more", rows.length, i), i == rows.length); + Assert.assertTrue(String.format("Got %s rows than expected. Expected %d but got %d", rows.size()>i ? "less" : "more", rows.size(), i), i == rows.size()); } private static String toString(Object o) diff --git a/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java b/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java index 075fbf8e91..84fd815378 100644 --- a/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java +++ b/test/unit/org/apache/cassandra/cql3/RandomSchemaTest.java @@ -32,6 +32,8 @@ import org.junit.Test; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.compaction.CursorCompactor; import org.apache.cassandra.io.sstable.format.SSTableFormat; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataOutputBuffer; @@ -56,6 +58,7 @@ import static org.junit.Assert.assertTrue; public class RandomSchemaTest extends CQLTester.InMemory { + static final boolean STRESS_CURSOR_COMPACTION = false; static { // make sure blob is always the same @@ -75,30 +78,34 @@ public class RandomSchemaTest extends CQLTester.InMemory resetSchema(); // TODO : when table level override of sstable format is allowed, migrate to that - DatabaseDescriptor.setSelectedSSTableFormat(sstableFormatGen.generate(random)); + if (!STRESS_CURSOR_COMPACTION) DatabaseDescriptor.setSelectedSSTableFormat(sstableFormatGen.generate(random)); Gen udtName = Generators.unique(IDENTIFIER_GEN); TypeGenBuilder withoutUnsafeEquality = AbstractTypeGenerators.withoutUnsafeEquality() .withUDTNames(udtName); - TableMetadata metadata = new TableMetadataBuilder() - .withKeyspaceName(KEYSPACE) - .withTableKinds(TableMetadata.Kind.REGULAR) - .withKnownMemtables() - .withDefaultTypeGen(AbstractTypeGenerators.builder() - .withoutEmpty() - .withMaxDepth(2) - .withDefaultSetKey(withoutUnsafeEquality) - .withoutTypeKinds(AbstractTypeGenerators.TypeKind.COUNTER) - .withUDTNames(udtName)) - .withPartitionColumnsCount(1) - .withPrimaryColumnTypeGen(new TypeGenBuilder(withoutUnsafeEquality) - // map of vector of map crossed the size cut-off for one of the tests, so changed max depth from 2 to 1, so we can't have the second map - .withMaxDepth(1)) - .withClusteringColumnsBetween(1, 2) - .withRegularColumnsBetween(1, 5) - .withStaticColumnsBetween(0, 2) - .build(random); + TableMetadata metadata; + do + { + metadata = new TableMetadataBuilder() + .withKeyspaceName(KEYSPACE) + .withTableKinds(TableMetadata.Kind.REGULAR) + .withKnownMemtables() + .withDefaultTypeGen(AbstractTypeGenerators.builder() + .withoutEmpty() + .withMaxDepth(2) + .withDefaultSetKey(withoutUnsafeEquality) + .withoutTypeKinds(AbstractTypeGenerators.TypeKind.COUNTER) + .withUDTNames(udtName)) + .withPartitionColumnsCount(1) + .withPrimaryColumnTypeGen(new TypeGenBuilder(withoutUnsafeEquality) + // map of vector of map crossed the size cut-off for one of the tests, so changed max depth from 2 to 1, so we can't have the second map + .withMaxDepth(1)) + .withClusteringColumnsBetween(1, 2) + .withRegularColumnsBetween(1, 5) + .withStaticColumnsBetween(0, 2) + .build(random); + } while (STRESS_CURSOR_COMPACTION && CursorCompactor.unsupportedMetadata(metadata)); maybeCreateUDTs(metadata); String createTable = metadata.toCqlString(true, false, false); // just to make the CREATE TABLE stmt easier to read for CUSTOM types @@ -129,6 +136,7 @@ public class RandomSchemaTest extends CQLTester.InMemory // check sstable flush(KEYSPACE, metadata.name); compact(KEYSPACE, metadata.name); + ColumnFamilyStore cfs = getColumnFamilyStore(KEYSPACE, metadata.name); assertRows(execute(selectStmt, (Object[]) rowKey), expected); assertRows(execute(tokenStmt, (Object[]) partitionKeys), partitionKeys); assertRowsNet(executeNet(selectStmt, (Object[]) rowKey), expected); diff --git a/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java b/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java new file mode 100644 index 0000000000..586d203c81 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/ClusteringComparatorTest.java @@ -0,0 +1,117 @@ +/* + * 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.db; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.Test; + +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.Int32Type; +import org.apache.cassandra.db.marshal.LongType; +import org.apache.cassandra.io.util.DataOutputBuffer; +import org.apache.cassandra.utils.ByteBufferUtil; + +import static org.junit.Assert.assertEquals; + +public class ClusteringComparatorTest +{ + @Test + public void compareLong() + { + Iterable> types; + ClusteringComparator comparator = new ClusteringComparator(LongType.instance); + for (int i=0;i<1000; i++) { + long l1 = ThreadLocalRandom.current().nextLong(); + long l2 = ThreadLocalRandom.current().nextLong(); + int cmp = comparator.compare( + Clustering.make(ByteBufferUtil.bytes(l1)), + Clustering.make(ByteBufferUtil.bytes(l2))); + assertEquals(Long.compare(l1, l2), cmp == 0?0:cmp<0?-1:1); + + } + } + + @Test + public void compareRawLong() throws IOException + { + AbstractType[] types = {LongType.instance}; + for (int i=0;i<1000; i++) { + long l1 = ThreadLocalRandom.current().nextLong(); + long l2 = ThreadLocalRandom.current().nextLong(); + + int compare = Long.compare(l1, l2); + int compareCluster = ClusteringComparator.compare(types, clusteringOfLongAsBuffer(types, l1), + clusteringOfLongAsBuffer(types, l2)); + assertEquals("FFS: l1=" + l1 + ", l2=" + l2, + compare, + compareCluster == 0?0:compareCluster<0?-1:1); + assertEquals("FFS: v1=" + l1 + ", v2=" + l2, + compare > 0, + compareCluster > 0); + assertEquals("FFS: v1=" + l1 + ", v2=" + l2, + compare < 0, + compareCluster < 0); + assertEquals("FFS: v1=" + l1 + ", v2=" + l2, + compare == 0, + compareCluster == 0); + } + } + @Test + public void compareRawInt() throws IOException + { + AbstractType[] types = { Int32Type.instance}; + for (int i=0;i<1000; i++) { + int i1 = ThreadLocalRandom.current().nextInt(); + int i2 = ThreadLocalRandom.current().nextInt(); + + int compare = Integer.compare(i1, i2); + int compareCluster = ClusteringComparator.compare(types, clusteringOfIntAsBuffer(types, i1), + clusteringOfIntAsBuffer(types, i2)); + assertEquals("FFS: v1=" + i1 + ", v2=" + i2, + compare > 0, + compareCluster > 0); + assertEquals("FFS: v1=" + i1 + ", v2=" + i2, + compare < 0, + compareCluster < 0); + assertEquals("FFS: v1=" + i1 + ", v2=" + i2, + compare == 0, + compareCluster == 0); + } + } + + private static ByteBuffer clusteringOfLongAsBuffer(AbstractType[] types, long v1) throws IOException + { + Clustering clustering = Clustering.make(ByteBufferUtil.bytes(v1)); + DataOutputBuffer out = new DataOutputBuffer(); + Clustering.serializer.serialize(clustering, out, 0, List.of(types)); + return out.asNewBuffer(); + } + + private static ByteBuffer clusteringOfIntAsBuffer(AbstractType[] types, int v1) throws IOException + { + Clustering clustering = Clustering.make(ByteBufferUtil.bytes(v1)); + DataOutputBuffer out = new DataOutputBuffer(); + Clustering.serializer.serialize(clustering, out, 0, List.of(types)); + return out.asNewBuffer(); + } +} \ No newline at end of file diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java index d09c955173..5d09a91bfe 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionIteratorTest.java @@ -482,6 +482,12 @@ public class CompactionIteratorTest extends CQLTester { return ImmutableSet.of(); } + + @Override + public boolean isFullRange() + { + return false; + } } @Test diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java index b18a20ec98..756b51dd08 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsCQLTest.java @@ -664,6 +664,9 @@ public class CompactionsCQLTest extends CQLTester Throwable cause = t; while (cause != null && !(cause instanceof MarshalException)) cause = cause.getCause(); + if (cause == null) { + t.printStackTrace(); + } assertNotNull(cause); MarshalException me = (MarshalException) cause; assertTrue(me.getMessage().contains(cfs.metadata.keyspace+"."+cfs.metadata.name)); diff --git a/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java b/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java index 79f01f9a59..8d9a8dcaa7 100644 --- a/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/CompactionsTest.java @@ -20,6 +20,7 @@ package org.apache.cassandra.db.compaction; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -331,6 +332,7 @@ public class CompactionsTest { keys.add(Util.dk(Integer.toString(i))); } + Collections.sort(keys); int[] dks = {0, 1, 3}; writeSSTableWithRangeTombstoneMaskingOneColumn(cfs, table, dks); diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnDeleteAndPurgeTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnDeleteAndPurgeTest.java new file mode 100644 index 0000000000..27cedf7f77 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnDeleteAndPurgeTest.java @@ -0,0 +1,292 @@ +/* + * 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.db.compaction.simple; + + +import java.util.Iterator; + +import org.junit.Test; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.utils.TestHelper; + +import static org.junit.Assert.assertTrue; + +@SuppressWarnings({ "UnnecessaryBoxing", "SingleCharacterStringConcatenation" }) +public class CompactionColumnDeleteAndPurgeTest extends SimpleCompactionTest +{ + @Test + public void testColumn1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with " + + "gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + + // Delete cell + execute("DELETE c1 FROM " + table + " using timestamp 1 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testWriteRowAndDeleteAllColumnsCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with " + + "gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE c1, c2 FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + TestHelper.verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + assertTrue(!cells.hasNext()); + } + + @Test + public void testWriteRowAndDeleteOneColumnCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with " + + "gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE c1 FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + TestHelper.verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertTrue(!cell.isTombstone()); + assertTrue(!cells.hasNext()); + } + + @Test + public void testWriteRowAndDeleteOneColumnViaTTLCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with " + + "gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // set column TTL + execute("UPDATE " + table + " using TTL 1 SET c1 = ? WHERE pk = ? AND ck1 = ? AND ck2 = ?", + Long.valueOf(2), // c1 + Long.valueOf(0), //pk + Long.valueOf(0), Integer.valueOf(0));//ck1,ck2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(2000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + TestHelper.verifyAndPrint(cfs, sstable); + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertTrue(!cell.isTombstone()); + assertTrue(!cells.hasNext()); + } + + @Test + public void testWriteRowAndDeleteOneStaticColumnCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with " + + "gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE sc1 FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + TestHelper.verifyAndPrint(cfs, sstable); + // Expected:{"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block", + // "position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2", + // "value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31,"clustering":[0,0], + // "liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"},"cells":[{"name":"c1", + // "deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00.000002Z"}, + // {"name":"c2","deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00 + // .000002Z"}]}]} + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block", + // "position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"}, + // {"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31, + // "clustering":[0,0],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"}, + // "cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"}, + // "tstamp":"1970-01-01T00:00:00.000002Z"},{"name":"c2", + // "deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"},"tstamp":"1970-01-01T00:00:00 + // .000002Z"}]}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + Row staticRow = partition.staticRow(); + assertTrue(!staticRow.isEmpty()); + Iterator> staticCells = staticRow.cells().iterator(); + Cell cell = staticCells.next(); + assertTrue(!cell.isTombstone()); + assertTrue(!staticCells.hasNext()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + cell = cells.next(); + assertTrue(!cell.isTombstone()); + cell = cells.next(); + assertTrue(!cell.isTombstone()); + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnTest.java new file mode 100644 index 0000000000..fdeb5722d3 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionColumnTest.java @@ -0,0 +1,555 @@ +/* + * 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.db.compaction.simple; + + +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; + +import org.junit.Test; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@SuppressWarnings({ "UnnecessaryBoxing", "SingleCharacterStringConcatenation" }) +public class CompactionColumnTest extends SimpleCompactionTest +{ + @Test + public void testColumn1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete cell + execute("DELETE c1 FROM " + table + " using timestamp 1 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":11},"rows":[{"type":"row","position":11,"clustering":[0,0],"cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-03-12T11:32:18Z"},"tstamp":"1970-01-01T00:00:00.000001Z"}]}]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertEquals(1, cell.timestamp()); + assertTrue(cell.isTombstone()); + } + + @Test + public void testColumnCompactionIntoSingleRow() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, c3 bigint, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c3)VALUES(?, ?,?, ?,?, ?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(3));//c3 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c2)VALUES(?, ?,?, ?,?, ?) using timestamp 2", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Integer.valueOf(2));//c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1)VALUES(?, ?,?, ?,?, ?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1));//c1 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + cell = cells.next(); + cell = cells.next(); + assertTrue(!partition.hasNext()); + + } + + @Test + public void testPartialColumnsCompaction64Columns() throws Throwable + { + int columnCount = 64; + testPartialColoumnsCompaction(columnCount); + } + + @Test + public void testPartialColumnsCompactionOver64Columns() throws Throwable + { + int columnCount = 68; + testPartialColoumnsCompaction(columnCount); + } + + private void testPartialColoumnsCompaction(int columnCount) throws IOException + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String createTable = "CREATE TABLE %s ( pk bigint, ck1 bigint"; + for (int i = 0; i < columnCount; i++) createTable += ", c" + i + " bigint"; + createTable += ", PRIMARY KEY(pk, ck1))"; + + String table = createTable(keyspace, createTable); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + // one row has all the columns + String insertAll = "INSERT INTO " + table + "(pk,ck1"; + for (int i = 0; i < columnCount; i++) insertAll += ", c" + i; + insertAll += ")VALUES(?,?"; + for (int i = 0; i < columnCount; i++) insertAll += ",?"; + insertAll += ") using timestamp 1"; + Long[] values = new Long[2 + columnCount]; + Arrays.fill(values, Long.valueOf(0)); + execute(insertAll, (Object[]) values); + + String insertEven = "INSERT INTO " + table + "(pk,ck1"; + for (int i = 0; i < columnCount; i+=2) insertEven += ", c" + i; + insertEven += ")VALUES(?,?"; + for (int i = 0; i < columnCount; i+=2) insertEven += ",?"; + insertEven += ") using timestamp 2"; + values = new Long[2 + columnCount / 2]; + Arrays.fill(values, Long.valueOf(1)); + execute(insertEven, (Object[]) values); + + String insertOdd = "INSERT INTO " + table + "(pk,ck1"; + for (int i = 1; i < columnCount; i+=2) insertOdd += ", c" + i; + insertOdd += ")VALUES(?,?"; + for (int i = 1; i < columnCount; i+=2) insertOdd += ",?"; + insertOdd += ") using timestamp 3"; + values = new Long[2 + columnCount / 2]; + Arrays.fill(values, Long.valueOf(2)); + execute(insertOdd, (Object[]) values); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + ISSTableScanner partitions = sstable.getScanner(); + + for (int i=0;i<3;i++) + { + UnfilteredRowIterator partition = partitions.next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + long timestamp = ((Row) row).primaryKeyLivenessInfo().timestamp(); + Iterator> cells = ((Row) row).cells().iterator(); + if (timestamp == 1) + { + for (int colIndex=0;colIndex cell = cells.next(); + assertTrue(cell.valueSize()!=0); + } + } + else if (timestamp == 2) + { + for (int colIndex=0;colIndex cell = cells.next(); + assertTrue(cell.valueSize()!=0); + String columnName = cell.column().name.toString(); + int cellColIndex = Integer.parseInt(columnName.substring(1)); + assertEquals("Unexpected position:" + cellColIndex, 0, cellColIndex % 2); + } + } + else if (timestamp == 3) + { + for (int colIndex = 0; colIndex cell = cells.next(); + assertTrue(cell.valueSize()!=0); + String columnName = cell.column().name.toString(); + int cellColIndex = Integer.parseInt(columnName.substring(1)); + assertEquals("Unexpected position:" + cellColIndex, 1, cellColIndex % 2); + } + } + else { + fail(); + } + assertTrue(!cells.hasNext()); + assertTrue(!partition.hasNext()); + } + assertTrue(!partitions.hasNext()); + } + + @Test + public void testPartialColumnsCompactionUnder64Columns() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, c3 bigint, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write 1,1,c1 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1)VALUES(?, ?,?, ?,?, ?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(1));//c1 + + // Write 2,2,c2 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c2)VALUES(?, ?,?, ?,?, ?) using timestamp 2", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Integer.valueOf(2));//c2 + + // Write 3,3,c3 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c3)VALUES(?, ?,?, ?,?, ?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(3));//c3 + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write 1,1,c3 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c3)VALUES(?, ?,?, ?,?, ?) using timestamp 4", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(1));//c3 + + // Write 2,2,c1 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1)VALUES(?, ?,?, ?,?, ?) using timestamp 5", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(2));//c1 + + // Write 3,3,c2 + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c2)VALUES(?, ?,?, ?,?, ?) using timestamp 6", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Integer.valueOf(3));//c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + // We expect: + // 1,1,c1=1,c3=1 + // 2,2,c1=2,c2=2 + // 3,3,c2=3,c3=3 + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31}, + // "rows":[ + // {"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000006Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000006Z"}]}, + // {"type":"row","position":31,"clustering":[1,1],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000004Z"}, + // "cells":[{"name":"c1","value":1},{"name":"c3","value":1}]}, + // {"type":"row","position":67,"clustering":[2,2],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000005Z"}, + // "cells":[{"name":"c1","value":2},{"name":"c2","value":2}]}, + // {"type":"row","position":99,"clustering":[3,3],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000006Z"}, + // "cells":[{"name":"c2","value":3},{"name":"c3","value":3}]}]} + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + for (int i=0;i<3;i++) + { + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + cell = cells.next(); + assertTrue(!cells.hasNext()); + } + assertTrue(!partition.hasNext()); + } + + @Test + public void testWriteRowAndDeleteAllColumnsCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE c1, c2 FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected:{"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31,"clustering":[0,0],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"},"cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00.000002Z"},{"name":"c2","deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00.000002Z"}]}]} + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31,"clustering":[0,0],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"},"cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"},"tstamp":"1970-01-01T00:00:00.000002Z"},{"name":"c2","deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"},"tstamp":"1970-01-01T00:00:00.000002Z"}]}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertEquals(2, cell.timestamp()); + assertTrue(cell.isTombstone()); + cell = cells.next(); + assertEquals(2, cell.timestamp()); + } + + @Test + public void testWriteRowAndDeleteOneColumnCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE c1 FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertEquals(2, cell.timestamp()); + assertTrue(cell.isTombstone()); + + cell = cells.next(); + assertTrue(!cell.isTombstone()); + } + + @Test + public void testWriteRowAndDeleteOneColumnViaTTLCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', " + + "'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 " + + "bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // set column TTL + execute("UPDATE " + table + " using TTL 1 SET c1 = ? WHERE pk = ? AND ck1 = ? AND ck2 = ?", + Long.valueOf(2), // c1 + Long.valueOf(0), //pk + Long.valueOf(0), Integer.valueOf(0));//ck1,ck2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(2000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row) row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + // The cell is converted to a tombstone, and the expiration time becomes both the TS and the LDT + assertEquals(cell.localDeletionTime(), cell.timestamp()/1000000); + assertTrue(cell.isTombstone()); + + assertTrue(cells.hasNext()); + cell = cells.next(); + assertTrue(!cell.isTombstone()); + assertTrue(!cells.hasNext()); + } + + @Test + public void testWriteRowAndDeleteOneStaticColumnCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(0), Integer.valueOf(0),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete cells + execute("DELETE sc1 FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected:{"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31,"clustering":[0,0],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"},"cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00.000002Z"},{"name":"c2","deletion_info":{"local_delete_time":"2025-01-25T08:48:55Z"},"tstamp":"1970-01-01T00:00:00.000002Z"}]}]} + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows":[{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]},{"type":"row","position":31,"clustering":[0,0],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000001Z"},"cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"},"tstamp":"1970-01-01T00:00:00.000002Z"},{"name":"c2","deletion_info":{"local_delete_time":"2025-01-25T08:49:54Z"},"tstamp":"1970-01-01T00:00:00.000002Z"}]}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + Row staticRow = partition.staticRow(); + assertTrue(!staticRow.isEmpty()); + Iterator> staticCells = staticRow.cells().iterator(); + Cell cell = staticCells.next(); + assertTrue(cell.isTombstone()); + cell = staticCells.next(); + assertTrue(!cell.isTombstone()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + + Iterator> cells = ((Row) row).cells().iterator(); + cell = cells.next(); + assertTrue(!cell.isTombstone()); + cell = cells.next(); + assertTrue(!cell.isTombstone()); + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgePKTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgePKTest.java new file mode 100644 index 0000000000..74a15c6740 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgePKTest.java @@ -0,0 +1,255 @@ +/* + * 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.db.compaction.simple; + + +import org.junit.Test; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class CompactionDeleteAndPurgePKTest extends SimpleCompactionTest +{ + @Test + public void testPK1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + // even with GC period 0, needs some time + Thread.sleep(1000); + cfs.forceMajorCompaction(); + + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testPK1WriteAndDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testPK2WriteAndDeleteCompactionTwice() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(112), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(12), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(12), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 4 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testPK3DeleteAndWriteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 1 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 2", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + assertTrue(!partition.next().isEmpty()); + } + + @Test + public void testPKDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete every other partition + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i) //pk + ); + } + + // delete a partition that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5 || pk % 2 == 0) { + fail("Expecting pk==5 to be purged"); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgeRowTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgeRowTest.java new file mode 100644 index 0000000000..64e95633b8 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteAndPurgeRowTest.java @@ -0,0 +1,637 @@ +/* + * 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.db.compaction.simple; + + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.CorruptSSTableException; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.KeyReader; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@SuppressWarnings({ "UnnecessaryBoxing", "SingleCharacterStringConcatenation" }) +public class CompactionDeleteAndPurgeRowTest extends SimpleCompactionTest +{ + @Test + public void testRow1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testRow1WriteAndDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11),Integer.valueOf(21) //ck1,ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + + cfs.forceMajorCompaction(); + + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + assertTrue(!partition.hasNext()); + } + + @Test + public void testRow1WriteAndDeleteViaTTLCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // set row TTL + execute("INSERT INTO " + table + "(pk, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using TTL 1", + Long.valueOf(0), //pk + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(2000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + assertTrue(!partition.hasNext()); + } + + @Test + public void testRow1WriteAndRowDeleteAndPKDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testRow1WriteAndPKDeleteAndRowDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + assertTrue(cfs.getLiveSSTables().isEmpty()); + } + + @Test + public void testRow2WriteDeleteWriteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(112), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(12), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); + assertTrue(!row.isEmpty()); + } + + @Test + public void testRowDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete rows [0,2,2] and [2,2,2] + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i), //pk + Long.valueOf(2), //ck1 + Integer.valueOf(2) //ck2 + ); + } + + // delete a partition + row that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + fail(); + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + // only have 2 live rows + Unfiltered row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + assertTrue("pk="+pk,!partition.hasNext()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + } + + @Test + public void testLargeRowDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 blob, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + byte[] blob = new byte[DatabaseDescriptor.getColumnIndexCacheSize()*2]; + ByteBuffer byteBuffer = ByteBuffer.wrap(blob); + ThreadLocalRandom.current().nextBytes(blob); + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), byteBuffer,//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), byteBuffer,//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), byteBuffer,//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete rows [0,2,2] and [2,2,2] + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i), //pk + Long.valueOf(2), //ck1 + Integer.valueOf(2) //ck2 + ); + } + + // delete a partition + row that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + fail(); + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + // only have 2 live rows + Unfiltered row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + assertTrue("pk="+pk,!partition.hasNext()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + try (KeyReader it = sstable.keyReader()) + { + ByteBuffer last = it.key(); + while (it.advance()) last = it.key(); // no-op, just check if index is readable + if (!Objects.equals(last, sstable.getLast().getKey())) + throw new CorruptSSTableException(new IOException("Failed to read partition index"), it.toString()); + } + } + + @Test + public void testLargeCKRowDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 blob, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + byte[] blob = new byte[DatabaseDescriptor.getColumnIndexCacheSize()*2]; + ByteBuffer byteBuffer = ByteBuffer.wrap(blob); + ThreadLocalRandom.current().nextBytes(blob); + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), byteBuffer,//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(1),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), byteBuffer,//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(1),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), byteBuffer, //ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(1), //c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete rows [0,2,bb] and [2,2,bb] + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i), //pk + Long.valueOf(2), //ck1 + byteBuffer //ck2 + ); + } + + // delete a partition + row that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5), //pk + Long.valueOf(11), //ck1 + byteBuffer //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + fail(); + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + // only have 2 live rows + Unfiltered row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,((Row)row).deletion().time().isLive()); + + assertTrue("pk="+pk,!partition.hasNext()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + try (KeyReader it = sstable.keyReader()) + { + ByteBuffer last = it.key(); + while (it.advance()) last = it.key(); // no-op, just check if index is readable + if (!Objects.equals(last, sstable.getLast().getKey())) + throw new CorruptSSTableException(new IOException("Failed to read partition index"), it.toString()); + } + } + + @Test + public void testSingleLargeCKRow() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, ck1 bigint, ck2 blob, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2)) with gc_grace_seconds=0"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + byte[] blob = new byte[DatabaseDescriptor.getColumnIndexCacheSize()*2]; + ByteBuffer byteBuffer = ByteBuffer.wrap(blob); + ThreadLocalRandom.current().nextBytes(blob); + for (int i = 0; i < 1; i++) + { + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), byteBuffer,//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(1),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(1000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeletePKTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeletePKTest.java new file mode 100644 index 0000000000..30b0972086 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeletePKTest.java @@ -0,0 +1,294 @@ +/* + * 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.db.compaction.simple; + + +import org.junit.Test; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class CompactionDeletePKTest extends SimpleCompactionTest +{ + @Test + public void testPK1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + assertEquals(2, partition.partitionLevelDeletion().markedForDeleteAt()); + } + + @Test + public void testPK1WriteAndDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + assertEquals(2, partition.partitionLevelDeletion().markedForDeleteAt()); + } + + @Test + public void testPK2WriteAndDeleteCompactionTwice() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(112), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(12), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(12), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 4 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + assertEquals(4, partition.partitionLevelDeletion().markedForDeleteAt()); + } + + @Test + public void testPK3DeleteAndWriteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 1 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 2", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + assertTrue(!partition.next().isEmpty()); + assertEquals(1, partition.partitionLevelDeletion().markedForDeleteAt()); + } + + @Test + public void testPKDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete every other partition + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i) //pk + ); + } + + // delete a partition that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + assertEquals(pk + timestamp, partition.partitionLevelDeletion().markedForDeleteAt()); + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk, !partition.hasNext()); + assertTrue("pk="+pk,!partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,partition.staticRow().isEmpty()); + assertEquals(pk + timestamp, partition.partitionLevelDeletion().markedForDeleteAt()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowRangeTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowRangeTest.java new file mode 100644 index 0000000000..174fd9bba1 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowRangeTest.java @@ -0,0 +1,771 @@ +/* + * 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.db.compaction.simple; + + +import org.junit.Test; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.rows.RangeTombstoneBoundMarker; +import org.apache.cassandra.db.rows.RangeTombstoneBoundaryMarker; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings({ "UnnecessaryBoxing", "SingleCharacterStringConcatenation" }) +public class CompactionDeleteRowRangeTest extends SimpleCompactionTest +{ + @Test + public void testRow1DeleteRangeCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR", + // "partition":{"key":["0"],"position":11}, + // "rows": [ + // {"type":"range_tombstone_bound","start":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T10:29:02Z"}}}, + // {"type":"range_tombstone_bound","end":{"type":"exclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T10:29:02Z"}}}]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void test2DeleteRangeWithExclusiveMatchingBoundCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 > ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR", + // "partition":{"key":["0"],"position":11},"rows":[ + // {"type":"range_tombstone_bound","start":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:05:05Z"}}}, + // {"type":"range_tombstone_bound","end":{"type":"exclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:05:05Z"}}}, + // {"type":"range_tombstone_bound","start":{"type":"exclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:05:05Z"}}}, + // {"type":"range_tombstone_bound","end":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:05:05Z"}}}]} + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(!((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void test2DeleteRangeWithInclusiveMatchingBoundCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 <= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 >= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":11},"rows":[ + // {"type":"range_tombstone_bound", + // "start":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}, + // {"type":"range_tombstone_boundary", + // "start":{"type":"inclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}, + // "end":{"type":"exclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}, + // {"type":"range_tombstone_bound", + // "end":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}]} + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).openIsInclusive(false)); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundaryMarker)tombstoneMarker).endDeletionTime().markedForDeleteAt()); + assertEquals(3, ((RangeTombstoneBoundaryMarker)tombstoneMarker).startDeletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void test2DeleteRangeWithInclusiveOverlapingBoundCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 <= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(3) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 >= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":11},"rows":[ + // {"type":"range_tombstone_bound", + // "start":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}, + // {"type":"range_tombstone_boundary", + // "start":{"type":"inclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}, + // "end":{"type":"exclusive","clustering":[0,0], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}, + // {"type":"range_tombstone_bound", + // "end":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}]} + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).openIsInclusive(false)); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundaryMarker)tombstoneMarker).endDeletionTime().markedForDeleteAt()); + assertEquals(3, ((RangeTombstoneBoundaryMarker)tombstoneMarker).startDeletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void test2DeleteRangeWithOverlapingBoundAndSameTimestampCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 <= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(3) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 >= ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":11},"rows":[ + // {"type":"range_tombstone_bound", + // "start":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}, + // {"type":"range_tombstone_bound", + // "end":{"type":"inclusive","clustering":[0,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-05-17T09:12:27Z"}}}]} + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void testWrite1RowAndRangeDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(22) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":31},"rows": + // [{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]}, + // {"type":"range_tombstone_bound","start": + // {"type":"inclusive","clustering":[11,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T10:29:02Z"}}}, + // {"type":"range_tombstone_bound","end": + // {"type":"exclusive","clustering":[11,22], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T10:29:02Z"}}}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void testWrite1RowAndRangeDeleteAndPKDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete RT + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(22) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete PK which should remove the RT and row + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertEquals(3, partition.partitionLevelDeletion().markedForDeleteAt()); + assertTrue(partition.staticRow().isEmpty()); + assertFalse(partition.hasNext()); + } + + @Test + public void testWrite1RowAndPKDeleteAndRangeDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete PK + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete RT, which is later than the PK delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(22) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + // {"table kind":"REGULAR","partition": + // {"key":["0"],"position":27, + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T12:22:21Z"}}, + // "rows":[ + // {"type":"range_tombstone_bound","start": + // {"type":"inclusive","clustering":[11,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z","local_delete_time":"2025-04-18T12:22:21Z"}}}, + // {"type":"range_tombstone_bound","end": + // {"type":"exclusive","clustering":[11,22], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000003Z","local_delete_time":"2025-04-18T12:22:21Z"}}}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertEquals(2, partition.partitionLevelDeletion().markedForDeleteAt()); + assertTrue(partition.staticRow().isEmpty()); + + Unfiltered tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(((RangeTombstoneBoundMarker)tombstoneMarker).openIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + + tombstoneMarker = partition.next(); + assertTrue(tombstoneMarker.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).isBoundary()); + assertTrue(!((RangeTombstoneMarker)tombstoneMarker).closeIsInclusive(false)); + assertEquals(3, ((RangeTombstoneBoundMarker)tombstoneMarker).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void testRow2WriteDeleteWriteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 > ?;", + Long.valueOf(0), //pk + Long.valueOf(10) //ck1 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(112), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(12), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31}, "rows":[ + // {"type":"static_block","position":31,"cells":[{"name":"sc1","value":112,"tstamp":"1970-01-01T00:00:00.000003Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000003Z"}]}, + // {"type":"range_tombstone_bound","start": + // {"type":"exclusive","clustering":[10,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T12:15:58Z"}}}, + // {"type": "row", + // "position":48,"clustering":[11,21],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000003Z"}, + // "cells":[{"name":"c1","value":12},{"name":"c2","value":2}]}, + // {"type":"range_tombstone_bound","end": + // {"type":"inclusive", + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000002Z", + // "local_delete_time":"2025-04-18T12:15:58Z"}}}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered unfiltered = partition.next(); + assertTrue(unfiltered.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)unfiltered).isBoundary()); + assertTrue(!((RangeTombstoneBoundMarker)unfiltered).openIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)unfiltered).deletionTime().markedForDeleteAt()); + + unfiltered = partition.next(); + assertTrue(unfiltered.isRow()); + assertTrue(!((Row)unfiltered).isEmpty()); + assertEquals(3, ((Row)unfiltered).primaryKeyLivenessInfo().timestamp()); + + unfiltered = partition.next(); + assertTrue(unfiltered.isRangeTombstoneMarker()); + assertTrue(!((RangeTombstoneMarker)unfiltered).isBoundary()); + assertTrue(((RangeTombstoneMarker)unfiltered).closeIsInclusive(false)); + assertEquals(2, ((RangeTombstoneBoundMarker)unfiltered).deletionTime().markedForDeleteAt()); + assertFalse(partition.hasNext()); + } + + @Test + public void testRowDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete every other partition + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 < ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i), //pk + Long.valueOf(2), //ck1 + Integer.valueOf(3) //ck2 + ); + } + + // delete a partition + row that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // {"table kind":"REGULAR","partition":{"key":["2"],"position":31}, + // "rows":[{"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000006Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000006Z"}]}, + // {"type":"row","position":31,"clustering":[1,1],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000006Z"},"cells":[{"name":"c1","value":2},{"name":"c2","value":2}]}, + // {"type":"range_tombstone_bound","start": + // {"type":"inclusive","clustering":[2,"*"], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000014Z","local_delete_time":"2025-04-18T12:42:52Z"}}}, + // {"type":"range_tombstone_bound","end": + // {"type":"exclusive","clustering":[2,3], + // "deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000014Z","local_delete_time":"2025-04-18T12:42:52Z"}}}, + // {"type":"row","position":100,"clustering":[3,3],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000008Z"},"cells":[{"name":"c1","value":2},{"name":"c2","value":2}]}]} + //{"table kind":"REGULAR","partition":{"key":["3"],"position":163}, + // "rows":[{"type":"static_block","position":163,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000009Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000009Z"}]},{"type":"row","position":163,"clustering":[1,1],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000009Z"},"cells":[{"name":"c1","value":3},{"name":"c2","value":3}]},{"type":"row","position":194,"clustering":[2,2],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000010Z"},"cells":[{"name":"c1","value":3},{"name":"c2","value":3}]},{"type":"row","position":225,"clustering":[3,3],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000011Z"},"cells":[{"name":"c1","value":3},{"name":"c2","value":3}]}]} + //{"table kind":"REGULAR","partition":{"key":["0"],"position":288},"rows":[ + // {"type":"static_block","position":288,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00Z"}]},{"type":"row","position":288,"clustering":[1,1],"liveness_info":{"tstamp":"1970-01-01T00:00:00Z"},"cells":[{"name":"c1","value":0},{"name":"c2","value":0}]},{"type":"range_tombstone_bound","start":{"type":"inclusive","clustering":[2,"*"],"deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000012Z","local_delete_time":"2025-04-18T12:42:52Z"}}},{"type":"range_tombstone_bound","end":{"type":"exclusive","clustering":[2,3],"deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000012Z","local_delete_time":"2025-04-18T12:42:52Z"}}},{"type":"row","position":357,"clustering":[3,3],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000002Z"},"cells":[{"name":"c1","value":0},{"name":"c2","value":0}]}]} + //{"table kind":"REGULAR","partition":{"key":["5"],"position":405},"rows":[ + // {"type":"row","position":405,"clustering":[11,21],"deletion_info":{"marked_deleted":"1970-01-01T00:00:00.000017Z","local_delete_time":"2025-04-18T12:42:52Z"},"cells":[]}]} + //{"table kind":"REGULAR","partition":{"key":["1"],"position":456},"rows":[ + // {"type":"static_block","position":456,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000003Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000003Z"}]},{"type":"row","position":456,"clustering":[1,1],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000003Z"},"cells":[{"name":"c1","value":1},{"name":"c2","value":1}]},{"type":"row","position":487,"clustering":[2,2],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000004Z"},"cells":[{"name":"c1","value":1},{"name":"c2","value":1}]},{"type":"row","position":518,"clustering":[3,3],"liveness_info":{"tstamp":"1970-01-01T00:00:00.000005Z"},"cells":[{"name":"c1","value":1},{"name":"c2","value":1}]}]} + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,row.isRow()); + assertTrue("pk="+pk,!((Row)row).deletion().time().isLive()); + assertEquals("pk="+pk,pk + timestamp, ((Row)row).deletion().time().markedForDeleteAt()); + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + assertTrue("pk="+pk,row.isRow()); + + row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,row.isRangeTombstoneMarker()); + + row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,row.isRangeTombstoneMarker()); + + row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + + assertTrue("pk="+pk,!partition.hasNext()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowTest.java new file mode 100644 index 0000000000..ad745757cb --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionDeleteRowTest.java @@ -0,0 +1,462 @@ +/* + * 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.db.compaction.simple; + + +import java.util.Iterator; + +import org.junit.Test; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.LivenessInfo; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.Unfiltered; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@SuppressWarnings({ "UnnecessaryBoxing", "SingleCharacterStringConcatenation" }) +public class CompactionDeleteRowTest extends SimpleCompactionTest +{ + @Test + public void testRow1DeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(ColumnFamilyStore::disableAutoCompaction)); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(0), //ck1 + Integer.valueOf(0) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(!((Row)row).deletion().time().isLive()); + assertEquals(2, ((Row)row).deletion().time().markedForDeleteAt()); + } + + @Test + public void testRow1WriteAndDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(!((Row)row).deletion().time().isLive()); + assertEquals(2, ((Row)row).deletion().time().markedForDeleteAt()); + + Iterator> cells = ((Row) row).cells().iterator(); + assertTrue(!cells.hasNext()); + } + + @Test + public void testRow1WriteAndDeleteViaTTLCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // set column TTL + execute("INSERT INTO " + table + "(pk, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using TTL 1", + Long.valueOf(0), //pk + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + Thread.sleep(2000); + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + // {"table kind":"REGULAR", + // "partition":{"key":["0"],"position":31}, + // "rows":[ + // {"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]}, + // {"type":"row","position":31,"clustering":[11,21], + // "liveness_info":{"tstamp":"2025-03-12T09:01:59.127Z","ttl":1,"expires_at":"2025-03-12T09:02:00Z","expired":true}, + // "cells":[{"name":"c1","deletion_info":{"local_delete_time":"2025-03-12T09:01:59Z"}},{"name":"c2","deletion_info":{"local_delete_time":"2025-03-12T09:01:59Z"}}]}]} + + // {"table kind":"REGULAR","partition":{"key":["0"],"position":31}, + // "rows":[ + // {"type":"static_block","position":31,"cells":[{"name":"sc1","value":111,"tstamp":"1970-01-01T00:00:00.000001Z"},{"name":"sc2","value":222,"tstamp":"1970-01-01T00:00:00.000001Z"}]}, + // {"type":"row","position":31,"clustering":[11,21], + // "liveness_info":{"tstamp":"2025-03-12T09:47:44.760Z","ttl":1,"expires_at":"2025-03-12T09:47:45Z","expired":true}, + // "cells":[{"name":"c1","value":""},{"name":"c2","value":""}]}]} + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(((Row)row).deletion().time().isLive()); // expired rows are not transformed into tombstones + LivenessInfo livenessInfo = ((Row) row).primaryKeyLivenessInfo(); + assertEquals(1, livenessInfo.ttl()); // TTL as set + assertEquals(livenessInfo.localExpirationTime()-1, livenessInfo.timestamp()/1000000); + + // TTL expiry for the row turns the cells into tombstones + Iterator> cells = ((Row) row).cells().iterator(); + Cell cell = cells.next(); + assertEquals(cell.localDeletionTime(), cell.timestamp()/1000000); + assertTrue(cell.isTombstone()); + assertTrue(cells.hasNext()); + cell = cells.next(); + assertEquals(cell.localDeletionTime(), cell.timestamp()/1000000); + assertTrue(cell.isTombstone()); + assertTrue(!cells.hasNext()); + } + + @Test + public void testRow1WriteAndRowDeleteAndPKDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + // Expected: {"table kind":"REGULAR","partition":{"key":["0"],"position":27,"deletion_info":{"marked_deleted":"2025-01-14T11:20:37.220Z","local_delete_time":"2025-01-14T11:20:37Z"}},"rows":[]} + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(!partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertEquals(3, partition.partitionLevelDeletion().markedForDeleteAt()); + assertTrue(partition.staticRow().isEmpty()); + } + + @Test + public void testRow1WriteAndPKDeleteAndRowDeleteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ?;", + Long.valueOf(0) //pk + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 3 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(!partition.partitionLevelDeletion().isLive()); + assertEquals(2, partition.partitionLevelDeletion().markedForDeleteAt()); + assertTrue(partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(!((Row)row).deletion().time().isLive()); + assertEquals(3, ((Row)row).deletion().time().markedForDeleteAt()); + assertTrue(((Row)row).columnData().isEmpty()); + } + + @Test + public void testRow2WriteDeleteWriteCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 1", + Long.valueOf(0), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(1), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Delete + execute("DELETE FROM " + table + " using timestamp 2 WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(0), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // Write + execute("INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp 3", + Long.valueOf(0), //pk + Long.valueOf(112), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(11), Integer.valueOf(21),//ck1,ck2 + Long.valueOf(12), Integer.valueOf(2));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + UnfilteredRowIterator partition = sstable.getScanner().next(); + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(!partition.staticRow().isEmpty()); + + Unfiltered row = partition.next(); + assertTrue(row.isRow()); + assertTrue(!((Row)row).deletion().time().isLive()); + assertEquals(2, ((Row)row).deletion().time().markedForDeleteAt()); + assertTrue(!row.isEmpty()); + } + + @Test + public void testRowDeleteCompactionInterleaving() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?) using timestamp ?"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?) using timestamp ?"; + int prefix = 0; + long timestamp = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i),//c1,c2 + timestamp++); + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + // delete every other partition + for (int i = 0; i < 4; i+=2) + { + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + i), // timestamp + Long.valueOf(i), //pk + Long.valueOf(2), //ck1 + Integer.valueOf(2) //ck2 + ); + } + + // delete a partition + row that we don't have + execute("DELETE FROM " + table + " using timestamp ? WHERE pk = ? AND ck1 = ? AND ck2 = ?;", + Long.valueOf(timestamp + 5), // timestamp + Long.valueOf(5), //pk + Long.valueOf(11), //ck1 + Integer.valueOf(21) //ck2 + ); + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + try(ISSTableScanner scanner = sstable.getScanner()) + { + while (scanner.hasNext()) { + UnfilteredRowIterator partition = scanner.next(); + long pk = partition.partitionKey().getKey().getLong(); + if (pk == 5) { + assertTrue(partition.hasNext()); + assertTrue(partition.partitionLevelDeletion().isLive()); + assertTrue(partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,row.isRow()); + assertTrue("pk="+pk,!((Row)row).deletion().time().isLive()); + assertEquals("pk="+pk,pk + timestamp, ((Row)row).deletion().time().markedForDeleteAt()); + + } + else if (pk % 2 == 0) + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + Unfiltered row = partition.next(); + assertTrue("pk=" + pk, !row.isEmpty()); + row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,row.isRow()); + assertTrue("pk="+pk,!((Row)row).deletion().time().isLive()); + assertEquals("pk="+pk,pk + timestamp, ((Row)row).deletion().time().markedForDeleteAt()); + + row = partition.next(); + assertTrue("pk="+pk,!row.isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + else + { + assertTrue("pk="+pk,partition.hasNext()); + assertTrue("pk="+pk,partition.partitionLevelDeletion().isLive()); + assertTrue("pk="+pk,!partition.staticRow().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.next().isEmpty()); + assertTrue("pk="+pk,!partition.hasNext()); + } + } + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/CompactionSimpleValueMergeTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionSimpleValueMergeTest.java new file mode 100644 index 0000000000..2223a1b88a --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/CompactionSimpleValueMergeTest.java @@ -0,0 +1,259 @@ +/* + * 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.db.compaction.simple; + + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.apache.cassandra.cql3.UntypedResultSet; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.io.sstable.format.SSTableReader; + +import static org.apache.cassandra.utils.TestHelper.verifyAndPrint; + +public class CompactionSimpleValueMergeTest extends SimpleCompactionTest +{ + @Test + public void testStaticRowCompaction() throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement1 = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?)"; + String writeStatement2 = "INSERT INTO " + table + "(pk,ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?)"; + int prefix = 0; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(111), Integer.valueOf(222),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + prefix = 10; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + prefix = 20; + // Writes, 3 rows in each partition + for (int i = 0; i < 4; i++) + { + execute(writeStatement1, + Long.valueOf(i), //pk + Long.valueOf(311), Integer.valueOf(322),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + execute(writeStatement2, + Long.valueOf(i), //pk + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+i), Integer.valueOf(prefix+i));//c1,c2 + } + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + List rows = new ArrayList<>(); + int[] pks = {2,3,0,1}; + for (int pk : pks) + { + rows.add(new Object[]{ + Long.valueOf(pk), //pk + Long.valueOf(311), Integer.valueOf(322),//sc1,sc2 + Long.valueOf(1), Integer.valueOf(1),//ck1,ck2 + Long.valueOf(prefix+pk), Integer.valueOf(prefix+pk)});//c1,c2 + rows.add(new Object[]{ + Long.valueOf(pk), //pk + Long.valueOf(311), Integer.valueOf(322),//sc1,sc2 + Long.valueOf(2), Integer.valueOf(2),//ck1,ck2 + Long.valueOf(prefix+pk), Integer.valueOf(prefix+pk)});//c1,c2 + rows.add(new Object[]{ + Long.valueOf(pk), //pk + Long.valueOf(311), Integer.valueOf(322),//sc1,sc2 + Long.valueOf(3), Integer.valueOf(3),//ck1,ck2 + Long.valueOf(prefix+pk), Integer.valueOf(prefix+pk)});//c1,c2 + } + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result, + rows); + } + + @Test + public void testCompaction1TableNoPartitionOverlap() throws Throwable + { + genetrateCompactAndVerify(1, 10, false, false, false); + } + + @Test + public void testCompaction2TableNoPartitionOverlap() throws Throwable + { + genetrateCompactAndVerify(2, 10, false, false, false); + } + + @Test + public void testCompaction3TableNoPartitionOverlap() throws Throwable + { + genetrateCompactAndVerify(3, 10, false, false, false); + } + + @Test + public void testCompaction2TableWithPartitionOverlap() throws Throwable + { + genetrateCompactAndVerify(2, 10, true, false, false); + } + + @Test + public void testCompaction3TableWithPartitionOverlap() throws Throwable + { + genetrateCompactAndVerify(3, 10, true, false, false); + } + @Test + public void testCompaction2TableWithRowOverlap() throws Throwable + { + genetrateCompactAndVerify(2, 10, true, true, false); + } + + @Test + public void testCompaction3TableWithRowOverlap() throws Throwable + { + genetrateCompactAndVerify(3, 10, true, true, false); + } + + protected void genetrateCompactAndVerify(int sstableCount, int partitionCount, boolean pOverlap, boolean rOverlap, boolean cOverlap) throws Throwable + { + String keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); + String table = createTable(keyspace, "CREATE TABLE %s ( pk bigint, sc1 bigint static, sc2 int static, ck1 bigint, ck2 int, c1 bigint, c2 int, PRIMARY KEY(pk, ck1, ck2))"); + execute("use " + keyspace + ";"); + Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); + + ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); + cfs.disableAutoCompaction(); + + String writeStatement = "INSERT INTO " + table + "(pk,sc1,sc2, ck1,ck2, c1,c2)VALUES(?, ?,?, ?,?, ?,?)"; + for (int j = 0; j < sstableCount; j++) + { + for (int i = 0; i < partitionCount; i++) + execute(writeStatement, + (Long.valueOf(pOverlap ? 0 : j * partitionCount) + i), //pk + Long.valueOf(j * partitionCount + i), Integer.valueOf(j * partitionCount + i),//sc1,sc2 + Long.valueOf((rOverlap ? 0 : j * partitionCount) + i), Integer.valueOf((rOverlap ? 0 : j * partitionCount) + i),//ck1,ck2 + Long.valueOf(j * partitionCount + i), Integer.valueOf(j * partitionCount + i));//c1,c2 + + cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); + } + + cfs.forceMajorCompaction(); + SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); + verifyAndPrint(cfs, sstable); + + List rows = new ArrayList<>(); + + if (pOverlap && rOverlap) + { + int prefix = (sstableCount - 1) * partitionCount; + int[] pks = {2,3,7,9,4,0,8,5,6,1}; + for (int pk : pks) + { + rows.add(new Object[]{ Long.valueOf(pk), //pk + Long.valueOf(prefix + pk), Integer.valueOf(prefix + pk),//sc1,sc2 + Long.valueOf(pk), Integer.valueOf(pk),//ck1,ck2 + Long.valueOf(prefix + pk), Integer.valueOf(prefix + pk) });//c1,c2 + } + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result, + rows); + } + + if (pOverlap && !rOverlap) + { + int prefix = (sstableCount - 1) * partitionCount; + int[] pks = {2,3,7,9,4,0,8,5,6,1}; + for (int pk : pks) + { + for (int j = 0; j < sstableCount; j++) + { + rows.add(new Object[]{ Long.valueOf(pk), //pk + Long.valueOf(prefix + pk), Integer.valueOf(prefix + pk),//sc1,sc2 + Long.valueOf(j * partitionCount + pk), Integer.valueOf(j * partitionCount + pk),//ck1,ck2 + Long.valueOf(j * partitionCount + pk), Integer.valueOf(j * partitionCount + pk) });//c1,c2 + } + } + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result, + rows); + } + + if (!pOverlap && !rOverlap) + { + int[][] pks = {{2,3,7,9,4,0,8,5,6,1}, + {19,2,3,16,12,13,7,15,9,4,10,0,11,14,8,5,6,1,18,17}, + {19,2,24,3,16,25,12,20,13,7,26,15,23,9,27,21,4,10,28,0,11,14,8,5,22,6,1,18,17,29}}; + for (int pk : pks[sstableCount - 1]) + { + rows.add(new Object[]{ Long.valueOf(pk), //pk + Long.valueOf(pk), Integer.valueOf(pk),//sc1,sc2 + Long.valueOf(pk), Integer.valueOf(pk),//ck1,ck2 + Long.valueOf(pk), Integer.valueOf(pk) });//c1,c2 + + } + UntypedResultSet result = execute("SELECT pk,sc1,sc2, ck1,ck2, c1,c2 FROM " + table); + assertRows(result, + rows); + } + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/simple/SimpleCompactionTest.java b/test/unit/org/apache/cassandra/db/compaction/simple/SimpleCompactionTest.java new file mode 100644 index 0000000000..27c7ba7e17 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/compaction/simple/SimpleCompactionTest.java @@ -0,0 +1,39 @@ +/* + * 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.db.compaction.simple; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.junit.AfterClass; +import org.junit.Ignore; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.utils.TestHelper; + + +@Ignore +public abstract class SimpleCompactionTest extends CQLTester +{ + @AfterClass + public static void teardown() throws IOException, InterruptedException, ExecutionException + { + TestHelper.teardown(); + } +} diff --git a/test/unit/org/apache/cassandra/db/compaction/unified/BackgroundCompactionTrackingTest.java b/test/unit/org/apache/cassandra/db/compaction/unified/BackgroundCompactionTrackingTest.java index 212182e2b8..33e615ce75 100644 --- a/test/unit/org/apache/cassandra/db/compaction/unified/BackgroundCompactionTrackingTest.java +++ b/test/unit/org/apache/cassandra/db/compaction/unified/BackgroundCompactionTrackingTest.java @@ -151,7 +151,7 @@ public class BackgroundCompactionTrackingTest extends CQLTester if (i == 0) Assert.assertEquals(uncompressedSize * 1.0 / tasks, op.getTotal(), uncompressedSize * 0.03); - assertTrue(op.getCompleted() <= op.getTotal() * 1.03); + assertTrue("Expected:" + op.getCompleted() +"<=" + (long)(op.getTotal() * 1.03), op.getCompleted() <= op.getTotal() * 1.03); if (op.getCompleted() >= op.getTotal() * 0.97) ++finished; } diff --git a/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java b/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java index 9a02fb086c..9a00ce49a4 100644 --- a/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java +++ b/test/unit/org/apache/cassandra/dht/Murmur3PartitionerTest.java @@ -74,7 +74,7 @@ public class Murmur3PartitionerTest extends PartitionerTestCase qt().forAll(longs().between(Long.MIN_VALUE + 1, Long.MAX_VALUE)) .check(token -> { ByteBuffer key = Murmur3Partitioner.LongToken.keyForToken(new Murmur3Partitioner.LongToken(token)); - return Murmur3Partitioner.instance.getToken(key).token == token; + return Murmur3Partitioner.instance.getToken(key).getLongValue() == token; }); } } diff --git a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java index 865d1b799e..92b825ba35 100644 --- a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java +++ b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java @@ -178,8 +178,8 @@ public class RouteIndexTest extends CQLTester TokenRange range = selectExistingRange(rs, ranges); // have a key, so find a key within the range - long start = range.start().isMin() ? Long.MIN_VALUE : ((LongToken) range.start().token()).token; - long end = range.end().isMax() ? Long.MAX_VALUE : ((LongToken) range.end().token()).token; + long start = range.start().isMin() ? Long.MIN_VALUE : ((LongToken) range.start().token()).getLongValue(); + long end = range.end().isMax() ? Long.MAX_VALUE : ((LongToken) range.end().token()).getLongValue(); long token = 1 + rs.nextLong(start, end); @Nullable DecidedRX decidedRX = state.nextDecidedRX(rs); return new KeySearch(storeId, new TokenKey(tableId, new LongToken(token)), state.nextTxnRange(rs), decidedRX); diff --git a/test/unit/org/apache/cassandra/io/sstable/VerifyTest.java b/test/unit/org/apache/cassandra/io/sstable/VerifyTest.java index 3b651fc36b..1d79c1d29d 100644 --- a/test/unit/org/apache/cassandra/io/sstable/VerifyTest.java +++ b/test/unit/org/apache/cassandra/io/sstable/VerifyTest.java @@ -142,6 +142,10 @@ public class VerifyTest standardCFMD(KEYSPACE, BF_ALWAYS_PRESENT).bloomFilterFpChance(1.0)); } + protected IVerifier getVerifier(SSTableReader sstable, ColumnFamilyStore cfs, IVerifier.Options.Builder verifierOptions) + { + return sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, verifierOptions.build()); + } @Test public void testVerifyCorrect() @@ -154,7 +158,7 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -174,7 +178,7 @@ public class VerifyTest fillCounterCF(cfs, 2); SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -194,7 +198,7 @@ public class VerifyTest fillCF(cfs, 2); SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -215,7 +219,7 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).extendedVerification(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true).extendedVerification(true))) { verifier.verify(); } @@ -236,7 +240,7 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -257,7 +261,7 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -278,7 +282,7 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().extendedVerification(true).invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().extendedVerification(true).invokeDiskFailurePolicy(true))) { verifier.verify(); } @@ -299,12 +303,13 @@ public class VerifyTest SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().extendedVerification(true).invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().extendedVerification(true).invokeDiskFailurePolicy(true))) { verifier.verify(); } catch (CorruptSSTableException err) { + err.printStackTrace(); fail("Unexpected CorruptSSTableException"); } } @@ -331,7 +336,7 @@ public class VerifyTest writeChecksum(++correctChecksum, sstable.descriptor.fileFor(Components.DIGEST)); } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a CorruptSSTableException to be thrown"); @@ -340,7 +345,7 @@ public class VerifyTest { } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(false).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(false))) { verifier.verify(); fail("Expected a RuntimeException to be thrown"); @@ -380,7 +385,7 @@ public class VerifyTest // Update the Digest to have the right Checksum writeChecksum(simpleFullChecksum(sstable.getFilename()), sstable.descriptor.fileFor(Components.DIGEST)); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { // First a simple verify checking digest, which should succeed try @@ -393,7 +398,7 @@ public class VerifyTest fail("Simple verify should have succeeded as digest matched"); } } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).extendedVerification(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true).extendedVerification(true))) { // Now try extended verify try @@ -425,7 +430,7 @@ public class VerifyTest file.position(0); file.write(ByteBufferUtil.bytes(StringUtils.repeat('z', 2))); file.close(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a CorruptSSTableException to be thrown"); @@ -433,7 +438,7 @@ public class VerifyTest catch (CorruptSSTableException expected) { } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(false).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(false))) { verifier.verify(); fail("Expected a RuntimeException to be thrown"); @@ -470,7 +475,7 @@ public class VerifyTest correctChecksum = file.readLong(); } writeChecksum(++correctChecksum, sstable.descriptor.fileFor(Components.DIGEST)); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().mutateRepairStatus(false).invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().mutateRepairStatus(false).invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a CorruptSSTableException to be thrown"); @@ -482,7 +487,7 @@ public class VerifyTest assertTrue(sstable.isRepaired()); // now the repair status should be changed: - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().mutateRepairStatus(true).invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().mutateRepairStatus(true).invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a CorruptSSTableException to be thrown"); @@ -507,7 +512,7 @@ public class VerifyTest .update(); SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().checkOwnsTokens(true).extendedVerification(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().checkOwnsTokens(true).extendedVerification(true))) { verifier.verify(); } @@ -536,7 +541,7 @@ public class VerifyTest correctChecksum = file.readLong(); } writeChecksum(++correctChecksum, sstable.descriptor.fileFor(Components.DIGEST)); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).mutateRepairStatus(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true).mutateRepairStatus(true))) { verifier.verify(); fail("should be corrupt"); @@ -581,7 +586,7 @@ public class VerifyTest fillCF(cfs, 2); SSTableReader sstable = cfs.getLiveSSTables().iterator().next(); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options())) { verifier.verify(); //still not corrupt, should pass } @@ -590,7 +595,7 @@ public class VerifyTest fileChannel.truncate(3); } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); fail("should throw exception"); @@ -622,7 +627,7 @@ public class VerifyTest writeChecksum(++correctChecksum, sstable.descriptor.fileFor(Components.DIGEST)); } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a CorruptSSTableException to be thrown"); @@ -631,12 +636,12 @@ public class VerifyTest { } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).quick(true).build())) // with quick = true we don't verify the digest + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true).quick(true))) // with quick = true we don't verify the digest { verifier.verify(); } - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().invokeDiskFailurePolicy(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().invokeDiskFailurePolicy(true))) { verifier.verify(); fail("Expected a RuntimeException to be thrown"); @@ -740,7 +745,7 @@ public class VerifyTest for (SSTableReader sstable : cfs.getLiveSSTables()) { - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().checkOwnsTokens(true).build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options().checkOwnsTokens(true))) { verifier.verify(); } @@ -759,7 +764,7 @@ public class VerifyTest { File f = sstable.descriptor.fileFor(Components.FILTER); assertFalse(f.exists()); - try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, IVerifier.options().build())) + try (IVerifier verifier = getVerifier(sstable, cfs, IVerifier.options())) { verifier.verify(); } diff --git a/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java b/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java index 625e2e65ef..3527fd4bc8 100644 --- a/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java +++ b/test/unit/org/apache/cassandra/locator/SimpleStrategyTest.java @@ -286,7 +286,7 @@ public class SimpleStrategyTest extends CassandraTestBase configOptions.put(ReplicationParams.CLASS, SimpleStrategy.class.getName()); configOptions.put("replication_factor", "3/1"); SchemaLoader.createKeyspace("ks", KeyspaceParams.create(false, configOptions)); - Range range1 = range(Murmur3Partitioner.MINIMUM.token, 100); + Range range1 = range(Murmur3Partitioner.MINIMUM.getLongValue(), 100); Util.assertRCEquals(EndpointsForToken.of(range1.right, Replica.fullReplica(endpoints.get(0), range1), diff --git a/test/unit/org/apache/cassandra/service/paxos/PaxosRepairHistoryTest.java b/test/unit/org/apache/cassandra/service/paxos/PaxosRepairHistoryTest.java index 9846895a12..65b9f332bb 100644 --- a/test/unit/org/apache/cassandra/service/paxos/PaxosRepairHistoryTest.java +++ b/test/unit/org/apache/cassandra/service/paxos/PaxosRepairHistoryTest.java @@ -481,7 +481,7 @@ public class PaxosRepairHistoryTest Ballot ballotForToken(LongToken token) { return canonical - .floorEntry(token.token == Long.MIN_VALUE ? token : token.decreaseSlightly()) + .floorEntry(token.getLongValue() == Long.MIN_VALUE ? token : token.decreaseSlightly()) .getValue(); } diff --git a/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java b/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java index bfc3a7f5b8..1285186767 100644 --- a/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java +++ b/test/unit/org/apache/cassandra/tools/OfflineToolUtils.java @@ -79,6 +79,8 @@ public abstract class OfflineToolUtils "Attach Listener", // spawned in intellij IDEA "JNA Cleaner", // spawned by JNA "ThreadLocalMetrics-Cleaner", // spawned by org.apache.cassandra.metrics.ThreadLocalMetrics + "Native reference cleanup thread", + "^ForkJoinPool\\.commonPool-worker-\\d+$" }; static final String[] NON_DEFAULT_MEMTABLE_THREADS = diff --git a/test/unit/org/apache/cassandra/utils/CassandraGenerators.java b/test/unit/org/apache/cassandra/utils/CassandraGenerators.java index edfbe88163..e179eb2f52 100644 --- a/test/unit/org/apache/cassandra/utils/CassandraGenerators.java +++ b/test/unit/org/apache/cassandra/utils/CassandraGenerators.java @@ -1408,16 +1408,16 @@ public final class CassandraGenerators List> unwrap = range.unwrap(); return rs -> { Range subRange = unwrap.get(Math.toIntExact(rs.next(Constraint.between(0, unwrap.size() - 1)))); - long end = ((Murmur3Partitioner.LongToken) subRange.right).token; + long end = ((Murmur3Partitioner.LongToken) subRange.right).getLongValue(); if (end == Long.MIN_VALUE) end = Long.MAX_VALUE; - Constraint token = Constraint.between(((Murmur3Partitioner.LongToken) subRange.left).token + 1, end); + Constraint token = Constraint.between(((Murmur3Partitioner.LongToken) subRange.left).getLongValue() + 1, end); return new Murmur3Partitioner.LongToken(rs.next(token)); }; } else { - Constraint token = Constraint.between(((Murmur3Partitioner.LongToken) range.left).token + 1, ((Murmur3Partitioner.LongToken) range.right).token); + Constraint token = Constraint.between(((Murmur3Partitioner.LongToken) range.left).getLongValue() + 1, ((Murmur3Partitioner.LongToken) range.right).getLongValue()); return rs -> new Murmur3Partitioner.LongToken(rs.next(token)); } } diff --git a/test/unit/org/apache/cassandra/utils/PreSortedBubbleInsertTest.java b/test/unit/org/apache/cassandra/utils/PreSortedBubbleInsertTest.java new file mode 100644 index 0000000000..4f60c16e7c --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/PreSortedBubbleInsertTest.java @@ -0,0 +1,131 @@ +/* + * 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.utils; + +import java.util.Comparator; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.db.compaction.CursorCompactor; + +public class PreSortedBubbleInsertTest +{ + @Test + public void testSortArray123() { + Integer[] array = new Integer[]{1,2,3}; + boolean[] equalsNext = new boolean[array.length]; + sort(array, equalsNext, Integer::compareTo); + Assert.assertArrayEquals(new Integer[]{1,2,3}, array); + Assert.assertArrayEquals(new boolean[array.length], equalsNext); + } + + @Test + public void testSortArray321() { + Integer[] array = new Integer[]{3,2,1}; + boolean[] equalsNext = new boolean[array.length]; + sort(array, equalsNext, Integer::compareTo); + Assert.assertArrayEquals(new Integer[]{1,2,3}, array); + Assert.assertArrayEquals(new boolean[array.length], equalsNext); + } + + @Test + public void testSortArray213() { + Integer[] array = new Integer[]{2,1,3}; + boolean[] equalsNext = new boolean[array.length]; + sort(array, equalsNext, Integer::compareTo); + Assert.assertArrayEquals(new Integer[]{1,2,3}, array); + Assert.assertArrayEquals(new boolean[array.length], equalsNext); + } + + @Test + public void testSortArray231() { + Integer[] array = new Integer[]{2,3,1}; + boolean[] equalsNext = new boolean[array.length]; + sort(array, equalsNext, Integer::compareTo); + Assert.assertArrayEquals(new Integer[]{1,2,3}, array); + Assert.assertArrayEquals(new boolean[array.length], equalsNext); + } + + @Test + public void testSortArray111() { + Integer[] array = new Integer[]{1,1,1}; + boolean[] equalsNext = new boolean[array.length]; + sort(array, equalsNext, Integer::compareTo); + Assert.assertArrayEquals(new Integer[]{1,1,1}, array); + Assert.assertArrayEquals(new boolean[]{true,true,false}, equalsNext); + } + + @Test + public void testSortFrom2Array113Presorted() { + Integer[] array = new Integer[]{1,1,3}; + boolean[] equalsNext = new boolean[]{true,true,false}; + bubbleSortFrom(array, equalsNext, Integer::compareTo, 2); + Assert.assertArrayEquals(new Integer[]{1,1,3}, array); + Assert.assertArrayEquals(new boolean[]{true,false,false}, equalsNext); + } + + @Test + public void testSortFrom1Array113Presorted() { + Integer[] array = new Integer[]{1,1,3}; + boolean[] equalsNext = new boolean[]{false,false,false}; + bubbleSortFrom(array, equalsNext, Integer::compareTo, 1); + Assert.assertArrayEquals(new Integer[]{1,1,3}, array); + Assert.assertArrayEquals(new boolean[]{true,false,false}, equalsNext); + } + + @Test + public void testSortFrom1Array2113Presorted() { + Integer[] array = new Integer[]{2,1,1,3}; + boolean[] equalsNext = new boolean[]{false,true,false,false}; + bubbleSortFrom(array, equalsNext, Integer::compareTo, 1); + Assert.assertArrayEquals(new Integer[]{1,1,2,3}, array); + Assert.assertArrayEquals(new boolean[]{true,false,false,false}, equalsNext); + } + + @Test + public void testSortFrom2Array2411113Presorted() { + Integer[] array = new Integer[]{2,4,1,1,1,1,3}; + boolean[] equalsNext = new boolean[]{false,true,true,true,true,false,false}; + bubbleSortFrom(array, equalsNext, Integer::compareTo, 2); + Assert.assertArrayEquals(new Integer[]{1,1,1,1,2,3,4}, array); + Assert.assertArrayEquals(new boolean[]{true,true,true,false,false,false,false}, equalsNext); + } + + @Test + public void testSortFrom2Array4411113Presorted() { + Integer[] array = new Integer[]{4,4,1,1,1,1,3}; + boolean[] equalsNext = new boolean[]{false,true,true,true,true,false,false}; + bubbleSortFrom(array, equalsNext, Integer::compareTo, 2); + Assert.assertArrayEquals(new Integer[]{1,1,1,1,3,4,4}, array); + Assert.assertArrayEquals(new boolean[]{true,true,true,false,false,true,false}, equalsNext); + } + + static void sort(T[] array, boolean[] equalsNext, Comparator comparator) { + int perturbedLimit = array.length; + bubbleSortFrom(array, equalsNext, comparator, perturbedLimit); + } + + private static void bubbleSortFrom(T[] array, boolean[] equalsNext, Comparator comparator, int perturbedLimit) + { + for (; perturbedLimit > 0; perturbedLimit--) { + CursorCompactor.bubbleInsertElementToPreSorted(array, equalsNext, perturbedLimit, array.length, comparator); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/TestHelper.java b/test/unit/org/apache/cassandra/utils/TestHelper.java new file mode 100644 index 0000000000..11772f81f6 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/TestHelper.java @@ -0,0 +1,60 @@ +/* + * 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.utils; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import org.junit.AfterClass; + +import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.commitlog.CommitLog; +import org.apache.cassandra.io.sstable.ISSTableScanner; +import org.apache.cassandra.io.sstable.IVerifier; +import org.apache.cassandra.io.sstable.format.SSTableReader; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tools.JsonTransformer; +import org.apache.cassandra.tools.Util; + +public class TestHelper +{ + public static void verifyAndPrint(ColumnFamilyStore cfs, SSTableReader sstable) throws IOException + { + try (IVerifier verifier = sstable.getVerifier(cfs, new OutputHandler.LogOutput(), false, + IVerifier.options().invokeDiskFailurePolicy(true).extendedVerification(true).build())) + { + verifier.verify(); + } + try (ISSTableScanner scanner = sstable.getScanner()) + { + JsonTransformer.toJsonLines(scanner, Util.iterToStream(scanner), false, false, sstable.metadata(), + Clock.Global.currentTimeMillis() / 1000, System.out); + } + } + + @AfterClass + public static void teardown() throws IOException, ExecutionException, InterruptedException + { + CommitLog.instance.shutdownBlocking(); + ClusterMetadataService.instance().log().close(); + CQLTester.tearDownClass(); + CQLTester.cleanup(); + } +} diff --git a/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceComparisonTest.java b/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceComparisonTest.java index e1f11ac15a..df4b661915 100644 --- a/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceComparisonTest.java +++ b/test/unit/org/apache/cassandra/utils/bytecomparable/ByteSourceComparisonTest.java @@ -812,9 +812,14 @@ public class ByteSourceComparisonTest extends ByteSourceTestBase ByteBuffer collision = Util.generateMurmurCollision(original, append.getBytes(StandardCharsets.UTF_8)); long[] hash = new long[2]; + long[] hash2 = new long[2]; MurmurHash.hash3_x64_128(original, 0, original.limit(), 0, hash); + MurmurHash.hash3_x64_128(original.array(), original.arrayOffset(), original.limit(), 0, hash2); + Assert.assertArrayEquals(hash, hash2); logger.info(String.format("Original hash %016x,%016x", hash[0], hash[1])); MurmurHash.hash3_x64_128(collision, 0, collision.limit(), 0, hash); + MurmurHash.hash3_x64_128(collision.array(), collision.arrayOffset(), original.limit(), 0, hash2); + Assert.assertArrayEquals(hash, hash2); logger.info(String.format("Collision hash %016x,%016x", hash[0], hash[1])); DecoratedKey kk1 = partitioner.decorateKey(original);