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.
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 super StatefulCursor> 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:
+ *