Add cursor based optimized compaction path

Adds a compaction implementation utilizing new fixed allocation SSTable reader/writer implementations, and other purpose built code, leading to improved efficiencies.

patch by Nitsan Wakart; reviewed by Branimir Lambov, Dmitry Konstantinov for CASSANDRA-20918
This commit is contained in:
Nitsan Wakart 2025-06-22 13:37:29 +02:00 committed by Josh McKenzie
parent 66a7a36647
commit ff78780d61
137 changed files with 13127 additions and 434 deletions

View File

@ -1,4 +1,5 @@
5.1 5.1
* Add cursor based optimized compaction path (CASSANDRA-20918)
* Ensure peers with LEFT status are expired from gossip state (CASSANDRA-21035) * Ensure peers with LEFT status are expired from gossip state (CASSANDRA-21035)
* Optimize UTF8Validator.validate for ASCII prefixed Strings (CASSANDRA-21075) * Optimize UTF8Validator.validate for ASCII prefixed Strings (CASSANDRA-21075)
* Switch LatencyMetrics to use ThreadLocalTimer/ThreadLocalCounter (CASSANDRA-21080) * Switch LatencyMetrics to use ThreadLocalTimer/ThreadLocalCounter (CASSANDRA-21080)

View File

@ -1414,6 +1414,7 @@
<jvmarg value="-Dcassandra.config=file:///${scm_none_yaml}"/> <jvmarg value="-Dcassandra.config=file:///${scm_none_yaml}"/>
<jvmarg value="-Dcassandra.test.storage_compatibility_mode=NONE"/> <jvmarg value="-Dcassandra.test.storage_compatibility_mode=NONE"/>
<jvmarg value="-Dcassandra.skip_sync=true" /> <jvmarg value="-Dcassandra.skip_sync=true" />
<jvmarg value="-Dcassandra.cursor_compaction_enabled=false" />
</testmacrohelper> </testmacrohelper>
</sequential> </sequential>
</macrodef> </macrodef>

View File

@ -193,6 +193,7 @@ public enum CassandraRelevantProperties
CONSISTENT_RANGE_MOVEMENT("cassandra.consistent.rangemovement", "true"), CONSISTENT_RANGE_MOVEMENT("cassandra.consistent.rangemovement", "true"),
CONSISTENT_SIMULTANEOUS_MOVES_ALLOW("cassandra.consistent.simultaneousmoves.allow"), CONSISTENT_SIMULTANEOUS_MOVES_ALLOW("cassandra.consistent.simultaneousmoves.allow"),
CRYPTO_PROVIDER_CLASS_NAME("cassandra.crypto_provider_class_name"), 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_DISK_ERROR_HANDLER("cassandra.custom_disk_error_handler"),
CUSTOM_GUARDRAILS_CONFIG_PROVIDER_CLASS("cassandra.custom_guardrails_config_provider_class"), CUSTOM_GUARDRAILS_CONFIG_PROVIDER_CLASS("cassandra.custom_guardrails_config_provider_class"),
CUSTOM_QUERY_HANDLER_CLASS("cassandra.custom_query_handler_class"), CUSTOM_QUERY_HANDLER_CLASS("cassandra.custom_query_handler_class"),

View File

@ -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.AUTOCOMPACTION_ON_STARTUP_ENABLED;
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_AVAILABLE_PROCESSORS; 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.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;
import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_PAXOS_REPAIR_ON_TOPOLOGY_CHANGE_KEYSPACES; 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) @Replaces(oldName = "enable_drop_compact_storage", converter = Converters.IDENTITY, deprecated = true)
public volatile boolean drop_compact_storage_enabled = false; public volatile boolean drop_compact_storage_enabled = false;
public boolean cursor_compaction_enabled = CURSOR_COMPACTION_ENABLED.getBoolean();
public volatile boolean use_statements_enabled = true; public volatile boolean use_statements_enabled = true;
/** /**

View File

@ -4725,6 +4725,17 @@ public class DatabaseDescriptor
conf.transient_replication_enabled = enabled; 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() public static boolean enableDropCompactStorage()
{ {
return conf.drop_compact_storage_enabled; return conf.drop_compact_storage_enabled;

View File

@ -25,7 +25,7 @@ import org.apache.cassandra.utils.bytecomparable.ByteComparable;
public class BufferDecoratedKey extends DecoratedKey public class BufferDecoratedKey extends DecoratedKey
{ {
private final ByteBuffer key; protected ByteBuffer key;
public BufferDecoratedKey(Token token, ByteBuffer key) public BufferDecoratedKey(Token token, ByteBuffer key)
{ {

View File

@ -133,7 +133,7 @@ public interface Clustering<V> extends ClusteringPrefix<V>, IMeasurableMemory
/** /**
* Serializer for Clustering object. * Serializer for Clustering object.
* <p> * <p>
* 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. * has been defined), we don't record that size.
*/ */
public static class Serializer public static class Serializer

View File

@ -26,14 +26,17 @@ import java.util.Objects;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; 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.marshal.ValueAccessor;
import org.apache.cassandra.db.rows.Row; 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.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.ByteComparable;
import org.apache.cassandra.utils.bytecomparable.ByteSource; 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.EXCLUDED;
import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT; import static org.apache.cassandra.utils.bytecomparable.ByteSource.NEXT_COMPONENT;
@ -156,6 +159,107 @@ public class ClusteringComparator implements Comparator<Clusterable>
return s1 < s2 ? c1.kind().comparedToClustering : -c2.kind().comparedToClustering; 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 <V1, V2> int compare(Clustering<V1> c1, Clustering<V2> c2) public <V1, V2> int compare(Clustering<V1> c1, Clustering<V2> c2)
{ {
return compare(c1, c2, size()); return compare(c1, c2, size());

View File

@ -519,7 +519,7 @@ public interface ClusteringPrefix<V> extends IMeasurableMemory, Clusterable<V>
return result; return result;
} }
byte[][] deserializeValuesWithoutSize(DataInputPlus in, int size, int version, List<AbstractType<?>> types) throws IOException public byte[][] deserializeValuesWithoutSize(DataInputPlus in, int size, int version, List<AbstractType<?>> 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). // 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; assert size > 0;

View File

@ -2330,6 +2330,11 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean, Memtable.Owner
return partitionKeySetIgnoreGcGrace.contains(dk); return partitionKeySetIgnoreGcGrace.contains(dk);
} }
public boolean shouldIgnoreGcGraceForAnyKey()
{
return !partitionKeySetIgnoreGcGrace.isEmpty();
}
public static Iterable<ColumnFamilyStore> all() public static Iterable<ColumnFamilyStore> all()
{ {
List<Iterable<ColumnFamilyStore>> stores = new ArrayList<>(Schema.instance.getKeyspaces().size()); List<Iterable<ColumnFamilyStore>> stores = new ArrayList<>(Schema.instance.getKeyspaces().size());

View File

@ -529,6 +529,7 @@ public class Columns extends AbstractCollection<ColumnMetadata> implements Colle
int supersetCount = superset.size(); int supersetCount = superset.size();
if (columnCount == supersetCount) if (columnCount == supersetCount)
{ {
/** This is prevented by caller for row serialization: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serializeRowBody}*/
out.writeUnsignedVInt32(0); out.writeUnsignedVInt32(0);
} }
else if (supersetCount < 64) else if (supersetCount < 64)

View File

@ -114,7 +114,7 @@ public abstract class DecoratedKey implements PartitionPosition, FilterKey
// The OSS50 version avoids this by adding a terminator. // The OSS50 version avoids this by adding a terminator.
return ByteSource.withTerminatorMaybeLegacy(version, return ByteSource.withTerminatorMaybeLegacy(version,
ByteSource.END_OF_STREAM, ByteSource.END_OF_STREAM,
token.asComparableBytes(version), getToken().asComparableBytes(version),
keyComparableBytes(version)); keyComparableBytes(version));
} }
@ -127,7 +127,7 @@ public abstract class DecoratedKey implements PartitionPosition, FilterKey
return ByteSource.withTerminator( return ByteSource.withTerminator(
before ? ByteSource.LT_NEXT_COMPONENT : ByteSource.GT_NEXT_COMPONENT, before ? ByteSource.LT_NEXT_COMPONENT : ByteSource.GT_NEXT_COMPONENT,
token.asComparableBytes(version), getToken().asComparableBytes(version),
keyComparableBytes(version)); keyComparableBytes(version));
}; };
} }

View File

@ -19,16 +19,16 @@ package org.apache.cassandra.db;
public interface DeletionPurger 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()); 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()); return !liveness.isLive(nowInSec) && shouldPurge(liveness.timestamp(), liveness.localExpirationTime());
} }

View File

@ -36,27 +36,29 @@ import static java.lang.Math.min;
/** /**
* Information on deletion of a storage engine object. * Information on deletion of a storage engine object.
*/ */
public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory public abstract class DeletionTime implements Comparable<DeletionTime>, 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. * 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 serializer = new Serializer();
private static final Serializer legacySerializer = new LegacySerializer(); private static final Serializer legacySerializer = new LegacySerializer();
private final long markedForDeleteAt; protected long markedForDeleteAt;
final int localDeletionTimeUnsignedInteger; protected int localDeletionTimeUnsignedInteger;
public static DeletionTime build(long markedForDeleteAt, long localDeletionTime) 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 // 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 return localDeletionTime < 0 || localDeletionTime > Cell.MAX_DELETION_TIME
? new InvalidDeletionTime(markedForDeleteAt) ? 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. // 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<DeletionTime>, IMeasurableMemory
{ {
return CassandraUInt.compare(Cell.MAX_DELETION_TIME_UNSIGNED_INTEGER, localDeletionTimeUnsignedInteger) < 0 return CassandraUInt.compare(Cell.MAX_DELETION_TIME_UNSIGNED_INTEGER, localDeletionTimeUnsignedInteger) < 0
? new InvalidDeletionTime(markedForDeleteAt) ? new InvalidDeletionTime(markedForDeleteAt)
: new DeletionTime(markedForDeleteAt, localDeletionTimeUnsignedInteger); : new ImmutableDeletionTime(markedForDeleteAt, localDeletionTimeUnsignedInteger);
} }
private DeletionTime(long markedForDeleteAt, long localDeletionTime) private DeletionTime(long markedForDeleteAt, long localDeletionTime)
@ -98,6 +100,11 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
return Cell.deletionTimeUnsignedIntegerToLong(localDeletionTimeUnsignedInteger); return Cell.deletionTimeUnsignedIntegerToLong(localDeletionTimeUnsignedInteger);
} }
public int localDeletionTimeUnsignedInteger()
{
return localDeletionTimeUnsignedInteger;
}
/** /**
* Returns whether this DeletionTime is live, that is deletes no columns. * Returns whether this DeletionTime is live, that is deletes no columns.
*/ */
@ -143,7 +150,7 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
@Override @Override
public String toString() 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) public int compareTo(DeletionTime dt)
@ -155,6 +162,10 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
else return CassandraUInt.compare(localDeletionTimeUnsignedInteger, dt.localDeletionTimeUnsignedInteger); 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) public boolean supersedes(DeletionTime dt)
{ {
return markedForDeleteAt() > dt.markedForDeleteAt() || (markedForDeleteAt() == dt.markedForDeleteAt() && localDeletionTime() > dt.localDeletionTime()); return markedForDeleteAt() > dt.markedForDeleteAt() || (markedForDeleteAt() == dt.markedForDeleteAt() && localDeletionTime() > dt.localDeletionTime());
@ -209,7 +220,7 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
public void serialize(DeletionTime delTime, DataOutputPlus out) throws IOException public void serialize(DeletionTime delTime, DataOutputPlus out) throws IOException
{ {
if (delTime == LIVE) if (delTime == LIVE || delTime.isLive())
out.writeByte(IS_LIVE_DELETION); out.writeByte(IS_LIVE_DELETION);
else else
{ {
@ -238,7 +249,30 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
long mfda = readBytesToMFDA(flags, bytes1, bytes2, bytes4); long mfda = readBytesToMFDA(flags, bytes1, bytes2, bytes4);
int localDeletionTimeUnsignedInteger = in.readInt(); 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<DeletionTime>, IMeasurableMemory
long mfda = buf.getLong(offset); long mfda = buf.getLong(offset);
int localDeletionTimeUnsignedInteger = buf.getInt(offset + TypeSizes.LONG_SIZE); 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<DeletionTime>, IMeasurableMemory
: DeletionTime.build(mfda, ldt); : 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) public DeletionTime deserialize(ByteBuffer buf, int offset)
{ {
int ldt = buf.getInt(offset); int ldt = buf.getInt(offset);
long mfda = buf.getLong(offset + 4); long mfda = buf.getLong(offset + 4);
return mfda == Long.MIN_VALUE && ldt == Integer.MAX_VALUE return mfda == Long.MIN_VALUE && ldt == Integer.MAX_VALUE
? LIVE ? LIVE
: new DeletionTime(mfda, ldt); : new ImmutableDeletionTime(mfda, ldt);
} }
public void skip(DataInputPlus in) throws IOException public void skip(DataInputPlus in) throws IOException
@ -336,8 +383,22 @@ public class DeletionTime implements Comparable<DeletionTime>, 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 // 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) private InvalidDeletionTime(long markedForDeleteAt)
{ {
@ -358,4 +419,61 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
return false; 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);
}
}
} }

View File

@ -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 * unaffected (of course, the rest of said row data might be ttl'ed on its own but this is
* separate). * separate).
*/ */
public class LivenessInfo implements IMeasurableMemory public interface LivenessInfo extends IMeasurableMemory
{ {
public static final long NO_TIMESTAMP = Long.MIN_VALUE; long NO_TIMESTAMP = Long.MIN_VALUE;
public static final int NO_TTL = Cell.NO_TTL; int NO_TTL = Cell.NO_TTL;
/** /**
* Used as flag for representing an expired liveness. * Used as flag for representing an expired liveness.
* *
* TTL per request is at most 20 yrs, so this shouldn't conflict * TTL per request is at most 20 yrs, so this shouldn't conflict
* (See {@link org.apache.cassandra.cql3.Attributes#MAX_TTL}) * (See {@link org.apache.cassandra.cql3.Attributes#MAX_TTL})
*/ */
public static final int EXPIRED_LIVENESS_TTL = Integer.MAX_VALUE; int EXPIRED_LIVENESS_TTL = Integer.MAX_VALUE;
public static final long NO_EXPIRATION_TIME = Cell.NO_DELETION_TIME; long NO_EXPIRATION_TIME = Cell.NO_DELETION_TIME;
public static final LivenessInfo EMPTY = new LivenessInfo(NO_TIMESTAMP); LivenessInfo EMPTY = new ImmutableLivenessInfo(NO_TIMESTAMP);
private static final long UNSHARED_HEAP_SIZE = ObjectSizes.measure(EMPTY); long UNSHARED_HEAP_SIZE = ObjectSizes.measure(EMPTY);
protected final long timestamp; static LivenessInfo create(long timestamp, long nowInSec)
protected LivenessInfo(long timestamp)
{ {
this.timestamp = timestamp; return new ImmutableLivenessInfo(timestamp);
} }
public static LivenessInfo create(long timestamp, long nowInSec) static LivenessInfo expiring(long timestamp, int ttl, long nowInSec)
{
return new LivenessInfo(timestamp);
}
public static LivenessInfo expiring(long timestamp, int ttl, long nowInSec)
{ {
assert ttl != EXPIRED_LIVENESS_TTL; assert ttl != EXPIRED_LIVENESS_TTL;
return new ExpiringLivenessInfo(timestamp, ttl, ExpirationDateOverflowHandling.computeLocalExpirationTime(nowInSec, ttl)); return new ExpiringLivenessInfo(timestamp, ttl, ExpirationDateOverflowHandling.computeLocalExpirationTime(nowInSec, ttl));
@ -86,7 +79,7 @@ public class LivenessInfo implements IMeasurableMemory
: expiring(timestamp, ttl, nowInSec, applyOverflowPolicy); : 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 return ttl == NO_TTL
? create(timestamp, nowInSec) ? create(timestamp, nowInSec)
@ -95,11 +88,11 @@ public class LivenessInfo implements IMeasurableMemory
// Note that this ctor takes the expiration time, not the current time. // Note that this ctor takes the expiration time, not the current time.
// Use when you know that's what you want. // 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) if (ttl == EXPIRED_LIVENESS_TTL)
return new ExpiredLivenessInfo(timestamp, ttl, localExpirationTime); 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. * @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). * @return the liveness info timestamp (or {@link #NO_TIMESTAMP} if the info is empty).
*/ */
public long timestamp() long timestamp();
{
return timestamp;
}
/** /**
* Whether the info has a ttl. * Whether the info has a ttl.
*/ */
public boolean isExpiring() boolean isExpiring();
{
return false;
}
/** /**
* The ttl (if any) on the row primary key columns or {@link #NO_TTL} if it is not * 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 * Please note that this value is the TTL that was set originally and is thus not
* changing. * changing.
*/ */
public int ttl() int ttl();
{
return NO_TTL;
}
/** /**
* The expiration time (in seconds) if the info is expiring ({@link #NO_EXPIRATION_TIME} otherwise). * The expiration time (in seconds) if the info is expiring ({@link #NO_EXPIRATION_TIME} otherwise).
* *
*/ */
public long localExpirationTime() long localExpirationTime();
{
return NO_EXPIRATION_TIME;
}
/** /**
* Whether that info is still live. * Whether that info is still live.
@ -160,17 +140,15 @@ public class LivenessInfo implements IMeasurableMemory
* @param nowInSec the current time in seconds. * @param nowInSec the current time in seconds.
* @return whether this liveness info is live or not. * @return whether this liveness info is live or not.
*/ */
public boolean isLive(long nowInSec) boolean isLive(long nowInSec);
{
return !isEmpty();
}
/** /**
* Adds this liveness information to the provided digest. * Adds this liveness information to the provided digest.
* *
* @param digest the digest to add this liveness information to. * @param digest the digest to add this liveness information to.
*/ */
public void digest(Digest digest) default void digest(Digest digest)
{ {
digest.updateWithLong(timestamp()); digest.updateWithLong(timestamp());
} }
@ -180,7 +158,7 @@ public class LivenessInfo implements IMeasurableMemory
* *
* @throws MarshalException if some of the data is corrupted. * @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. * @return the size of the data this liveness information contains.
*/ */
public int dataSize() default int dataSize()
{ {
return TypeSizes.sizeof(timestamp()); return TypeSizes.sizeof(timestamp());
} }
/** /**
* Whether this liveness information supersedes another one (that is * 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, * 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, * 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 * If timestamps are the same and one of them is expired livenessInfo. Expired livenessInfo
* supersedes, ie. tombstone supersedes. * supersedes, ie. tombstone supersedes.
@ -216,10 +194,12 @@ public class LivenessInfo implements IMeasurableMemory
* *
* @return whether this {@code LivenessInfo} supersedes {@code other}. * @return whether this {@code LivenessInfo} supersedes {@code other}.
*/ */
public boolean supersedes(LivenessInfo other) default boolean supersedes(LivenessInfo other)
{ {
if (timestamp != other.timestamp) long tTimestamp = timestamp();
return timestamp > other.timestamp; long oTimestamp = other.timestamp();
if (tTimestamp != oTimestamp)
return tTimestamp > oTimestamp;
if (isExpired() ^ other.isExpired()) if (isExpired() ^ other.isExpired())
return isExpired(); return isExpired();
if (isExpiring() == other.isExpiring()) if (isExpiring() == other.isExpiring())
@ -231,7 +211,7 @@ public class LivenessInfo implements IMeasurableMemory
return isExpiring(); return isExpiring();
} }
protected boolean isExpired() default boolean isExpired()
{ {
return false; return false;
} }
@ -244,47 +224,24 @@ public class LivenessInfo implements IMeasurableMemory
* as timestamp. If it has no timestamp however, this liveness info is returned * as timestamp. If it has no timestamp however, this liveness info is returned
* unchanged. * 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); return LivenessInfo.create(newTimestamp, ttl(), newLocalDeletionTime, true);
} }
// C14227 To prevent row resurrection and be backwards compatible sometimes we need to force an overflowed ldt // 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); return LivenessInfo.create(newTimestamp, ttl(), newLocalDeletionTime, applyOverflowPolicy);
} }
@Override @Override
public String toString() default long unsharedHeapSize()
{
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()
{ {
return this == EMPTY ? 0 : UNSHARED_HEAP_SIZE; 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}. * 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) 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 int ttl;
private final long localExpirationTime; private final long localExpirationTime;
@ -401,7 +358,7 @@ public class LivenessInfo implements IMeasurableMemory
@Override @Override
public String toString() 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() public long unsharedHeapSize()
@ -409,4 +366,66 @@ public class LivenessInfo implements IMeasurableMemory
return UNSHARED_HEAP_SIZE; 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());
}
}
} }

View File

@ -145,7 +145,7 @@ public class RangeTombstoneList implements Iterable<RangeTombstone>, IMeasurable
add(tombstone.deletedSlice().start(), add(tombstone.deletedSlice().start(),
tombstone.deletedSlice().end(), tombstone.deletedSlice().end(),
tombstone.deletionTime().markedForDeleteAt(), tombstone.deletionTime().markedForDeleteAt(),
tombstone.deletionTime().localDeletionTimeUnsignedInteger); tombstone.deletionTime().localDeletionTimeUnsignedInteger());
} }
/** /**

View File

@ -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) +
'}');
}
}

View File

@ -207,6 +207,14 @@ public class SerializationHeader
return DeletionTime.build(markedAt, localDeletionTime); 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) public long timestampSerializedSize(long timestamp)
{ {
return TypeSizes.sizeofUnsignedVInt(timestamp - stats.minTimestamp); return TypeSizes.sizeofUnsignedVInt(timestamp - stats.minTimestamp);

View File

@ -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<SSTableReader> nonExpiredSSTables);
@Override
public abstract void close() throws IOException;
public abstract Collection<SSTableReader> finishWriting();
public abstract long estimatedKeys();
public abstract void stop();
}

View File

@ -820,7 +820,7 @@ public class CompactionStrategyManager implements INotificationConsumer
* *
* lives in matches the list index of the holder that's responsible for it * lives in matches the list index of the holder that's responsible for it
*/ */
public List<GroupedSSTableContainer> groupSSTables(Iterable<SSTableReader> sstables) public final List<GroupedSSTableContainer> groupSSTables(Iterable<SSTableReader> sstables)
{ {
List<GroupedSSTableContainer> classified = new ArrayList<>(holders.size()); List<GroupedSSTableContainer> classified = new ArrayList<>(holders.size());
for (AbstractStrategyHolder holder : holders) for (AbstractStrategyHolder holder : holders)
@ -970,7 +970,7 @@ public class CompactionStrategyManager implements INotificationConsumer
* @param ranges * @param ranges
* @return * @return
*/ */
public AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges) public final AbstractCompactionStrategy.ScannerList maybeGetScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
{ {
maybeReloadDiskBoundaries(); maybeReloadDiskBoundaries();
List<ISSTableScanner> scanners = new ArrayList<>(sstables.size()); List<ISSTableScanner> scanners = new ArrayList<>(sstables.size());

View File

@ -70,7 +70,9 @@ import static org.apache.cassandra.utils.Clock.Global.nanoTime;
public class CompactionTask extends AbstractCompactionTask public class CompactionTask extends AbstractCompactionTask
{ {
private static final int MEGABYTE = 1024 * 1024;
protected static final Logger logger = LoggerFactory.getLogger(CompactionTask.class); protected static final Logger logger = LoggerFactory.getLogger(CompactionTask.class);
protected final long gcBefore; protected final long gcBefore;
protected final boolean keepOriginals; protected final boolean keepOriginals;
protected static long totalBytesCompacted = 0; 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, * For internal use and testing only. The rest of the system should go through the submit* methods,
* which are properly serialized. * which are properly serialized.
* Caller is in charge of marking/unmarking the sstables as compacting. * Caller is in charge of marking/unmarking the sstables as compacting.
*
* NOTE: this method is a Byteman hook location
*/ */
@Override @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 // 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. // 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(); long nowInSec = FBUtilities.nowInSeconds();
try (Refs<SSTableReader> refs = Refs.ref(actuallyCompact); try (Refs<SSTableReader> refs = Refs.ref(actuallyCompact);
AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(actuallyCompact, rangeList); 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; long lastCheckObsoletion = start;
inputSizeBytes = scanners.getTotalCompressedSize(); inputSizeBytes = scanners.getTotalCompressedSize();
@ -253,30 +257,29 @@ public class CompactionTask extends AbstractCompactionTask
if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO) if (compressionRatio == MetadataCollector.NO_COMPRESSION_RATIO)
compressionRatio = 1.0; compressionRatio = 1.0;
long lastBytesScanned = 0;
activeCompactions.beginCompaction(ci); 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 // 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. // 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 // We already have the sstables marked compacting here so CompactionManager#waitForCessation will
// block until the below exception is thrown and the transaction is cancelled. // block until the below exception is thrown and the transaction is cancelled.
if (!controller.cfs.getCompactionStrategyManager().isActive()) if (!controller.cfs.getCompactionStrategyManager().isActive())
throw new CompactionInterruptedException(ci.getCompactionInfo()); throw new CompactionInterruptedException(ci.getCompactionInfo());
estimatedKeys = writer.estimatedKeys(); estimatedKeys = ci.estimatedKeys();
while (ci.hasNext()) while (ci.processNextPartitionKey())
{ {
if (writer.append(ci.next())) long bytesScanned = ci.getTotalBytesScanned();
totalKeysWritten++;
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 // Rate limit the scanners, and account for compression
CompactionManager.instance.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio); CompactionManager.instance.compactionRateLimiterAcquire(limiter, bytesScanned, lastBytesScanned, compressionRatio);
lastBytesScanned = bytesScanned; lastBytesScanned = bytesScanned;
}
if (nanoTime() - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L)) if (nanoTime() - lastCheckObsoletion > TimeUnit.MINUTES.toNanos(1L))
{ {
@ -284,19 +287,27 @@ public class CompactionTask extends AbstractCompactionTask
lastCheckObsoletion = nanoTime(); lastCheckObsoletion = nanoTime();
} }
} }
if (ci.getTotalBytesScanned() != lastBytesScanned)
{
// Report any leftover bytes
CompactionManager.instance.compactionRateLimiterAcquire(limiter, ci.getTotalBytesScanned(), lastBytesScanned, compressionRatio);
}
timeSpentWritingKeys = TimeUnit.NANOSECONDS.toMillis(nanoTime() - start); timeSpentWritingKeys = TimeUnit.NANOSECONDS.toMillis(nanoTime() - start);
// point of no return // point of no return
newSStables = writer.finish(); newSStables = finish(ci);
} }
finally finally
{ {
activeCompactions.finishCompaction(ci); activeCompactions.finishCompaction(ci);
mergedRowCounts = ci.getMergedRowCounts(); mergedRowCounts = ci.getMergedRowCounts();
totalSourceCQLRows = ci.getTotalSourceCQLRows(); totalSourceCQLRows = ci.getTotalSourceCQLRows();
totalKeysWritten = ci.getTotalKeysWritten();
} }
} }
if (transaction.isOffline()) if (transaction.isOffline())
return; return;
@ -343,6 +354,28 @@ public class CompactionTask extends AbstractCompactionTask
// update the metrics // update the metrics
cfs.metric.compactionBytesWritten.inc(endsize); 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<SSTableReader> finish(AbstractCompactionPipeline pipeline)
{
return pipeline.finishWriting();
}
/**
* NOTE: a Byteman hook
*/
protected AutoCloseable getCompactionAwareWriter(Set<SSTableReader> actuallyCompact, AbstractCompactionPipeline pipeline)
{
return pipeline.openWriterResource(cfs, getDirectories(), transaction, actuallyCompact);
} }
public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs, public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs,

View File

@ -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<SSTableReader> nonExpiredSSTables) {
this.writer = task.getCompactionAwareWriter(cfs, directories, transaction, nonExpiredSSTables);
return writer;
}
@Override
public Collection<SSTableReader> 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();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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<SSTableReader> nonExpiredSSTables) {
this.writer = task.getCompactionAwareWriter(cfs, directories, transaction, nonExpiredSSTables);
return writer;
}
@Override
public Collection<SSTableReader> 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();
}
}

View File

@ -297,6 +297,7 @@ public class LeveledCompactionStrategy extends AbstractCompactionStrategy
return levelFanoutSize; return levelFanoutSize;
} }
@Override
public ScannerList getScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges) public ScannerList getScanners(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)
{ {
Set<SSTableReader>[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot(); Set<SSTableReader>[] sstablesPerLevel = manifest.getSStablesPerLevelSnapshot();
@ -432,7 +433,12 @@ public class LeveledCompactionStrategy extends AbstractCompactionStrategy
assert sstableIterator.hasNext(); // caller should check intersecting first assert sstableIterator.hasNext(); // caller should check intersecting first
SSTableReader currentSSTable = sstableIterator.next(); SSTableReader currentSSTable = sstableIterator.next();
currentScanner = currentSSTable.getScanner(ranges); currentScanner = currentSSTable.getScanner(ranges);
}
@Override
public boolean isFullRange()
{
return ranges == null;
} }
public static Collection<SSTableReader> intersecting(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges) public static Collection<SSTableReader> intersecting(Collection<SSTableReader> sstables, Collection<Range<Token>> ranges)

View File

@ -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());
}
}
}

View File

@ -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.SSTableReader;
import org.apache.cassandra.io.sstable.format.SSTableWriter; import org.apache.cassandra.io.sstable.format.SSTableWriter;
import org.apache.cassandra.io.sstable.metadata.MetadataCollector; 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.FBUtilities;
import org.apache.cassandra.utils.TimeUUID; import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.Transactional; import org.apache.cassandra.utils.concurrent.Transactional;
@ -69,7 +68,7 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa
private final List<PartitionPosition> diskBoundaries; private final List<PartitionPosition> diskBoundaries;
private int locationIndex; private int locationIndex;
protected Directories.DataDirectory currentDirectory; protected Directories.DataDirectory currentDirectory;
protected String sstableDirectoryPath;
public CompactionAwareWriter(ColumnFamilyStore cfs, public CompactionAwareWriter(ColumnFamilyStore cfs,
Directories directories, Directories directories,
ILifecycleTransaction txn, ILifecycleTransaction txn,
@ -151,9 +150,10 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa
return realAppend(partition); 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 @Override
@ -173,35 +173,36 @@ public abstract class CompactionAwareWriter extends Transactional.AbstractTransa
* specific strategy has decided a new sstable is needed. * specific strategy has decided a new sstable is needed.
* Guaranteed to be called before the first call to realAppend. * Guaranteed to be called before the first call to realAppend.
*/ */
protected void maybeSwitchWriter(DecoratedKey key) public final SSTableWriter maybeSwitchWriter(DecoratedKey key)
{ {
if (maybeSwitchLocation(key)) SSTableWriter newWriter = maybeSwitchLocation(key);
return; if (newWriter == null && shouldSwitchWriterInCurrentLocation(key))
{
if (shouldSwitchWriterInCurrentLocation(key)) newWriter = switchCompactionWriter(currentDirectory, key);
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 * Switches the file location and writer and returns true if the new key should be placed in a different data
* directory. * directory.
*/ */
protected boolean maybeSwitchLocation(DecoratedKey key) private SSTableWriter maybeSwitchLocation(DecoratedKey key)
{ {
if (diskBoundaries == null) if (diskBoundaries == null)
{ {
if (locationIndex < 0) if (locationIndex < 0)
{ {
Directories.DataDirectory defaultLocation = getWriteDirectory(nonExpiredSSTables, getExpectedWriteSize()); Directories.DataDirectory defaultLocation = getWriteDirectory(nonExpiredSSTables, getExpectedWriteSize());
switchCompactionWriter(defaultLocation, key); SSTableWriter writer = switchCompactionWriter(defaultLocation, key);
locationIndex = 0; locationIndex = 0;
return true; return writer;
} }
return false; return null;
} }
if (locationIndex > -1 && key.compareTo(diskBoundaries.get(locationIndex)) < 0) if (locationIndex > -1 && key.compareTo(diskBoundaries.get(locationIndex)) < 0)
return false; return null;
int prevIdx = locationIndex; int prevIdx = locationIndex;
while (locationIndex == -1 || key.compareTo(diskBoundaries.get(locationIndex)) > 0) 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); Directories.DataDirectory newLocation = locations.get(locationIndex);
if (prevIdx >= 0) if (prevIdx >= 0)
logger.debug("Switching write location from {} to {}", locations.get(prevIdx), newLocation); logger.debug("Switching write location from {} to {}", locations.get(prevIdx), newLocation);
switchCompactionWriter(newLocation, key); return switchCompactionWriter(newLocation, key);
return true;
} }
/** /**
@ -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. * Implementations of this method should finish the current sstable writer and start writing to this directory.
* <p> * <p>
* Called once before starting to append and then whenever we see a need to start writing to another 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; 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) protected SSTableWriter sstableWriter(Directories.DataDirectory directory, DecoratedKey nextKey)

View File

@ -26,6 +26,7 @@ import org.apache.cassandra.db.compaction.LeveledManifest;
import org.apache.cassandra.db.lifecycle.ILifecycleTransaction; import org.apache.cassandra.db.lifecycle.ILifecycleTransaction;
import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SSTableWriter;
public class MajorLeveledCompactionWriter extends CompactionAwareWriter public class MajorLeveledCompactionWriter extends CompactionAwareWriter
{ {
@ -87,12 +88,12 @@ public class MajorLeveledCompactionWriter extends CompactionAwareWriter
} }
@Override @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)); averageEstimatedKeysPerSSTable = Math.round(((double) averageEstimatedKeysPerSSTable * sstablesWritten + partitionsWritten) / (sstablesWritten + 1));
partitionsWritten = 0; partitionsWritten = 0;
sstablesWritten = 0; sstablesWritten = 0;
super.switchCompactionWriter(location, nextKey); return super.switchCompactionWriter(location, nextKey);
} }
protected int sstableLevel() protected int sstableLevel()

View File

@ -53,14 +53,14 @@ public class MaxThreshold extends Threshold
} }
@Override @Override
protected long failValue(ClientState state) public long failValue(ClientState state)
{ {
long failValue = failThreshold.applyAsLong(state); long failValue = failThreshold.applyAsLong(state);
return failValue <= 0 ? Long.MAX_VALUE : failValue; return failValue <= 0 ? Long.MAX_VALUE : failValue;
} }
@Override @Override
protected long warnValue(ClientState state) public long warnValue(ClientState state)
{ {
long warnValue = warnThreshold.applyAsLong(state); long warnValue = warnThreshold.applyAsLong(state);
return warnValue <= 0 ? Long.MAX_VALUE : warnValue; return warnValue <= 0 ? Long.MAX_VALUE : warnValue;

View File

@ -288,7 +288,7 @@ public abstract class UnfilteredPartitionIterators
} }
/** /**
* Digests the the provided iterator. * Digests the provided iterator.
* *
* Caller must close the provided iterator. * Caller must close the provided iterator.
* *

View File

@ -80,13 +80,15 @@ public class BTreeRow extends AbstractRow
private static final Comparator<ColumnData> COLUMN_COMPARATOR = (cd1, cd2) -> cd1.column.compareTo(cd2.column); private static final Comparator<ColumnData> 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 // We need to filter the tombstones of a row on every read (twice in fact: first to remove purgeable tombstone,
// 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 // and then after reconciliation to remove all tombstone since we don't return them to the client) as well as on
// 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, // compaction. But it's likely that many rows won't have any tombstone at all, so we want to speed up that case
// 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 // by not having to iterate/copy the row in this case. We could keep a single boolean telling us if we have
// 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 // tombstones, but that doesn't work for expiring columns. So instead we keep the deletion time for the first
// 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 // thing in the row to be deleted. This allows at any given time to know if we have any deleted information or not.
// no expiring cells, this will be Cell.MAX_DELETION_TIME; // 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 final long minLocalDeletionTime;
private BTreeRow(Clustering clustering, private BTreeRow(Clustering clustering,

View File

@ -258,7 +258,7 @@ public abstract class Cell<V> extends ColumnData
* where not all field are always present (in fact, only the [ flags ] are guaranteed to be present). The fields have the following * where not all field are always present (in fact, only the [ flags ] are guaranteed to be present). The fields have the following
* meaning: * meaning:
* - [ flags ] is the cell flags. It is a byte for which each bit represents a flag whose meaning is explained below (*_MASK constants) * - [ 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) * - [ 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. * 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 * - [ 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<V> extends ColumnData
* - [ value ]: the cell value, unless it has the HAS_EMPTY_VALUE_MASK. * - [ 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. * - [ 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. public 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. public 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. 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.
private 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_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 USE_ROW_TTL_MASK = 0x10; // Wether the cell has the same ttl than the row this is a cell of.
public <T> void serialize(Cell<T> cell, ColumnMetadata column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException public <T> void serialize(Cell<T> cell, ColumnMetadata column, DataOutputPlus out, LivenessInfo rowLiveness, SerializationHeader header) throws IOException
{ {
@ -319,11 +319,11 @@ public abstract class Cell<V> extends ColumnData
public <V> Cell<V> deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnMetadata column, SerializationHeader header, DeserializationHelper helper, ValueAccessor<V> accessor) throws IOException public <V> Cell<V> deserialize(DataInputPlus in, LivenessInfo rowLiveness, ColumnMetadata column, SerializationHeader header, DeserializationHelper helper, ValueAccessor<V> accessor) throws IOException
{ {
int flags = in.readUnsignedByte(); int flags = in.readUnsignedByte();
boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0; boolean hasValue = hasValue(flags);
boolean isDeleted = (flags & IS_DELETED_MASK) != 0; boolean isDeleted = isDeleted(flags);
boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0; boolean isExpiring = isExpiring(flags);
boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0; boolean useRowTimestamp = useRowTimestamp(flags);
boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0; boolean useRowTTL = useRowTTL(flags);
long timestamp = useRowTimestamp ? rowLiveness.timestamp() : header.readTimestamp(in); long timestamp = useRowTimestamp ? rowLiveness.timestamp() : header.readTimestamp(in);
@ -390,11 +390,11 @@ public abstract class Cell<V> extends ColumnData
public boolean skip(DataInputPlus in, ColumnMetadata column, SerializationHeader header) throws IOException public boolean skip(DataInputPlus in, ColumnMetadata column, SerializationHeader header) throws IOException
{ {
int flags = in.readUnsignedByte(); int flags = in.readUnsignedByte();
boolean hasValue = (flags & HAS_EMPTY_VALUE_MASK) == 0; boolean hasValue = hasValue(flags);
boolean isDeleted = (flags & IS_DELETED_MASK) != 0; boolean isDeleted = isDeleted(flags);
boolean isExpiring = (flags & IS_EXPIRING_MASK) != 0; boolean isExpiring = isExpiring(flags);
boolean useRowTimestamp = (flags & USE_ROW_TIMESTAMP_MASK) != 0; boolean useRowTimestamp = useRowTimestamp(flags);
boolean useRowTTL = (flags & USE_ROW_TTL_MASK) != 0; boolean useRowTTL = useRowTTL(flags);
if (!useRowTimestamp) if (!useRowTimestamp)
header.skipTimestamp(in); header.skipTimestamp(in);
@ -413,5 +413,30 @@ public abstract class Cell<V> extends ColumnData
return true; 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;
}
} }
} }

View File

@ -36,7 +36,7 @@ public abstract class Cells
private 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 cell the cell for which to collect stats.
* @param collector the stats collector. * @param collector the stats collector.

View File

@ -159,7 +159,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory
return DeletionTime.LIVE; return DeletionTime.LIVE;
DeletionTime biggestDeletionTime = openMarkers[biggestOpenMarker]; 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; return !biggestDeletionTime.supersedes(partitionDeletion) ? DeletionTime.LIVE : biggestDeletionTime;
} }
@ -172,7 +172,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory
continue; continue;
// Note that we can have boundaries that are both open and close, but in that case all we care about // 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)) if (marker.isOpen(reversed))
openMarkers[i] = marker.openDeletionTime(reversed); openMarkers[i] = marker.openDeletionTime(reversed);
else else
@ -192,7 +192,7 @@ public interface RangeTombstoneMarker extends Unfiltered, IMeasurableMemory
{ {
DeletionTime openMarker = currentOpenDeletionTimeInMerged(); 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 // 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; return openMarker.isLive() ? partitionDeletion : openMarker;
} }
} }

View File

@ -99,19 +99,19 @@ public class UnfilteredSerializer
/* /*
* Unfiltered flags constants. * Unfiltered flags constants.
*/ */
private final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a <flags> field with that flag. public final static int END_OF_PARTITION = 0x01; // Signal the end of the partition. Nothing follows a <flags> 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. public 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). public 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). public 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. public 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. public 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. public 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 EXTENSION_FLAG = 0x80; // If present, another byte is read containing the "extended flags" above.
/* /*
* Extended flags * 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 * 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 * previously deleted cell not updated by a subsequent update, SEE CASSANDRA-11500
@ -119,7 +119,7 @@ public class UnfilteredSerializer
* @deprecated See CASSANDRA-11500 * @deprecated See CASSANDRA-11500
*/ */
@Deprecated(since = "4.0") @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) public void serialize(Unfiltered unfiltered, SerializationHelper helper, DataOutputPlus out, int version)
throws IOException throws IOException
@ -220,14 +220,14 @@ public class UnfilteredSerializer
LivenessInfo pkLiveness = row.primaryKeyLivenessInfo(); LivenessInfo pkLiveness = row.primaryKeyLivenessInfo();
Row.Deletion deletion = row.deletion(); Row.Deletion deletion = row.deletion();
if ((flags & HAS_TIMESTAMP) != 0) if (hasTimestamp(flags))
header.writeTimestamp(pkLiveness.timestamp(), out); header.writeTimestamp(pkLiveness.timestamp(), out);
if ((flags & HAS_TTL) != 0) if (hasTTL(flags))
{ {
header.writeTTL(pkLiveness.ttl(), out); header.writeTTL(pkLiveness.ttl(), out);
header.writeLocalDeletionTime(pkLiveness.localExpirationTime(), out); header.writeLocalDeletionTime(pkLiveness.localExpirationTime(), out);
} }
if ((flags & HAS_DELETION) != 0) if (hasDeletion(flags))
header.writeDeletionTime(deletion.time(), out); header.writeDeletionTime(deletion.time(), out);
if ((flags & HAS_ALL_COLUMNS) == 0) if ((flags & HAS_ALL_COLUMNS) == 0)
@ -251,7 +251,7 @@ public class UnfilteredSerializer
if (cd.column.isSimple()) if (cd.column.isSimple())
Cell.serializer.serialize((Cell<?>) cd, column, out, pkLiveness, header); Cell.serializer.serialize((Cell<?>) cd, column, out, pkLiveness, header);
else 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) catch (IOException e)
{ {
@ -412,7 +412,7 @@ public class UnfilteredSerializer
public void writeEndOfPartition(DataOutputPlus out) throws IOException public void writeEndOfPartition(DataOutputPlus out) throws IOException
{ {
out.writeByte((byte)1); out.writeByte((byte)END_OF_PARTITION);
} }
public long serializedSizeEndOfPartition() public long serializedSizeEndOfPartition()
@ -502,12 +502,12 @@ public class UnfilteredSerializer
else else
{ {
assert !isStatic(extendedFlags); // deserializeStaticRow should be used for that. assert !isStatic(extendedFlags); // deserializeStaticRow should be used for that.
if ((flags & HAS_DELETION) != 0) if (hasDeletion(flags))
{ {
assert header.isForSSTable(); assert header.isForSSTable();
boolean hasTimestamp = (flags & HAS_TIMESTAMP) != 0; boolean hasTimestamp = hasTimestamp(flags);
boolean hasTTL = (flags & HAS_TTL) != 0; boolean hasTTL = hasTTL(flags);
boolean deletionIsShadowable = (extendedFlags & HAS_SHADOWABLE_DELETION) != 0; boolean deletionIsShadowable = deletionIsShadowable(extendedFlags);
Clustering<byte[]> clustering = Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes()); Clustering<byte[]> clustering = Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes());
long nextPosition = in.readUnsignedVInt() + in.getFilePointer(); long nextPosition = in.readUnsignedVInt() + in.getFilePointer();
in.readUnsignedVInt(); // skip previous unfiltered size in.readUnsignedVInt(); // skip previous unfiltered size
@ -572,12 +572,12 @@ public class UnfilteredSerializer
try try
{ {
boolean isStatic = isStatic(extendedFlags); boolean isStatic = isStatic(extendedFlags);
boolean hasTimestamp = (flags & HAS_TIMESTAMP) != 0; boolean hasTimestamp = hasTimestamp(flags);
boolean hasTTL = (flags & HAS_TTL) != 0; boolean hasTTL = hasTTL(flags);
boolean hasDeletion = (flags & HAS_DELETION) != 0; boolean hasDeletion = hasDeletion(flags);
boolean deletionIsShadowable = (extendedFlags & HAS_SHADOWABLE_DELETION) != 0; boolean deletionIsShadowable = deletionIsShadowable(extendedFlags);
boolean hasComplexDeletion = (flags & HAS_COMPLEX_DELETION) != 0; boolean hasComplexDeletion = hasComplexDeletion(flags);
boolean hasAllColumns = (flags & HAS_ALL_COLUMNS) != 0; boolean hasAllColumns = hasAllColumns(flags);
Columns headerColumns = header.columns(isStatic); Columns headerColumns = header.columns(isStatic);
if (header.isForSSTable()) if (header.isForSSTable())
@ -734,7 +734,17 @@ public class UnfilteredSerializer
public static Unfiltered.Kind kind(int flags) 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) public static boolean isStatic(int extendedFlags)
@ -742,7 +752,12 @@ public class UnfilteredSerializer
return (extendedFlags & IS_STATIC) != 0; 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; return (flags & EXTENSION_FLAG) != 0;
} }
@ -756,4 +771,29 @@ public class UnfilteredSerializer
{ {
return row.isStatic() || row.deletion().isShadowable(); 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;
}
} }

View File

@ -21,7 +21,7 @@ abstract class ComparableObjectToken<C extends Comparable<C>> extends Token
{ {
private static final long serialVersionUID = 1L; 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) protected ComparableObjectToken(C token)
{ {

View File

@ -41,6 +41,19 @@ public interface IPartitioner
return DatabaseDescriptor.getPartitioner(); 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. * Transform key to object representation of the on-disk format.
* *

View File

@ -243,4 +243,38 @@ public class LocalPartitioner implements IPartitioner
{ {
return AccordBytesSplitter::new; 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;
}
} }

View File

@ -21,6 +21,7 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -35,6 +36,7 @@ import com.google.common.primitives.Longs;
import accord.primitives.Ranges; import accord.primitives.Ranges;
import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.PreHashedDecoratedKey; import org.apache.cassandra.db.PreHashedDecoratedKey;
import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.AbstractType;
@ -177,7 +179,7 @@ public class Murmur3Partitioner implements IPartitioner
{ {
static final long serialVersionUID = -5833580143318243006L; static final long serialVersionUID = -5833580143318243006L;
public final long token; long token;
public LongToken(long token) public LongToken(long token)
{ {
@ -320,6 +322,18 @@ public class Murmur3Partitioner implements IPartitioner
return new LongToken(normalize(hash[0])); 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 @Override
public boolean isFixedLength() public boolean isFixedLength()
{ {
@ -386,10 +400,15 @@ public class Murmur3Partitioner implements IPartitioner
private long[] getHash(ByteBuffer key) private long[] getHash(ByteBuffer key)
{ {
long[] hash = new long[2]; long[] hash = new long[2];
MurmurHash.hash3_x64_128(key, key.position(), key.remaining(), 0, hash); populateHash(key, hash);
return hash; return hash;
} }
private void populateHash(ByteBuffer key, long[] hash)
{
MurmurHash.hash3_x64_128(key, key.position(), key.remaining(), 0, hash);
}
public LongToken getRandomToken() public LongToken getRandomToken()
{ {
return getRandomToken(ThreadLocalRandom.current()); return getRandomToken(ThreadLocalRandom.current());
@ -565,4 +584,79 @@ public class Murmur3Partitioner implements IPartitioner
{ {
return ignore -> splitter; 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;
}
} }

View File

@ -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);
}
}

View File

@ -500,7 +500,7 @@ public abstract class AbstractSSTableIterator<RIE extends AbstractRowIndexEntry>
{ {
// We use a same reasoning as in handlePreSliceData regarding the strictness of the inequality below. // 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 // 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 // (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 // 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 // our slice end, but in that case we will properly close the range tombstone anyway as part of our "close

View File

@ -47,7 +47,7 @@ import org.apache.cassandra.service.ActiveRepairService;
/** /**
* Base class for the sstable writers used by CQLSSTableWriter. * 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 File directory;
protected final TableMetadataRef metadata; 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)); return getUpdateFor(metadata.get().partitioner.decorateKey(key));
} }

View File

@ -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<AbstractType<?>> 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);
}
}
}

View File

@ -56,6 +56,12 @@ public class EmptySSTableScanner extends AbstractUnfilteredPartitionIterator imp
return ImmutableSet.of(sstable); return ImmutableSet.of(sstable);
} }
@Override
public boolean isFullRange()
{
return false;
}
public long getCurrentPosition() public long getCurrentPosition()
{ {
return 0; return 0;

View File

@ -39,6 +39,7 @@ public interface ISSTableScanner extends UnfilteredPartitionIterator
public long getCurrentPosition(); public long getCurrentPosition();
public long getBytesScanned(); public long getBytesScanned();
public Set<SSTableReader> getBackingSSTables(); public Set<SSTableReader> getBackingSSTables();
public boolean isFullRange();
public static void closeAllAndPropagate(Collection<ISSTableScanner> scanners, Throwable throwable) public static void closeAllAndPropagate(Collection<ISSTableScanner> scanners, Throwable throwable)
{ {

View File

@ -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.sstable.format.big.RowIndexEntry;
import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.ObjectSizes;
/** /**
@ -87,6 +88,17 @@ public class IndexInfo
return new IndexInfo.Serializer(version, header.clusteringTypes()); 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<IndexInfo> public static class Serializer implements ISerializer<IndexInfo>
{ {
// This is the default index size that we use to delta-encode width when serializing so we get better vint-encoding. // This is the default index size that we use to delta-encode width when serializing so we get better vint-encoding.

View File

@ -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:
* <pre>
* struct partition_header header {
* be16 key_length; // e.g. 8 if long
* byte key[key_length];
* struct deletion_time deletion_time {
* be32 local_deletion_time;
* be64 marked_for_delete_at;
* };
* };
* </pre>
*/
void load(RandomAccessReader dataReader, DeletionTime.Serializer serializer) throws IOException
{
position = dataReader.getPosition();
boolean resize = loadShortLength(dataReader);
if (!resize)
{
decoratedKey.shadowKey(length());
}
else
{
decoratedKey.shadowKey(buffer(), bytes(), length());
}
serializer.deserialize(dataReader, deletionTime);
}
public long position()
{
return position;
}
public DeletionTime deletionTime()
{
return deletionTime;
}
public DecoratedKey key()
{
return decoratedKey;
}
public ByteBuffer keyBuffer() {
return super.buffer();
}
public int keyLength() {
return super.length();
}
public byte[] keyBytes() {
return super.bytes();
}
public void resetPartition()
{
resetBuffer();
decoratedKey.reset();
deletionTime.resetLive();
position = 0;
}
@Override
public String toString()
{
return "PartitionDescriptor{ key=" + decoratedKey +
", position=" + position +
", deletionTime=" + deletionTime +
'}';
}
}

View File

@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.annotation.concurrent.NotThreadSafe;
import org.apache.cassandra.io.util.ResizableByteBuffer;
import org.apache.cassandra.io.util.FileHandle;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.io.util.RandomAccessReader;
import org.apache.cassandra.utils.Throwables;
@NotThreadSafe
public class SSTableCursorKeyReader implements AutoCloseable
{
private final FileHandle indexFile;
private final RandomAccessReader indexFileReader;
private final long initialPosition;
public static class Entry extends ResizableByteBuffer
{
private long dataPosition = -1;
private long keyPosition = -1;
public void load(RandomAccessReader indexReader) throws IOException
{
keyPosition = indexReader.getFilePointer();
super.loadShortLength(indexReader);
if (length() != 0)
{
dataPosition = indexReader.readUnsignedVInt();
// skip row index entries
int size = indexReader.readUnsignedVInt32();
if (size > 0)
indexReader.skipBytesFully(size);
}
else
{
dataPosition = -1;
}
}
public long dataPosition()
{
return dataPosition;
}
public long keyPosition()
{
return keyPosition;
}
public ByteBuffer getKey()
{
return buffer();
}
}
private SSTableCursorKeyReader(FileHandle indexFile,
RandomAccessReader indexFileReader)
{
this.indexFile = indexFile;
this.indexFileReader = indexFileReader;
this.initialPosition = indexFileReader.getFilePointer();
}
public static SSTableCursorKeyReader create(RandomAccessReader indexFileReader) throws IOException
{
return new SSTableCursorKeyReader(null, indexFileReader);
}
@SuppressWarnings({ "resource", "RedundantSuppression" }) // iFile and reader are closed in the BigTableKeyReader#close method
public static SSTableCursorKeyReader create(FileHandle indexFile) throws IOException
{
FileHandle iFile = null;
RandomAccessReader reader = null;
try
{
iFile = indexFile.sharedCopy();
reader = iFile.createReader();
return new SSTableCursorKeyReader(iFile, reader);
}
catch (RuntimeException ex)
{
Throwables.closeNonNullAndAddSuppressed(ex, reader, iFile);
throw ex;
}
}
@Override
public void close()
{
FileUtils.closeQuietly(indexFileReader);
FileUtils.closeQuietly(indexFile);
}
public boolean advance(Entry entry) throws IOException
{
if (indexFileReader.isEOF())
{
return false;
}
entry.load(indexFileReader);
return true;
}
public boolean isExhausted()
{
return indexFileReader.isEOF();
}
public long indexPosition()
{
return indexFileReader.getFilePointer();
}
public void seek(long position) throws IOException
{
if (position > indexLength())
throw new IndexOutOfBoundsException("The requested position exceeds the index length");
indexFileReader.seek(position);
}
public long indexLength()
{
return indexFileReader.length();
}
public void reset() throws IOException
{
indexFileReader.seek(initialPosition);
}
@Override
public String toString()
{
return String.format("BigTable-SSTableCursorKeyReader(%s), indexPosition=%d", indexFile.path(), indexFileReader.getFilePointer());
}
}

View File

@ -0,0 +1,891 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.IOException;
import com.google.common.collect.ImmutableList;
import org.apache.cassandra.db.ReusableLivenessInfo;
import org.apache.cassandra.io.util.ResizableByteBuffer;
import org.apache.cassandra.db.ClusteringPrefix;
import org.apache.cassandra.db.Columns;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.LivenessInfo;
import org.apache.cassandra.db.SerializationHeader;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.db.rows.RangeTombstoneMarker;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.SerializationHelper;
import org.apache.cassandra.db.rows.UnfilteredSerializer;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.Version;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.RandomAccessReader;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableMetadataRef;
import org.apache.cassandra.tools.Util;
import org.apache.cassandra.utils.concurrent.Ref;
import static org.apache.cassandra.io.sstable.SSTableCursorReader.State.*;
public class SSTableCursorReader implements AutoCloseable
{
private static final ColumnMetadata[] COLUMN_METADATA_TYPE = new ColumnMetadata[0];
private final boolean hasStaticColumns;
public interface State
{
/** start of file, after partition end but before EOF */
int PARTITION_START = 1;
int STATIC_ROW_START = 1 << 1;
int ROW_START = 1 << 2;
/** common to row/static row cells */
int CELL_HEADER_START = 1 << 3;
int CELL_VALUE_START = 1 << 4;
int CELL_END = 1 << 5;
int TOMBSTONE_START = 1 << 6;
/** common to rows/tombstones. Call continue(); for next unfiltered, or maybe partition end */
int UNFILTERED_END = 1 << 7;
/** at {@link UnfilteredSerializer#isEndOfPartition(int)} */
int PARTITION_END = 1 << 8;
/** EOF */
int DONE = 1 << 9;
/* Special case for seeking in file */
int SEEK = 1 << 10;
static boolean isState(int state, int mask) {
return (state & mask) != 0;
}
}
public class CellCursor {
public ReusableLivenessInfo rowLiveness;
public Columns columns;
public int columnsSize;
public int columnsIndex;
public int cellFlags;
public final ReusableLivenessInfo cellLiveness = new ReusableLivenessInfo();
public CellPath cellPath;
public AbstractType<?> cellType;
public ColumnMetadata cellColumn;
private ColumnMetadata[] columnsArray;
private AbstractType<?>[] cellTypeArray;
void init (Columns columns, ReusableLivenessInfo rowLiveness)
{
if (this.columns != columns)
{
// This will be a problem with changing columns
this.columns = columns;
columnsArray = columns.toArray(COLUMN_METADATA_TYPE);
cellTypeArray = new AbstractType<?>[columnsArray.length];
for (int i = 0; i < columnsArray.length; i++)
{
ColumnMetadata cellColumn = columnsArray[i];
cellTypeArray[i] = serializationHeader.getType(cellColumn);
}
columnsSize = columns.size();
}
this.rowLiveness = rowLiveness;
columnsIndex = 0;
cellFlags = 0;
cellPath = null;
cellType = null;
}
public boolean hasNext()
{
return columnsIndex < columnsSize;
}
/**
* For Cell deserialization see {@link Cell.Serializer#deserialize}
*
* @return true if the cell has a value, false otherwise
*/
boolean readCellHeader() throws IOException
{
if (!(columnsIndex < columnsSize)) throw new IllegalStateException();
// HOTSPOT: suprisingly expensive
int currIndex = columnsIndex++;
cellColumn = columnsArray[currIndex];
cellType = cellTypeArray[currIndex];
cellFlags = dataReader.readUnsignedByte();
// TODO: specialize common case where flags == HAS_VALUE | USE_ROW_TS?
boolean hasValue = Cell.Serializer.hasValue(cellFlags);
boolean isDeleted = Cell.Serializer.isDeleted(cellFlags);
boolean isExpiring = Cell.Serializer.isExpiring(cellFlags);
boolean useRowTimestamp = Cell.Serializer.useRowTimestamp(cellFlags);
boolean useRowTTL = Cell.Serializer.useRowTTL(cellFlags);
long timestamp = useRowTimestamp ? rowLiveness.timestamp() : serializationHeader.readTimestamp(dataReader);
long localDeletionTime = useRowTTL
? rowLiveness.localExpirationTime()
: (isDeleted || isExpiring ? serializationHeader.readLocalDeletionTime(dataReader) : Cell.NO_DELETION_TIME);
int ttl = useRowTTL ? rowLiveness.ttl() : (isExpiring ? serializationHeader.readTTL(dataReader) : Cell.NO_TTL);
localDeletionTime = Cell.decodeLocalDeletionTime(localDeletionTime, ttl, deserializationHelper);
cellLiveness.reset(timestamp, ttl, localDeletionTime);
cellPath = cellColumn.isComplex()
? cellColumn.cellPathSerializer().deserialize(dataReader)
: null;
return hasValue;
}
}
private final Ref<SSTableReader> ssTableReaderRef;
private final AbstractType<?>[] clusteringColumnTypes;
private final DeserializationHelper deserializationHelper;
private final SerializationHeader serializationHeader;
// need to be closed
private final SSTableReader ssTableReader;
private final RandomAccessReader dataReader;
private final DeletionTime.Serializer deletionTimeSerializer;
private final CellCursor staticRowCellCursor = new CellCursor();
private final CellCursor rowCellCursor = new CellCursor();
private CellCursor cellCursor;
// SHARED STATIC_ROW/ROW/TOMB
private int basicUnfilteredFlags = 0;
private int extendedFlags = 0;
private int state = PARTITION_START;
public static SSTableCursorReader fromDescriptor(Descriptor desc) throws IOException
{
TableMetadata metadata = Util.metadataFromSSTable(desc);
SSTableReader reader = SSTableReader.openNoValidation(null, desc, TableMetadataRef.forOfflineTools(metadata));
return new SSTableCursorReader(reader, metadata, reader.ref());
}
public SSTableCursorReader(SSTableReader reader)
{
this(reader, reader.metadata(), null);
}
private SSTableCursorReader(SSTableReader reader, TableMetadata metadata, Ref<SSTableReader> readerRef)
{
ssTableReader = reader;
ssTableReaderRef = readerRef;
Version version = reader.descriptor.version;
deletionTimeSerializer = DeletionTime.getSerializer(version);
ImmutableList<ColumnMetadata> clusteringColumns = metadata.clusteringColumns();
int clusteringColumnCount = clusteringColumns.size();
clusteringColumnTypes = new AbstractType<?>[clusteringColumnCount];
for (int i = 0; i < clusteringColumnTypes.length; i++)
{
clusteringColumnTypes[i] = clusteringColumns.get(i).type;
}
deserializationHelper = new DeserializationHelper(metadata, version.correspondingMessagingVersion(), DeserializationHelper.Flag.LOCAL, null);
serializationHeader = reader.header;
dataReader = reader.openDataReader();
hasStaticColumns = metadata.hasStaticColumns();
}
@Override
public void close()
{
dataReader.close();
if (ssTableReaderRef != null)
ssTableReaderRef.close();
}
private void resetOnPartitionStart()
{
basicUnfilteredFlags = 0;
extendedFlags = 0;
}
public int seekPartition(long position)
{
state = SEEK;
if (position == 0)
{
dataReader.seek(position);
state = PARTITION_START;
}
else {
// verify partition start is after a partition end marker
dataReader.seek(position - 1);
try
{
basicUnfilteredFlags = dataReader.readUnsignedByte();
}
catch (Exception e)
{
return corruptSSTable(e);
}
// end of partition
if (!UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags)) {
throw new IllegalArgumentException("Seeking to a partition at: " + position + " did not result in a valid state");
}
state = dataReader.isEOF() ? DONE : PARTITION_START;
}
resetOnPartitionStart();
return state;
}
public int seekUnfiltered(long position)
{
state = SEEK;
// partition elements (Unfiltered) have flags
dataReader.seek(position);
int state = 0;
try
{
state = checkNextFlagsAfterStaticRowOrUnfilteredStart(false);
}
catch (IOException e)
{
return corruptSSTable(e);
}
if (!isState(state , ROW_START | TOMBSTONE_START | DONE)) throw new IllegalStateException();
return state;
}
// struct partition {
// struct partition_header header
// optional<struct row> row
// struct unfiltered unfiltereds[];
//};
public int readPartitionHeader(PartitionDescriptor header)
{
if (state != PARTITION_START) throw new IllegalStateException();
resetOnPartitionStart();
try
{
header.load(dataReader, deletionTimeSerializer);
return checkNextFlagsAfterPartitionStart(false);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
// struct static_row {
// byte flags; // preloaded
// byte extended_flags; // preloaded
// varint row_body_size;
// varint prev_unfiltered_size; // for backward traversing, ignored
// optional<struct liveness_info> liveness_info;
// optional<struct delta_deletion_time> deletion_time;
// *** We read the columns in a separate method ***
// optional<varint[]> missing_columns;
// cell[] cells; // potentially only some
//};
public int readStaticRowHeader(UnfilteredDescriptor unfilteredDescriptor)
{
if (state != STATIC_ROW_START) throw new IllegalStateException();
try
{
unfilteredDescriptor.loadStaticRow(dataReader, serializationHeader, deserializationHelper, basicUnfilteredFlags, extendedFlags);
}
catch (IOException e)
{
return corruptSSTable(e);
}
staticRowCellCursor.init(unfilteredDescriptor.rowColumns(), unfilteredDescriptor.livenessInfo());
cellCursor = staticRowCellCursor;
if (!staticRowCellCursor.hasNext())
{
try
{
return checkNextFlagsAfterStaticRowOrUnfilteredStart(false);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
else
{
return state = State.CELL_HEADER_START;
}
}
public int copyCellValue(DataOutputPlus writer, byte[] buffer) throws IOException
{
if (state != CELL_VALUE_START) throw new IllegalStateException();
if (cellCursor.cellType == null) throw new IllegalStateException();
int length = cellCursor.cellType.valueLengthIfFixed();
copyCellContents(writer, buffer, length);
try
{
if (!cellCursor.hasNext())
{
try
{
return checkNextFlagsAfterCellValuesEnd();
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
return state = State.CELL_END;
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
// TODO: move to cell cursor? maybe avoid copy through buffer?
private void copyCellContents(DataOutputPlus writer, byte[] transferBuffer, int length) throws IOException
{
if (length >= 0)
{
try
{
dataReader.readFully(transferBuffer, 0, length);
}
catch (Exception e)
{
corruptSSTable(e);
}
writer.write(transferBuffer, 0, length);
}
else
{
try
{
length = dataReader.readUnsignedVInt32();
}
catch (IOException e)
{
corruptSSTable(e);
}
if (length < 0)
corruptSSTable("Corrupt (negative) value length encountered");
writer.writeUnsignedVInt32(length);
int remaining = length;
while (remaining > 0)
{
int readLength = Math.min(remaining, transferBuffer.length);
try
{
dataReader.readFully(transferBuffer, 0, readLength);
}
catch (Exception e)
{
corruptSSTable(e);
}
writer.write(transferBuffer, 0, readLength);
remaining -= readLength;
}
}
}
// struct row {
// byte flags;
// optional<struct clustering_block[]> clustering_blocks;
// varint row_body_size;
// varint prev_unfiltered_size; // for backward traversing, ignored
// optional<struct liveness_info> liveness_info;
// optional<struct delta_deletion_time> deletion_time;
// *** We read the columns in a separate step ***
// optional<varint[]> missing_columns;
// cell[] cells; // potentially only some
//};
public int readRowHeader(UnfilteredDescriptor unfilteredDescriptor)
{
if (state != State.ROW_START) throw new IllegalStateException();
if (!UnfilteredSerializer.isRow(basicUnfilteredFlags)) throw new IllegalStateException();
try
{
unfilteredDescriptor.loadRow(dataReader, serializationHeader, deserializationHelper, basicUnfilteredFlags);
rowCellCursor.init(unfilteredDescriptor.rowColumns(), unfilteredDescriptor.livenessInfo());
cellCursor = rowCellCursor;
if (!rowCellCursor.hasNext())
{
return checkNextFlagsAfterStaticRowOrUnfilteredStart(false);
}
else
{
return state = State.CELL_HEADER_START;
}
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
// TODO: introduce cell header class
public int readCellHeader()
{
if (state != State.CELL_HEADER_START) throw new IllegalStateException();
try
{
if (cellCursor.readCellHeader())
{
return state = State.CELL_VALUE_START;
}
if (!cellCursor.hasNext())
return checkNextFlagsAfterCellValuesEnd();
return state = State.CELL_END;
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
public int skipCellValue()
{
if (state != State.CELL_VALUE_START) throw new IllegalStateException();
try
{
cellCursor.cellType.skipValue(dataReader);
return !cellCursor.hasNext() ? checkNextFlagsAfterCellValuesEnd() : (state = State.CELL_HEADER_START);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
/**
* See: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serialize(RangeTombstoneMarker, SerializationHelper, DataOutputPlus, long, int)}
* <pre>
* struct range_tombstone_marker {
* byte flags = IS_MARKER;
* byte kind_ordinal;
* be16 bound_values_count;
* struct clustering_block[] clustering_blocks;
* varint marker_body_size;
* varint prev_unfiltered_size;
* };
* struct range_tombstone_bound_marker : range_tombstone_marker {
* struct delta_deletion_time deletion_time;
* };
* struct range_tombstone_boundary_marker : range_tombstone_marker {
* struct delta_deletion_time end_deletion_time;
* struct delta_deletion_time start_deletion_time;
* };
* </pre>
*
*/
public int readTombstoneMarker(UnfilteredDescriptor unfilteredDescriptor)
{
try
{
if (state != TOMBSTONE_START) throw new IllegalStateException();
if (!UnfilteredSerializer.isTombstoneMarker(basicUnfilteredFlags)) throw new IllegalStateException();
unfilteredDescriptor.loadTombstone(dataReader, serializationHeader, basicUnfilteredFlags);
return checkNextFlagsAfterStaticRowOrUnfilteredStart(false);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
/**
* {@link ClusteringPrefix.Serializer#deserializeValuesWithoutSize}
*/
static void readUnfilteredClustering(RandomAccessReader dataReader, AbstractType<?>[] types, int clusteringColumnsBound, ResizableByteBuffer clustering) throws IOException
{
clustering.resetBuffer();
if (clusteringColumnsBound == 0) {
return;
}
long clusteringBlockHeader = 0;
int fixedLengthClusteringLength = 0;
for (int clusteringIndex = 0; clusteringIndex < clusteringColumnsBound; clusteringIndex++)
{
// struct clustering_block {
// varint clustering_block_header;
// simple_cell[] clustering_cells;
// };
if (clusteringIndex % 32 == 0)
{
if (fixedLengthClusteringLength != 0) {
clustering.loadPart(dataReader, fixedLengthClusteringLength);
fixedLengthClusteringLength = 0;
}
clusteringBlockHeader = dataReader.readUnsignedVInt();
clustering.writeUnsignedVInt(clusteringBlockHeader);
}
// load value if present
if ((clusteringBlockHeader & 0b11) == 0)
{
AbstractType<?> type = types[clusteringIndex];
if (type.isValueLengthFixed())
{
fixedLengthClusteringLength += type.valueLengthIfFixed();
}
else
{
if (fixedLengthClusteringLength != 0) {
clustering.loadPart(dataReader, fixedLengthClusteringLength);
fixedLengthClusteringLength = 0;
}
int varLength = dataReader.readUnsignedVInt32();
clustering.writeUnsignedVInt(varLength);
clustering.loadPart(dataReader, varLength);
}
}
clusteringBlockHeader = clusteringBlockHeader >>> 2;
}
if (fixedLengthClusteringLength != 0) clustering.loadPart(dataReader, fixedLengthClusteringLength);
if (clusteringBlockHeader != 0) {
throw new IOException("Clustering block upper bits (those not associated with keys) expected to be 0:" + clusteringBlockHeader);
}
}
private static void skipClustering(RandomAccessReader dataReader, AbstractType<?>[] types, int clusteringColumnsBound) throws IOException
{
long clusteringBlockHeader = 0;
for (int clusteringIndex = 0; clusteringIndex < clusteringColumnsBound; clusteringIndex++)
{
// struct clustering_block {
// varint clustering_block_header;
// simple_cell[] clustering_cells;
// };
if (clusteringIndex % 32 == 0)
{
clusteringBlockHeader = dataReader.readUnsignedVInt();
}
// skip value if present
if ((clusteringBlockHeader & 0b11) == 0)
{
AbstractType<?> type = types[clusteringIndex];
int len = type.isValueLengthFixed() ? type.valueLengthIfFixed() : dataReader.readUnsignedVInt32();
dataReader.skipBytes(len);
}
clusteringBlockHeader = clusteringBlockHeader >>> 2;
}
if (clusteringBlockHeader != 0) {
throw new IOException("Clustering block upper bits (those not associated with keys) expected to be 0:" + clusteringBlockHeader);
}
}
/**
* {@link UnfilteredSerializer#deserializeRowBody(DataInputPlus, SerializationHeader, DeserializationHelper, int, int, Row.Builder)}
*/
static void readLivenessInfo(RandomAccessReader dataReader, SerializationHeader serializationHeader, DeserializationHelper deserializationHelper, int flags, ReusableLivenessInfo livenessInfo) throws IOException
{
long timestamp = LivenessInfo.NO_TIMESTAMP;
int ttl = LivenessInfo.NO_TTL;
long localExpirationTime = LivenessInfo.NO_EXPIRATION_TIME;
if (UnfilteredSerializer.hasTimestamp(flags))
{
// struct liveness_info {
// varint64 delta_timestamp;
// optional<varint32> delta_ttl;
// optional<varint64> delta_local_deletion_time;
//};
timestamp = serializationHeader.readTimestamp(dataReader);
if (UnfilteredSerializer.hasTTL(flags))
{
ttl = serializationHeader.readTTL(dataReader);
localExpirationTime = Cell.decodeLocalDeletionTime(serializationHeader.readLocalDeletionTime(dataReader), ttl, deserializationHelper);
}
}
livenessInfo.reset(timestamp, ttl, localExpirationTime);
}
// SKIPPING
public int skipPartition()
{
if (state == PARTITION_END)
return continueReading();
if (state == PARTITION_START)
{
try
{
int partitionKeyLength = dataReader.readUnsignedShort();
dataReader.skipBytes(partitionKeyLength);
// PARTITION DELETION TIME
deletionTimeSerializer.skip(dataReader);
checkNextFlagsAfterPartitionStart(true);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
else if (!isState(state, STATIC_ROW_START | ROW_START | TOMBSTONE_START | PARTITION_END))
{
throw new IllegalStateException("Unexpected state: " + state);
}
while (!isState(state,PARTITION_START | DONE))
{
switch (state)
{
case STATIC_ROW_START:
state = skipStaticRow(true);
break;
case ROW_START:
case TOMBSTONE_START:
state = skipUnfiltered(true);
break;
}
}
return state;
}
public int skipStaticRow(boolean autoContinue)
{
if (state != State.STATIC_ROW_START) throw new IllegalStateException();
try
{
long rowSize = dataReader.readUnsignedVInt();
dataReader.seek(dataReader.getPosition() + rowSize);
return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue);
}
catch (IOException e)
{
return corruptSSTable(e);
}
}
public int skipUnfiltered(boolean autoContinue)
{
if (!isState(state, ROW_START | TOMBSTONE_START))
throw new IllegalStateException();
AbstractType<?>[] types = clusteringColumnTypes;
int clusteringColumnsBound = types.length;
// tombstone markers have `kind` & `clusteringColumnsBound`
try
{
if (!UnfilteredSerializer.isRow(basicUnfilteredFlags))
{
dataReader.readByte();// byte kind =
clusteringColumnsBound = dataReader.readUnsignedShort();
}
/**
* {@link org.apache.cassandra.db.ClusteringPrefix.Deserializer}
*/
skipClustering(dataReader, types, clusteringColumnsBound);
// same for row/tombstone
long rowSize = dataReader.readUnsignedVInt();
dataReader.seek(dataReader.getPosition() + rowSize);
return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue);
}
catch (Exception e)
{
return corruptSSTable(e);
}
}
public int skipRowCells(long unfilteredDataStart, long unfilteredSize, boolean autoContinue)
{
if (!(isState(state,CELL_HEADER_START | CELL_VALUE_START | CELL_END))) throw new IllegalStateException();
try
{
dataReader.seek(unfilteredDataStart + unfilteredSize);
return checkNextFlagsAfterStaticRowOrUnfilteredStart(autoContinue);
}
catch (IOException e)
{
return corruptSSTable(e);
}
}
public int continueReading() {
// TODO: can be optimized by pre-calculating next state when the flags are read
switch (state)
{
case PARTITION_END:
state = dataReader.isEOF() ? DONE : PARTITION_START;
break;
case UNFILTERED_END:
if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags))
{
state = PARTITION_END;
}
else
{
state = UnfilteredSerializer.isRow(basicUnfilteredFlags) ? ROW_START : TOMBSTONE_START;
}
break;
case CELL_END:
if (cellCursor.hasNext())
{
state = CELL_HEADER_START;
}
else
{
state = UNFILTERED_END;
}
break;
default:
throw new IllegalStateException("Cannot continue reading in current state: " + state);
}
return state;
}
private int checkNextFlagsAfterPartitionStart(boolean autoContinue) throws IOException
{
long preFlagsPosition = dataReader.getPosition();
basicUnfilteredFlags = dataReader.readUnsignedByte();
if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags))
{
state = !autoContinue ? PARTITION_END :
dataReader.isEOF() ? DONE : PARTITION_START;
}
else if (UnfilteredSerializer.isExtended(basicUnfilteredFlags))
{
state = STATIC_ROW_START;
extendedFlags = dataReader.readUnsignedByte();
validateStaticRowFlags(preFlagsPosition);
}
else
{
state = UnfilteredSerializer.isRow(basicUnfilteredFlags) ? ROW_START : TOMBSTONE_START;
}
return state;
}
private void validateStaticRowFlags(long preFlagsPosition)
{
if (!UnfilteredSerializer.isStatic(extendedFlags))
{
corruptSSTable("Row at: " + preFlagsPosition + " has extended flags but is not static, extendedFlags: " + extendedFlags);
}
if (!UnfilteredSerializer.isRow(basicUnfilteredFlags))
{
corruptSSTable("Static row at: " + preFlagsPosition + " is not a row, flags: " + basicUnfilteredFlags);
}
if (!hasStaticColumns)
{
corruptSSTable("Row at: " + preFlagsPosition + " is static, but table has no static columns " + ssTableReader.metadata());
}
if (UnfilteredSerializer.deletionIsShadowable(extendedFlags))
{
throw new UnsupportedOperationException("Static row at: " + preFlagsPosition + " has deletionIsShadowable, which is deprecated since 4.0");
}
}
private int checkNextFlagsAfterStaticRowOrUnfilteredStart(boolean autoContinue) throws IOException
{
int flags = this.basicUnfilteredFlags = dataReader.readUnsignedByte();
if (UnfilteredSerializer.isExtended(flags))
{
corruptSSTable("Unexpected static row (flags=" + flags + ") mid-partition, at position: " + (dataReader.getPosition() - 1));
}
if (!autoContinue) {
return this.state = UNFILTERED_END;
}
else
{
return this.state = nextStateMidPartition(flags);
}
}
private int checkNextFlagsAfterCellValuesEnd() throws IOException
{
int flags = this.basicUnfilteredFlags = dataReader.readUnsignedByte();
if (UnfilteredSerializer.isExtended(flags))
{
corruptSSTable("Unexpected static row (flags=" + flags + ") mid-partition, at position: " + (dataReader.getPosition() - 1));
}
return this.state = CELL_END;
}
private int corruptSSTable(Exception e)
{
ssTableReader.markSuspect();
if (e instanceof CorruptSSTableException)
throw (CorruptSSTableException) e;
throw new CorruptSSTableException(e, ssTableReader.getFilename());
}
protected int corruptSSTable(String message)
{
return corruptSSTable(new IllegalStateException(message));
}
private int nextStateMidPartition(int basicUnfilteredFlags)
{
if (UnfilteredSerializer.isEndOfPartition(basicUnfilteredFlags))
{
return dataReader.isEOF() ? DONE : PARTITION_START;
}
else if (UnfilteredSerializer.isRow(basicUnfilteredFlags))
{
return ROW_START;
}
else
{
return TOMBSTONE_START;
}
}
public boolean isEOF() {
return state == DONE || dataReader.isEOF();
}
public int state()
{
return state;
}
public long position() {
return dataReader.getFilePointer();
}
public long uncompressedLength()
{
return ssTableReader.uncompressedLength();
}
public SSTableReader ssTableReader()
{
return ssTableReader;
}
public CellCursor cellCursor()
{
return cellCursor;
}
}

View File

@ -0,0 +1,689 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.primitives.Ints;
import org.agrona.collections.IntArrayList;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ClusteringPrefix;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime;
import org.apache.cassandra.db.LivenessInfo;
import org.apache.cassandra.db.ReusableLivenessInfo;
import org.apache.cassandra.db.SerializationHeader;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.Threshold;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.SerializationHelper;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredSerializer;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.io.FSWriteError;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SortedTableWriter;
import org.apache.cassandra.io.sstable.format.big.BigFormatPartitionWriter;
import org.apache.cassandra.io.sstable.format.big.BigTableWriter;
import org.apache.cassandra.io.sstable.format.big.RowIndexEntry;
import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.SequentialWriter;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.BloomFilter;
import org.apache.cassandra.utils.ByteArrayUtil;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.concurrent.Ref;
import static org.apache.cassandra.db.rows.UnfilteredSerializer.*;
public class SSTableCursorWriter implements AutoCloseable
{
private static final UnfilteredSerializer SERIALIZER = UnfilteredSerializer.serializer;
private static final ColumnMetadata[] EMPTY_COL_META = new ColumnMetadata[0];
private final SortedTableWriter<?,?> ssTableWriter;
private final SequentialWriter dataWriter;
private final SortedTableWriter.AbstractIndexWriter indexWriter;
private final DeletionTime.Serializer deletionTimeSerializer;
private final MetadataCollector metadataCollector;
private final SerializationHeader serializationHeader;
/**
* See: {@link BloomFilter#reusableIndexes}
*/
private final long[] reusableIndexes = new long[21];
private final boolean hasStaticColumns;
private long partitionStart;
// ROW contents, needed because of the order of writing and the var int fields
private int rowFlags; // discovered as we go along
private int rowExtendedFlags;
private final DataOutputBuffer rowHeaderBuffer = new DataOutputBuffer(); // holds the contents between FLAGS and SIZE
private final DataOutputBuffer rowBuffer = new DataOutputBuffer();
private final ReusableDeletionTime openMarker = ReusableDeletionTime.live();
private final ColumnMetadata[] staticColumns;
private final ColumnMetadata[] regularColumns;
private final IntArrayList missingColumns = new IntArrayList();
private ColumnMetadata[] columns; // points to static/regular
private int columnsWrittenCount = 0;
private int nextCellIndex = 0;
// Index info
private final DataOutputBuffer rowIndexEntries = new DataOutputBuffer();
private final IntArrayList rowIndexEntriesOffsets = new IntArrayList();
private final ClusteringDescriptor rowIndexEntryLastClustering;
private int indexBlockStartOffset;
private int rowIndexEntryOffset;
private final int indexBlockThreshold;
private SSTableCursorWriter(
Descriptor desc,
SortedTableWriter<?,?> ssTableWriter,
SequentialWriter dataWriter,
SortedTableWriter.AbstractIndexWriter indexWriter,
MetadataCollector metadataCollector,
SerializationHeader serializationHeader)
{
this.ssTableWriter = ssTableWriter;
this.dataWriter = dataWriter;
this.indexWriter = indexWriter;
this.deletionTimeSerializer = DeletionTime.getSerializer(desc.version);
this.metadataCollector = metadataCollector;
this.serializationHeader = serializationHeader;
hasStaticColumns = serializationHeader.hasStatic();
staticColumns = hasStaticColumns ? serializationHeader.columns(true).toArray(EMPTY_COL_META) : EMPTY_COL_META;
regularColumns = serializationHeader.columns(false).toArray(EMPTY_COL_META);
this.indexBlockThreshold = DatabaseDescriptor.getColumnIndexSize(BigFormatPartitionWriter.DEFAULT_GRANULARITY);
rowIndexEntryLastClustering = new ClusteringDescriptor(serializationHeader.clusteringTypes().toArray(AbstractType[]::new));
}
public SSTableCursorWriter(SortedTableWriter<?,?> ssTableWriter)
{
this(ssTableWriter.descriptor,
ssTableWriter,
ssTableWriter.dataWriter,
ssTableWriter.indexWriter,
ssTableWriter.metadataCollector,
ssTableWriter.partitionWriter.getHeader());
}
@Override
public void close()
{
SSTableReader finish = ssTableWriter.finish(false);
if (finish != null) {
Ref<SSTableReader> ref = finish.ref();
if (ref != null) ref.close();
}
ssTableWriter.close();
}
public long getPartitionStart()
{
return partitionStart;
}
public long getPosition()
{
return dataWriter.position();
}
public int writePartitionStart(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime) throws IOException
{
rowIndexEntries.clear();
rowIndexEntriesOffsets.clear();
rowIndexEntryOffset = 0;
openMarker.resetLive();
partitionStart = dataWriter.position();
writePartitionHeader(partitionKey, partitionKeyLength, partitionDeletionTime);
updateIndexBlockStartOffset(dataWriter.position());
return indexBlockStartOffset;
}
public void writePartitionEnd(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime, int headerLength) throws IOException
{
SERIALIZER.writeEndOfPartition(dataWriter);
long partitionEnd = dataWriter.position();
long partitionSize = partitionEnd - partitionStart;
addPartitionMetadata(partitionKey, partitionKeyLength, partitionSize, partitionDeletionTime);
/** {@link SortedTableWriter#endPartition(DecoratedKey, DeletionTime)}
lastWrittenKey = key; // tracked for verification, see {@link SortedTableWriter#verifyPartition(DecoratedKey)}, checking the key size and sorting
// first/last are retained for metadata {@link org.apache.cassandra.io.sstable.format.SSTableWriter#finalizeMetadata()}. They are also exposed via
// getters from the writer, but usage is unclear.
last = lastWrittenKey;
if (first == null)
first = lastWrittenKey;
// this is implemented differently for BIG/BTI
createRowIndexEntry(key, partitionLevelDeletion, partitionEnd - 1);
*/
appendBIGIndex(partitionKey, partitionKeyLength, partitionStart, headerLength, partitionDeletionTime, partitionEnd);
}
private void appendBIGIndex(byte[] key, int keyLength, long partitionStart, int headerLength, DeletionTime partitionDeletionTime, long partitionEnd) throws IOException
{
/**
* {@link BigTableWriter#createRowIndexEntry(DecoratedKey, DeletionTime, long)}
* {@link BigTableWriter.IndexWriter#append(DecoratedKey, RowIndexEntry, long, ByteBuffer)}
*
*/
BigTableWriter.IndexWriter indexWriter = (BigTableWriter.IndexWriter) this.indexWriter;
SequentialWriter indexFileWriter = indexWriter.writer;
((BloomFilter)indexWriter.bf).add(key, 0, keyLength, reusableIndexes);
long indexStart = indexFileWriter.position();
try
{
ByteArrayUtil.writeWithShortLength(key, 0, keyLength, indexFileWriter);
indexFileWriter.writeUnsignedVInt(partitionStart);
// if the list of entries has one or fewer entries, no point in index entries.
/** See: {@link org.apache.cassandra.io.sstable.format.big.RowIndexEntry#create} */
if (rowIndexEntriesOffsets.size() <= 1)
{
/**
* {@link RowIndexEntry#serialize(DataOutputPlus, ByteBuffer)}
*/
indexFileWriter.writeUnsignedVInt32(0); // size
}
else {
// add last block
long indexBlockSize = (partitionEnd - partitionStart - 1) - indexBlockStartOffset;
if (indexBlockSize != 0) {
addIndexBlock(partitionEnd - 1, indexBlockSize);
}
// if we have intermeddiate index info elements we also need to serialize the partitionDeletionTime
/** {@link RowIndexEntry.IndexedEntry#serialize(DataOutputPlus, ByteBuffer) */
// size up to the offsets?
int endOfEntries = rowIndexEntries.getLength();
// Write the headerLength, partitionDeletionTime and rowIndexEntriesOffsets.size() after the entries,
// just to calculate size.
rowIndexEntries.writeUnsignedVInt((long)headerLength);
deletionTimeSerializer.serialize(partitionDeletionTime, rowIndexEntries);
rowIndexEntries.writeUnsignedVInt32(rowIndexEntriesOffsets.size()); // number of entries
// bytes until offsets
int entriesAndOffsetsSize = rowIndexEntries.getLength() + rowIndexEntriesOffsets.size() * 4;
assert entriesAndOffsetsSize > 0;
indexFileWriter.writeUnsignedVInt32(entriesAndOffsetsSize); // size != 0
// copy the header elements
indexFileWriter.write(rowIndexEntries.getData(), endOfEntries, rowIndexEntries.getLength() - endOfEntries);
indexFileWriter.write(rowIndexEntries.getData(), 0, endOfEntries);
for (int i = 0; i < rowIndexEntriesOffsets.size(); i++)
{
int offset = rowIndexEntriesOffsets.get(i);
indexFileWriter.writeInt(offset);
}
}
}
catch (IOException e)
{
throw new FSWriteError(e, indexFileWriter.getPath());
}
indexWriter.summary.maybeAddEntry(key, 0, keyLength, indexStart);
}
final long guardrailsPartitionSizeWarning = Guardrails.partitionSize.warnValue(null);
final long guardrailsPartitionTombstonesWarning = Guardrails.partitionTombstones.warnValue(null);
/**
* update metadata like {@link SortedTableWriter#endPartition} and {@link SortedTableWriter#startPartition}
*/
private void addPartitionMetadata(byte[] partitionKey, int partitionKeyLength, long partitionSize, DeletionTime partitionDeletionTime)
{
if (partitionSize > guardrailsPartitionSizeWarning)
guardPartitionThreshold(Guardrails.partitionSize, partitionKey, partitionKeyLength, partitionSize);
if (metadataCollector.totalTombstones > guardrailsPartitionTombstonesWarning)
guardPartitionThreshold(Guardrails.partitionTombstones, partitionKey, partitionKeyLength, metadataCollector.totalTombstones);
metadataCollector.updatePartitionDeletion(partitionDeletionTime);
metadataCollector.addPartitionSizeInBytes(partitionSize);
metadataCollector.addKey(partitionKey, 0, partitionKeyLength);
metadataCollector.addCellPerPartitionCount();
}
private void guardPartitionThreshold(Threshold guardrail, byte[] partitionKey, int partitionKeyLength, long size)
{
if (guardrail.triggersOn(size, null))
{
String message = String.format("%s.%s:%s on sstable %s",
ssTableWriter.metadata().keyspace,
ssTableWriter.metadata().name,
ssTableWriter.metadata().partitionKeyType.getString(ByteBuffer.wrap(partitionKey, 0, partitionKeyLength)),
ssTableWriter.getFilename());
guardrail.guard(size, message, true, null);
}
}
private void writePartitionHeader(byte[] partitionKey, int partitionKeyLength, DeletionTime partitionDeletionTime) throws IOException
{
dataWriter.writeShort(partitionKeyLength);
dataWriter.write(partitionKey, 0, partitionKeyLength);
deletionTimeSerializer.serialize(partitionDeletionTime, dataWriter);
}
public boolean writeEmptyStaticRow() throws IOException
{
if (!hasStaticColumns)
return false;
rowFlags = UnfilteredSerializer.EXTENSION_FLAG;
rowExtendedFlags = UnfilteredSerializer.IS_STATIC;
columns = staticColumns;
// TOD: we should be able to skip the use of the row buffers in this special case, maybe it doesn't matter
rowHeaderBuffer.clear();
// NOTE: if we are to write this value (which is not used), this is where we should compute it.
rowHeaderBuffer.writeUnsignedVInt32(0);
rowBuffer.clear();
columnsWrittenCount = 0;
missingColumns.clear();
writeRowEnd(null, false);
updateIndexBlockStartOffset(dataWriter.position());
return true;
}
public void writeRowStart(LivenessInfo livenessInfo, DeletionTime deletionTime, boolean isStatic) throws IOException
{
if (isStatic) {
rowFlags = UnfilteredSerializer.EXTENSION_FLAG;
rowExtendedFlags = UnfilteredSerializer.IS_STATIC;
columns = staticColumns;
}
else {
rowFlags = 0;
rowExtendedFlags = 0;
columns = regularColumns;
}
// NOTE: Data after this point needs a computed ahead of write size. This, combined with the cost of rewriting
// the size after the writing completes, means we have to buffer the row timestamps (most likely to differ in length)
// and the row columns data (will differ if they use their own timestamps, probably). Unfortunate.
// rest of header
rowHeaderBuffer.clear();
missingColumns.clear();
rowBuffer.clear();
columnsWrittenCount = 0;
nextCellIndex = 0;
// NOTE: if we are to write this value (which is not used), this is where we should compute it.
rowHeaderBuffer.writeUnsignedVInt32(0);
// copy TS/TTL/deletion data
rowFlags |= writeRowTimeData(livenessInfo, deletionTime, rowHeaderBuffer);
}
/**
* See {@link UnfilteredSerializer#serialize(Row, SerializationHelper, DataOutputPlus, long, int)}
*/
private int writeRowTimeData(LivenessInfo livenessInfo, DeletionTime deletionTime, DataOutputPlus writer) throws IOException
{
int flags = 0;
boolean writtenLivenessMetadata = false;
if (!livenessInfo.isEmpty())
{
flags |= HAS_TIMESTAMP;
serializationHeader.writeTimestamp(livenessInfo.timestamp(), writer);
metadataCollector.update(livenessInfo);
writtenLivenessMetadata = true;
}
if (livenessInfo.isExpiring())
{
flags |= HAS_TTL;
serializationHeader.writeTTL(livenessInfo.ttl(), writer);
serializationHeader.writeLocalDeletionTime(livenessInfo.localExpirationTime(), writer);
if (!writtenLivenessMetadata) metadataCollector.update(livenessInfo);
}
if (!deletionTime.isLive())
{
flags |= HAS_DELETION;
writeDeletionTime(deletionTime, writer);
}
/**
* Metadata calls matching: {@link org.apache.cassandra.db.rows.Rows#collectStats}
* But the collection of data is conditional and the cell metadata is collected elsewhere.
*/
return flags;
}
private void writeDeletionTime(DeletionTime deletionTime, DataOutputPlus writer) throws IOException
{
serializationHeader.writeDeletionTime(deletionTime, writer);
metadataCollector.update(deletionTime);
}
public void writeCellHeader(int cellFlags, ReusableLivenessInfo cellLiveness, ColumnMetadata cellColumn) throws IOException
{
for (; nextCellIndex < columns.length; nextCellIndex++) {
if (columns[nextCellIndex].compareTo(cellColumn) == 0)
break;
missingColumns.addInt(nextCellIndex);
}
if (nextCellIndex == columns.length)
throw new IllegalStateException("Column not found: " + cellColumn +" or cell writes out of order, or bug.");
nextCellIndex++;
writeCellHeader(cellFlags, cellLiveness, rowBuffer);
}
private void writeCellHeader(int cellFlags, ReusableLivenessInfo cellLiveness, DataOutputPlus writer) throws IOException
{
columnsWrittenCount++;
writer.writeByte(cellFlags);
if (!Cell.Serializer.useRowTimestamp(cellFlags)) {
long timestamp = cellLiveness.timestamp();
serializationHeader.writeTimestamp(timestamp, writer);
}
if (!Cell.Serializer.useRowTTL(cellFlags)) {
boolean isDeleted = Cell.Serializer.isDeleted(cellFlags);
boolean isExpiring = Cell.Serializer.isExpiring(cellFlags);
if (isDeleted || isExpiring) {
// TODO: is this conversion from LET to LDT correct?
serializationHeader.writeLocalDeletionTime(cellLiveness.localExpirationTime(), writer);
}
if (isExpiring) {
serializationHeader.writeTTL(cellLiveness.ttl(), writer);
}
}
/**
* matching {@link org.apache.cassandra.db.rows.Cells#collectStats};
*/
metadataCollector.updateCellLiveness(cellLiveness);
}
public int writeCellValue(SSTableCursorReader cursor, byte[] copyColumnValueBuffer) throws IOException
{
return cursor.copyCellValue(rowBuffer, copyColumnValueBuffer);
}
public void writeCellValue(DataOutputBuffer tempCellBuffer) throws IOException
{
rowBuffer.write(tempCellBuffer.getData(), 0, tempCellBuffer.getLength());
}
public void writeRowEnd(UnfilteredDescriptor rHeader, boolean updateClusteringMetadata) throws IOException
{
boolean isExtended = isExtended(rowFlags);
boolean isStatic = isExtended && UnfilteredSerializer.isStatic(rowExtendedFlags);
int columnsLength = columns.length;
if (columnsWrittenCount == columnsLength)
{
rowFlags |= HAS_ALL_COLUMNS;
}
else if (columnsWrittenCount == 0) {
// Same as Columns.serializer.serializeSubset(Columns.NONE, serializationHeader.columns(isStatic), rowHeaderBuffer)
if (columnsLength < 64) {
// all the bits are set, because all the columns are missing, value is always positive
rowHeaderBuffer.writeUnsignedVInt(-1L >>> (64 - columnsLength));
}
else {
// no columns are present, nothing to write
rowHeaderBuffer.writeUnsignedVInt32(columnsLength);
}
}
else if (columnsWrittenCount < columnsLength)
{
for (; nextCellIndex < columnsLength; nextCellIndex++)
missingColumns.addInt(nextCellIndex);
if (columnsLength < 64) {
// set a bit for every missing column
long mask = 0;
for (int missingIndex : missingColumns) {
mask |= (1L << missingIndex);
}
rowHeaderBuffer.writeUnsignedVInt(mask);
}
else {
encodeLargeColumnsSubset();
}
}
long unfilteredStartPosition = dataWriter.position();
/** See: {@link UnfilteredSerializer#serialize} */
dataWriter.writeByte(rowFlags);
if (isExtended)
{
dataWriter.writeByte(rowExtendedFlags);
}
if (!isStatic)
{
byte[] clustering = rHeader.clusteringBytes();
int clusteringLength = rHeader.clusteringLength();
dataWriter.write(clustering, 0, clusteringLength);
}
// Now that we know the size, write it + the rest of the data
dataWriter.writeUnsignedVInt32(rowHeaderBuffer.getLength() + rowBuffer.getLength());
dataWriter.write(rowHeaderBuffer.getData(), 0, rowHeaderBuffer.getLength());
dataWriter.write(rowBuffer.getData(), 0, rowBuffer.getLength());
long unfilteredEndPosition = getPosition();
/**
* Matching the: {@link org.apache.cassandra.db.rows.Rows#collectStats} along with above cell level metadata updates
*/
metadataCollector.updateColumnSetPerRow(columnsWrittenCount);
if (isStatic)
{
updateIndexBlockStartOffset(dataWriter.position());
}
else
{
updateMetadataAndIndexBlock(rHeader, unfilteredStartPosition, unfilteredEndPosition, updateClusteringMetadata);
}
}
/**
* See: {@link org.apache.cassandra.io.sstable.format.SortedTableWriter#addRangeTomstoneMarker}
*/
public void writeRangeTombstone(UnfilteredDescriptor rangeTombstone, boolean updateClusteringMetadata) throws IOException
{
int tombstoneKind = rangeTombstone.clusteringKindEncoded();
ClusteringPrefix.Kind kind = ClusteringPrefix.Kind.values()[tombstoneKind];
long unfilteredStartPosition = getPosition();
/** See: {@link org.apache.cassandra.db.rows.UnfilteredSerializer#serialize */
dataWriter.writeByte((byte)IS_MARKER);
/** See: {@link org.apache.cassandra.db.ClusteringBoundOrBoundary.Serializer#serialize} */
dataWriter.writeByte(tombstoneKind);
dataWriter.writeShort(rangeTombstone.clusteringColumnsBound());
int clusteringLength = rangeTombstone.clusteringLength();
if (clusteringLength != 0)
{
byte[] clustering = rangeTombstone.clusteringBytes();
dataWriter.write(clustering, 0, clusteringLength);
}
rowHeaderBuffer.clear();
// TODO: previousUnfilteredSize
rowHeaderBuffer.writeUnsignedVInt32(0);
if (kind.isBoundary())
{
writeDeletionTime(rangeTombstone.deletionTime(), rowHeaderBuffer);
writeDeletionTime(rangeTombstone.deletionTime2(), rowHeaderBuffer);
openMarker.reset(rangeTombstone.deletionTime2());
}
else
{
writeDeletionTime(rangeTombstone.deletionTime(), rowHeaderBuffer);
if (kind.isOpen(false))
openMarker.reset(rangeTombstone.deletionTime());
else
openMarker.resetLive();
}
dataWriter.writeUnsignedVInt32(rowHeaderBuffer.getLength());
dataWriter.write(rowHeaderBuffer.getData(), 0, rowHeaderBuffer.getLength());
long unfilteredEndPosition = getPosition();
/** {@link org.apache.cassandra.io.sstable.format.big.BigFormatPartitionWriter#addUnfiltered(Unfiltered)} */
// if we hit the index block size that we have to index after, go ahead and index it.
updateMetadataAndIndexBlock(rangeTombstone, unfilteredStartPosition, unfilteredEndPosition, updateClusteringMetadata);
}
private void updateMetadataAndIndexBlock(
UnfilteredDescriptor unfilteredDescriptor,
long unfilteredStartPosition,
long unfilteredEndPosition,
boolean updateClusteringMetadata) throws IOException
{
if (updateClusteringMetadata) updateClusteringMetadata(unfilteredDescriptor);
// write the first clustering into rowIndexEntries buffer (we will need it unless we never write the first entry)
if (unfilteredStartPosition == indexBlockStartOffset || (rowIndexEntryOffset == rowIndexEntries.position())) {
writeClusteringToRowIndexEntries(unfilteredDescriptor);
}
else
{
rowIndexEntryLastClustering.copy(unfilteredDescriptor);
}
/** {@link BigFormatPartitionWriter#addUnfiltered(Unfiltered)} */
// if we hit the index block size that we have to index after, go ahead and index it.
long indexBlockSize = currentOffsetInPartition(unfilteredEndPosition) - indexBlockStartOffset;
if (indexBlockSize >= this.indexBlockThreshold)
addIndexBlock(unfilteredEndPosition, indexBlockSize);
}
public void updateClusteringMetadata(UnfilteredDescriptor unfilteredDescriptor)
{
metadataCollector.updateClusteringValues(unfilteredDescriptor);
}
/**
* See:
* {@link BigFormatPartitionWriter#addIndexBlock()}
* - {@link org.apache.cassandra.io.sstable.IndexInfo.Serializer#serialize(org.apache.cassandra.io.sstable.IndexInfo, org.apache.cassandra.io.util.DataOutputPlus)}
*/
private void addIndexBlock(long endOfRowPosition, long indexBlockSize) throws IOException
{
if (rowIndexEntriesOffsets.isEmpty() && rowIndexEntryOffset != 0) {
throw new IllegalStateException();
}
// serialize the index info
/** {@link org.apache.cassandra.io.sstable.IndexInfo.Serializer#serialize(org.apache.cassandra.io.sstable.IndexInfo, org.apache.cassandra.io.util.DataOutputPlus)}*/
rowIndexEntriesOffsets.addInt(rowIndexEntryOffset);
// first clustering is already in, write last entry
if (rowIndexEntryLastClustering.clusteringLength() == 0) {
// first entry is the last entry, copy it
byte[] entriesData = rowIndexEntries.getData();
long endOfFirstEntry = rowIndexEntries.position();
rowIndexEntries.write(entriesData, rowIndexEntryOffset, (int) (endOfFirstEntry - rowIndexEntryOffset));
}
else
{
writeClusteringToRowIndexEntries(rowIndexEntryLastClustering);
rowIndexEntryLastClustering.resetClustering();
}
rowIndexEntries.writeUnsignedVInt((long)indexBlockStartOffset);
rowIndexEntries.writeVInt(indexBlockSize - IndexInfo.Serializer.WIDTH_BASE);
boolean isDeleteTimePresent = !openMarker.isLive();
rowIndexEntries.writeBoolean(isDeleteTimePresent);
if (isDeleteTimePresent)
deletionTimeSerializer.serialize(openMarker, rowIndexEntries);
// next block starts
rowIndexEntryOffset = Ints.checkedCast(rowIndexEntries.position());
updateIndexBlockStartOffset(endOfRowPosition);
}
private void updateIndexBlockStartOffset(long endOfRowPosition)
{
indexBlockStartOffset = (int) (endOfRowPosition - partitionStart);
}
private void writeClusteringToRowIndexEntries(ClusteringDescriptor clustering) throws IOException
{
ClusteringPrefix.Kind kind = clustering.clusteringKind();
rowIndexEntries.writeByte(kind.ordinal());
if (kind != ClusteringPrefix.Kind.CLUSTERING)
rowIndexEntries.writeShort(clustering.clusteringColumnsBound());
rowIndexEntries.write(clustering.clusteringBytes(), 0, clustering.clusteringLength());
}
private long currentOffsetInPartition(long position)
{
return position - partitionStart;
}
private void encodeLargeColumnsSubset() throws IOException
{
// no columns are present, nothing to write
rowHeaderBuffer.writeUnsignedVInt32(missingColumns.size());
if (missingColumns.size() > columns.length / 2)
{
// write present columns
int presentIndex = 0;
int missingIndex = 0;
for (int i = 0; i < missingColumns.size(); i++)
{
missingIndex = missingColumns.get(i);
for (; presentIndex < missingIndex; presentIndex++)
rowHeaderBuffer.writeUnsignedVInt32(presentIndex);
presentIndex = missingIndex + 1;
}
if (missingIndex < columns.length-1) {
for (; presentIndex < missingIndex; presentIndex++)
rowHeaderBuffer.writeUnsignedVInt32(presentIndex);
}
}
else
{
// write missing columns
for (int missingIndex : missingColumns) {
rowHeaderBuffer.writeUnsignedVInt32(missingIndex);
}
}
}
public void setLast(ByteBuffer key)
{
IPartitioner partitioner = ssTableWriter.getPartitioner();
DecoratedKey last = partitioner.decorateKey(ByteBufferUtil.clone(key));
ssTableWriter.setLast(last);
}
public void setFirst(ByteBuffer key)
{
IPartitioner partitioner = ssTableWriter.getPartitioner();
DecoratedKey first = partitioner.decorateKey(ByteBufferUtil.clone(key));
ssTableWriter.setFirst(first);
ssTableWriter.setLast(first);
}
public IPartitioner partitioner()
{
return ssTableWriter.getPartitioner();
}
public DeletionTime openMarker() {
return openMarker;
}
}

View File

@ -68,7 +68,7 @@ class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter
private final BlockingQueue<Buffer> writeQueue = newBlockingQueue(0); private final BlockingQueue<Buffer> writeQueue = newBlockingQueue(0);
private final DiskWriter diskWriter = new DiskWriter(); private final DiskWriter diskWriter = new DiskWriter();
SSTableSimpleUnsortedWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) public SSTableSimpleUnsortedWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB)
{ {
this(null, directory, metadata, columns, maxSSTableSizeInMiB); this(null, directory, metadata, columns, maxSSTableSizeInMiB);
} }
@ -198,7 +198,7 @@ class SSTableSimpleUnsortedWriter extends AbstractSSTableSimpleWriter
} }
} }
static class SyncException extends RuntimeException public static class SyncException extends RuntimeException
{ {
SyncException(IOException ioe) SyncException(IOException ioe)
{ {

View File

@ -64,7 +64,7 @@ class SSTableSimpleWriter extends AbstractSSTableSimpleWriter
* @param maxSSTableSizeInMiB defines the max SSTable size if the value is positive. * @param maxSSTableSizeInMiB defines the max SSTable size if the value is positive.
* Any non-positive value indicates the sstable size is unlimited. * Any non-positive value indicates the sstable size is unlimited.
*/ */
protected SSTableSimpleWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB) public SSTableSimpleWriter(File directory, TableMetadataRef metadata, RegularAndStaticColumns columns, long maxSSTableSizeInMiB)
{ {
this(null, directory, metadata, columns, maxSSTableSizeInMiB); this(null, directory, metadata, columns, maxSSTableSizeInMiB);
} }

View File

@ -0,0 +1,226 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.IOException;
import java.util.Arrays;
import org.apache.cassandra.db.Columns;
import org.apache.cassandra.db.DeletionTime.ReusableDeletionTime;
import org.apache.cassandra.db.ReusableLivenessInfo;
import org.apache.cassandra.db.SerializationHeader;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.db.rows.UnfilteredSerializer;
import org.apache.cassandra.io.util.RandomAccessReader;
public class UnfilteredDescriptor extends ClusteringDescriptor
{
private final ReusableLivenessInfo rowLivenessInfo = new ReusableLivenessInfo();
private final ReusableDeletionTime deletionTime = ReusableDeletionTime.live();
private final ReusableDeletionTime deletionTime2 = ReusableDeletionTime.live();
private long position;
private int flags;
private int extendedFlags;
private long unfilteredSize;
private long unfilteredDataStart;
private long prevUnfilteredSize;
Columns rowColumns;
public UnfilteredDescriptor(AbstractType<?>[] clusteringTypes)
{
super(clusteringTypes);
}
void loadTombstone(RandomAccessReader dataReader,
SerializationHeader serializationHeader,
int flags) throws IOException
{
this.flags = flags;
this.extendedFlags = 0;
rowColumns = null;
byte clusteringKind = dataReader.readByte();
if (clusteringKind == STATIC_CLUSTERING_KIND || clusteringKind == ROW_CLUSTERING_KIND) {
// STATIC_CLUSTERING or CLUSTERING -> no deletion info, should not happen
throw new IllegalStateException();
}
int columnsBound = dataReader.readUnsignedShort();
loadClustering(dataReader, clusteringKind, columnsBound);
unfilteredSize = dataReader.readUnsignedVInt();
prevUnfilteredSize = dataReader.readUnsignedVInt(); // debug only, unused otherwise
if (clusteringKind == EXCL_END_INCL_START_BOUNDARY_CLUSTERING_KIND ||
clusteringKind == INCL_END_EXCL_START_BOUNDARY_CLUSTERING_KIND)
{
// boundary
// CLOSE
serializationHeader.readDeletionTime(dataReader, deletionTime);
// OPEN
serializationHeader.readDeletionTime(dataReader, deletionTime2);
}
else
{
// bound
// CLOSE|OPEN
serializationHeader.readDeletionTime(dataReader, deletionTime);
}
}
void loadRow(RandomAccessReader dataReader,
SerializationHeader serializationHeader,
DeserializationHelper deserializationHelper,
int flags) throws IOException {
// body = whatever is covered by size, so inclusive of the prev_row_size inclusive of flags
position = dataReader.getPosition() - 1;
this.flags = flags;
this.extendedFlags = 0;
loadClustering(dataReader, ROW_CLUSTERING_KIND, this.clusteringTypes.length);
rowColumns = serializationHeader.columns(false);
loadCommonRowFields(dataReader, serializationHeader, deserializationHelper, flags);
}
void loadStaticRow(RandomAccessReader dataReader,
SerializationHeader serializationHeader,
DeserializationHelper deserializationHelper,
int flags,
int extendedFlags) throws IOException {
// body = whatever is covered by size, so inclusive of the prev_row_size inclusive of flags
position = dataReader.getPosition() - 2;
this.flags = flags;
this.extendedFlags = extendedFlags;
// no clustering
loadClustering(dataReader, STATIC_CLUSTERING_KIND, 0);
rowColumns = serializationHeader.columns(true);
loadCommonRowFields(dataReader, serializationHeader, deserializationHelper, flags);
}
private void loadCommonRowFields(RandomAccessReader dataReader,
SerializationHeader serializationHeader,
DeserializationHelper deserializationHelper,
int flags) throws IOException
{
unfilteredSize = dataReader.readUnsignedVInt();
unfilteredDataStart = dataReader.getPosition();
prevUnfilteredSize = dataReader.readUnsignedVInt(); // debug only, unused otherwise
SSTableCursorReader.readLivenessInfo(dataReader,
serializationHeader,
deserializationHelper,
flags,
rowLivenessInfo);
if (UnfilteredSerializer.hasDeletion(flags))
{
// struct delta_deletion_time {
// varint delta_marked_for_delete_at;
// varint delta_local_deletion_time;
//};
serializationHeader.readDeletionTime(dataReader, deletionTime);
}
else
{
deletionTime.resetLive();
}
if (!UnfilteredSerializer.hasAllColumns(flags))
{
// TODO: re-implement GC free
rowColumns = Columns.serializer.deserializeSubset(rowColumns, dataReader);
}
}
public void resetUnfiltered()
{
resetClustering();
position = 0;
flags = 0;
extendedFlags = 0;
unfilteredSize = 0;
unfilteredDataStart = 0;
prevUnfilteredSize = 0;
rowColumns = null;
}
public long position()
{
return position;
}
public ReusableLivenessInfo livenessInfo()
{
return rowLivenessInfo;
}
public ReusableDeletionTime deletionTime()
{
return deletionTime;
}
public ReusableDeletionTime openDeletionTime()
{
return isBoundary() ? deletionTime2 : isEndBound() ? null : deletionTime;
}
public ReusableDeletionTime deletionTime2()
{
return deletionTime2;
}
public int flags()
{
return flags;
}
public long size()
{
return unfilteredSize;
}
public long dataStart()
{
return unfilteredDataStart;
}
public Columns rowColumns()
{
return rowColumns;
}
@Override
public String toString()
{
return "UnfilteredDescriptor{" +
"rowLivenessInfo=" + rowLivenessInfo +
", deletionTime=" + deletionTime +
", position=" + position +
", flags=" + flags +
", extFlags=" + extendedFlags +
", unfilteredSize=" + unfilteredSize +
", prevUnfilteredSize=" + prevUnfilteredSize +
", unfilteredDataStart=" + unfilteredDataStart +
", rowColumns=" + rowColumns +
", clusteringTypes=" + Arrays.toString(clusteringTypes()) +
'}';
}
}

View File

@ -926,7 +926,16 @@ public abstract class SSTableReader extends SSTable implements UnfilteredSource,
/** /**
* Returns a {@link KeyReader} over all keys in the sstable. * Returns a {@link KeyReader} over all keys in the sstable.
*/ */
public abstract KeyReader keyReader() throws IOException; public final KeyReader keyReader() throws IOException {
return keyReader(false);
}
/**
* Returns a {@link KeyReader} over all keys in the sstable.
*
* @param detailed should the iterator also provide details per partition entry(e.g. row entry details)
*/
public abstract KeyReader keyReader(boolean detailed) throws IOException;
/** /**
* Returns a {@link KeyReader} over all keys in the sstable after a given key. * Returns a {@link KeyReader} over all keys in the sstable after a given key.

View File

@ -280,4 +280,12 @@ implements ISSTableScanner
} }
} }
} }
@Override
public boolean isFullRange()
{
return dataRange == null || (dataRange.startKey().equals(sstable.getFirst()) &&
dataRange.stopKey().equals(sstable.getLast()) &&
dataRange.isUnrestricted(sstable.metadata()));
}
} }

View File

@ -129,6 +129,13 @@ implements ISSTableScanner
return ImmutableSet.of(sstable); return ImmutableSet.of(sstable);
} }
@Override
public boolean isFullRange()
{
// hasNext will init start and end
return hasNext() && currentStartPosition == 0 && currentEndPosition == sstable.uncompressedLength();
}
public TableMetadata metadata() public TableMetadata metadata()
{ {
return sstable.metadata(); return sstable.metadata();

View File

@ -77,8 +77,8 @@ public abstract class SSTableWriter extends SSTable implements Transactional
protected boolean isTransient; protected boolean isTransient;
protected long maxDataAge = -1; protected long maxDataAge = -1;
protected final long keyCount; protected final long keyCount;
protected final MetadataCollector metadataCollector; public final MetadataCollector metadataCollector;
protected final SerializationHeader header; public final SerializationHeader header;
protected final List<SSTableFlushObserver> observers; protected final List<SSTableFlushObserver> observers;
protected final MmappedRegionsCache mmappedRegionsCache; protected final MmappedRegionsCache mmappedRegionsCache;
protected final TransactionalProxy txnProxy = txnProxy(); protected final TransactionalProxy txnProxy = txnProxy();
@ -334,7 +334,7 @@ public abstract class SSTableWriter extends SSTable implements Transactional
} }
} }
protected Map<MetadataType, MetadataComponent> finalizeMetadata() protected final Map<MetadataType, MetadataComponent> finalizeMetadata()
{ {
return metadataCollector.finalizeMetadata(getPartitioner().getClass().getCanonicalName(), return metadataCollector.finalizeMetadata(getPartitioner().getClass().getCanonicalName(),
metadata().params.bloomFilterFpChance, metadata().params.bloomFilterFpChance,
@ -595,4 +595,12 @@ public abstract class SSTableWriter extends SSTable implements Transactional
return new SSTableZeroCopyWriter(this, txn, owner); return new SSTableZeroCopyWriter(this, txn, owner);
} }
} }
public void setFirst(DecoratedKey key) {
first = key;
}
public void setLast(DecoratedKey key) {
last = key;
}
} }

View File

@ -43,11 +43,11 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
private final SerializationHelper helper; private final SerializationHelper helper;
private final Version version; private final Version version;
private long previousRowStart; private long previousRowStartOffset;
private long initialPosition; private long partitionStartPosition;
private long headerLength; private long headerLength;
protected long startPosition; protected long indexBlockStartOffset;
protected int written; protected int written;
protected ClusteringPrefix<?> firstClustering; protected ClusteringPrefix<?> firstClustering;
@ -78,9 +78,9 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
protected void reset() protected void reset()
{ {
this.initialPosition = writer.position(); this.partitionStartPosition = writer.position();
this.startPosition = -1; this.indexBlockStartOffset = -1;
this.previousRowStart = 0; this.previousRowStartOffset = 0;
this.written = 0; this.written = 0;
this.firstClustering = null; this.firstClustering = null;
this.lastClustering = null; this.lastClustering = null;
@ -106,7 +106,7 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
if (!header.hasStatic()) if (!header.hasStatic())
{ {
this.headerLength = writer.position() - initialPosition; this.headerLength = writer.position() - partitionStartPosition;
state = State.AWAITING_ROWS; state = State.AWAITING_ROWS;
return; return;
} }
@ -121,7 +121,7 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
UnfilteredSerializer.serializer.serializeStaticRow(staticRow, helper, writer, version.correspondingMessagingVersion()); UnfilteredSerializer.serializer.serializeStaticRow(staticRow, helper, writer, version.correspondingMessagingVersion());
this.headerLength = writer.position() - initialPosition; this.headerLength = writer.position() - partitionStartPosition;
state = State.AWAITING_ROWS; state = State.AWAITING_ROWS;
} }
@ -129,21 +129,21 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
{ {
checkState(state == State.AWAITING_ROWS); checkState(state == State.AWAITING_ROWS);
long pos = currentPosition(); long offset = currentOffsetInPartition();
if (firstClustering == null) if (firstClustering == null)
{ {
// Beginning of an index block. Remember the start and position // Beginning of an index block. Remember the start clustering and position
firstClustering = unfiltered.clustering(); firstClustering = unfiltered.clustering();
startOpenMarker = openMarker; startOpenMarker = openMarker; // first entry is always LIVE (for BTI format)
startPosition = pos; indexBlockStartOffset = offset;
} }
long unfilteredPosition = writer.position(); long unfilteredPosition = writer.position();
unfilteredSerializer.serialize(unfiltered, helper, writer, pos - previousRowStart, version.correspondingMessagingVersion()); unfilteredSerializer.serialize(unfiltered, helper, writer, offset - previousRowStartOffset, version.correspondingMessagingVersion());
lastClustering = unfiltered.clustering(); lastClustering = unfiltered.clustering();
previousRowStart = pos; previousRowStartOffset = offset;
++written; ++written;
if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER) if (unfiltered.kind() == Unfiltered.Kind.RANGE_TOMBSTONE_MARKER)
@ -159,19 +159,30 @@ public abstract class SortedTablePartitionWriter implements AutoCloseable
state = State.COMPLETED; state = State.COMPLETED;
long endPosition = currentPosition(); long partitionLength = currentOffsetInPartition();
unfilteredSerializer.writeEndOfPartition(writer); unfilteredSerializer.writeEndOfPartition(writer);
return endPosition; return partitionLength;
} }
protected long currentPosition() protected long currentOffsetInPartition()
{ {
return writer.position() - initialPosition; return writer.position() - partitionStartPosition;
} }
public long getInitialPosition() public long getPartitionStartPosition()
{ {
return initialPosition; return partitionStartPosition;
}
/** Some bullshit access for now */
public SerializationHeader getHeader()
{
return header;
}
public SerializationHelper getHelper()
{
return helper;
} }
} }

View File

@ -175,7 +175,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
{ {
try try
{ {
outputHandler.debug("Deserializing bloom filter for %s", sstable); if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing bloom filter for %s", sstable);
deserializeBloomFilter(sstable); deserializeBloomFilter(sstable);
} }
catch (Throwable t) catch (Throwable t)
@ -217,7 +217,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
protected int verifyOwnedRanges() protected int verifyOwnedRanges()
{ {
List<Range<Token>> ownedRanges = Collections.emptyList(); List<Range<Token>> ownedRanges = Collections.emptyList();
outputHandler.debug("Checking that all tokens are owned by the current node"); if (outputHandler.isDebugEnabled()) outputHandler.debug("Checking that all tokens are owned by the current node");
try (KeyIterator iter = sstable.keyIterator()) try (KeyIterator iter = sstable.keyIterator())
{ {
ownedRanges = Range.normalize(tokenLookup.apply(cfs.metadata.keyspace)); ownedRanges = Range.normalize(tokenLookup.apply(cfs.metadata.keyspace));
@ -287,7 +287,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
throw new CompactionInterruptedException(verifyInfo.getCompactionInfo()); throw new CompactionInterruptedException(verifyInfo.getCompactionInfo());
long rowStart = dataFile.getFilePointer(); long rowStart = dataFile.getFilePointer();
outputHandler.debug("Reading row at %d", rowStart); if (outputHandler.isDebugEnabled()) outputHandler.debug("Reading row at %d", rowStart);
DecoratedKey key = null; DecoratedKey key = null;
try try
@ -332,8 +332,11 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
long dataSize = nextRowPositionFromIndex - dataStartFromIndex; long dataSize = nextRowPositionFromIndex - dataStartFromIndex;
// avoid an NPE if key is null // avoid an NPE if key is null
if (outputHandler.isDebugEnabled())
{
String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey()); String keyName = key == null ? "(unreadable key)" : ByteBufferUtil.bytesToHex(key.getKey());
outputHandler.debug("row %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSize)); outputHandler.debug("row %s is %s", keyName, FBUtilities.prettyPrintMemory(dataSize));
}
try try
{ {
@ -352,7 +355,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
prevKey = key; prevKey = key;
outputHandler.debug("Row %s at %s valid, moving to next row at %s ", goodRows, rowStart, nextRowPositionFromIndex); if (outputHandler.isDebugEnabled()) outputHandler.debug("Row %s at %s valid, moving to next row at %s ", goodRows, rowStart, nextRowPositionFromIndex);
dataFile.seek(nextRowPositionFromIndex); dataFile.seek(nextRowPositionFromIndex);
} }
catch (Throwable th) catch (Throwable th)
@ -374,7 +377,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
{ {
try try
{ {
outputHandler.debug("Deserializing index for %s", sstable); if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing index for %s", sstable);
deserializeIndex(sstable); deserializeIndex(sstable);
} }
catch (Throwable t) catch (Throwable t)
@ -384,7 +387,7 @@ public abstract class SortedTableVerifier<R extends SSTableReaderWithFilter> imp
} }
} }
private void deserializeIndex(SSTableReader sstable) throws IOException protected void deserializeIndex(SSTableReader sstable) throws IOException
{ {
try (KeyReader it = sstable.keyReader()) try (KeyReader it = sstable.keyReader())
{ {

View File

@ -79,9 +79,9 @@ public abstract class SortedTableWriter<P extends SortedTablePartitionWriter, I
// TODO dataWriter is not needed to be directly accessible - we can access everything we need for the dataWriter // TODO dataWriter is not needed to be directly accessible - we can access everything we need for the dataWriter
// from a partition writer // from a partition writer
protected final SequentialWriter dataWriter; public final SequentialWriter dataWriter;
protected final I indexWriter; public final I indexWriter;
protected final P partitionWriter; public final P partitionWriter;
private final FileHandle.Builder dataFileBuilder = new FileHandle.Builder(descriptor.fileFor(Components.DATA)); private final FileHandle.Builder dataFileBuilder = new FileHandle.Builder(descriptor.fileFor(Components.DATA));
private DecoratedKey lastWrittenKey; private DecoratedKey lastWrittenKey;
private DataPosition dataMark; private DataPosition dataMark;
@ -237,13 +237,15 @@ public abstract class SortedTableWriter<P extends SortedTablePartitionWriter, I
private AbstractRowIndexEntry endPartition(DecoratedKey key, DeletionTime partitionLevelDeletion) throws IOException private AbstractRowIndexEntry endPartition(DecoratedKey key, DeletionTime partitionLevelDeletion) throws IOException
{ {
// partitionLength not inclusive of last byte for BIG/default impl, but something different for BTI (and maybe other impls)
long finishResult = partitionWriter.finish(); long finishResult = partitionWriter.finish();
long endPosition = dataWriter.position(); long endPosition = dataWriter.position();
long rowSize = endPosition - partitionWriter.getInitialPosition(); // inclusive of last byte
guardPartitionThreshold(Guardrails.partitionSize, key, rowSize); long partitionSize = endPosition - partitionWriter.getPartitionStartPosition();
guardPartitionThreshold(Guardrails.partitionSize, key, partitionSize);
guardPartitionThreshold(Guardrails.partitionTombstones, key, metadataCollector.totalTombstones); guardPartitionThreshold(Guardrails.partitionTombstones, key, metadataCollector.totalTombstones);
metadataCollector.addPartitionSizeInBytes(rowSize); metadataCollector.addPartitionSizeInBytes(partitionSize);
metadataCollector.addKey(key.getKey()); metadataCollector.addKey(key.getKey());
metadataCollector.addCellPerPartitionCount(); metadataCollector.addCellPerPartitionCount();
@ -260,7 +262,7 @@ public abstract class SortedTableWriter<P extends SortedTablePartitionWriter, I
protected void onStartPartition(DecoratedKey key) protected void onStartPartition(DecoratedKey key)
{ {
if (hasObservers()) if (hasObservers())
notifyObservers(o -> o.startPartition(key, partitionWriter.getInitialPosition(), partitionWriter.getInitialPosition())); notifyObservers(o -> o.startPartition(key, partitionWriter.getPartitionStartPosition(), partitionWriter.getPartitionStartPosition()));
} }
protected void onStaticRow(Row row) protected void onStaticRow(Row row)
@ -331,7 +333,7 @@ public abstract class SortedTableWriter<P extends SortedTablePartitionWriter, I
} }
@Override @Override
public long getFilePointer() public final long getFilePointer()
{ {
return dataWriter.position(); return dataWriter.position();
} }
@ -435,13 +437,13 @@ public abstract class SortedTableWriter<P extends SortedTablePartitionWriter, I
} }
} }
protected static abstract class AbstractIndexWriter extends AbstractTransactional implements Transactional public static abstract class AbstractIndexWriter extends AbstractTransactional implements Transactional
{ {
protected final Descriptor descriptor; protected final Descriptor descriptor;
protected final TableMetadataRef metadata; protected final TableMetadataRef metadata;
protected final Set<Component> components; protected final Set<Component> components;
protected final IFilter bf; public final IFilter bf;
protected AbstractIndexWriter(Builder<?, ?, ?, ?> b) protected AbstractIndexWriter(Builder<?, ?, ?, ?> b)
{ {

View File

@ -48,11 +48,11 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
@VisibleForTesting @VisibleForTesting
public static final int DEFAULT_GRANULARITY = 64 * 1024; public static final int DEFAULT_GRANULARITY = 64 * 1024;
// used, if the row-index-entry reaches config column_index_cache_size // used, if the row-index-entry reaches config switchIndexInfoToBufferThreshold
private DataOutputBuffer buffer; private DataOutputBuffer rowIndexEntryBuffer;
// used to track the size of the serialized size of row-index-entry (unused for buffer) // used to track the total serialized size of indexSamples (unused for buffer)
private int indexSamplesSerializedSize; private int indexSamplesSerializedSize;
// used, until the row-index-entry reaches config column_index_cache_size // used, until the row-index-entry reaches switchIndexInfoToBufferThreshold (from config column_index_cache_size, or default 64k)
private final List<IndexInfo> indexSamples = new ArrayList<>(); private final List<IndexInfo> indexSamples = new ArrayList<>();
private DataOutputBuffer reusableBuffer; private DataOutputBuffer reusableBuffer;
@ -62,8 +62,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
private final ISerializer<IndexInfo> idxSerializer; private final ISerializer<IndexInfo> idxSerializer;
private final int cacheSizeThreshold; /** Beyond this limit we switch from storing IndexInfo in the list to directly serializing them into a buffer */
private final int indexSize; private final int switchIndexInfoToBufferThreshold;
/** If a partition grows beyond this size we store inter-partition index data in IndexInfo */
private final int indexBlockThreshold;
BigFormatPartitionWriter(SerializationHeader header, BigFormatPartitionWriter(SerializationHeader header,
SequentialWriter writer, SequentialWriter writer,
@ -82,8 +84,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
{ {
super(header, writer, version); super(header, writer, version);
this.idxSerializer = indexInfoSerializer; this.idxSerializer = indexInfoSerializer;
this.cacheSizeThreshold = cacheSizeThreshold; this.switchIndexInfoToBufferThreshold = cacheSizeThreshold;
this.indexSize = indexSize; this.indexBlockThreshold = indexSize;
} }
public void reset() public void reset()
@ -93,9 +95,9 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
this.indexSamplesSerializedSize = 0; this.indexSamplesSerializedSize = 0;
this.indexSamples.clear(); this.indexSamples.clear();
if (this.buffer != null) if (this.rowIndexEntryBuffer != null)
this.reusableBuffer = this.buffer; this.reusableBuffer = this.rowIndexEntryBuffer;
this.buffer = null; this.rowIndexEntryBuffer = null;
} }
public int getColumnIndexCount() public int getColumnIndexCount()
@ -105,12 +107,12 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
public ByteBuffer buffer() public ByteBuffer buffer()
{ {
return buffer != null ? buffer.buffer() : null; return rowIndexEntryBuffer != null ? rowIndexEntryBuffer.buffer() : null;
} }
public List<IndexInfo> indexSamples() public List<IndexInfo> indexSamples()
{ {
if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) <= cacheSizeThreshold) if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) <= switchIndexInfoToBufferThreshold)
{ {
return indexSamples; return indexSamples;
} }
@ -129,8 +131,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
{ {
IndexInfo cIndexInfo = new IndexInfo(firstClustering, IndexInfo cIndexInfo = new IndexInfo(firstClustering,
lastClustering, lastClustering,
startPosition, indexBlockStartOffset,
currentPosition() - startPosition, currentOffsetInPartition() - indexBlockStartOffset,
!openMarker.isLive() ? openMarker : null); !openMarker.isLive() ? openMarker : null);
// indexOffsets is used for both shallow (ShallowIndexedEntry) and non-shallow IndexedEntry. // indexOffsets is used for both shallow (ShallowIndexedEntry) and non-shallow IndexedEntry.
@ -156,8 +158,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
else else
{ {
indexOffsets[columnIndexCount] = indexOffsets[columnIndexCount] =
buffer != null rowIndexEntryBuffer != null
? Ints.checkedCast(buffer.position()) ? Ints.checkedCast(rowIndexEntryBuffer.position())
: indexSamplesSerializedSize; : indexSamplesSerializedSize;
} }
} }
@ -165,16 +167,20 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
// First, we collect the IndexInfo objects until we reach Config.column_index_cache_size in an ArrayList. // First, we collect the IndexInfo objects until we reach Config.column_index_cache_size in an ArrayList.
// When column_index_cache_size is reached, we switch to byte-buffer mode. // When column_index_cache_size is reached, we switch to byte-buffer mode.
if (buffer == null) if (rowIndexEntryBuffer == null)
{ {
indexSamplesSerializedSize += idxSerializer.serializedSize(cIndexInfo); indexSamplesSerializedSize += idxSerializer.serializedSize(cIndexInfo);
if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0) > cacheSizeThreshold) if (indexSamplesSerializedSize + columnIndexCount * TypeSizes.INT_SIZE > switchIndexInfoToBufferThreshold)
{ {
buffer = reuseOrAllocateBuffer(); rowIndexEntryBuffer = reuseOrAllocateBuffer();
// serialize pre-existing samples
for (IndexInfo indexSample : indexSamples) for (IndexInfo indexSample : indexSamples)
{ {
idxSerializer.serialize(indexSample, buffer); /** {@link IndexInfo.Serializer#serialize} */
idxSerializer.serialize(indexSample, rowIndexEntryBuffer);
} }
// release pre-existing samples
indexSamples.clear();
} }
else else
{ {
@ -182,9 +188,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
} }
} }
// don't put an else here since buffer may be allocated in preceding if block // don't put an else here since buffer may be allocated in preceding if block
if (buffer != null) if (rowIndexEntryBuffer != null)
{ {
idxSerializer.serialize(cIndexInfo, buffer); /** {@link IndexInfo.Serializer#serialize} */
idxSerializer.serialize(cIndexInfo, rowIndexEntryBuffer);
} }
firstClustering = null; firstClustering = null;
@ -201,7 +208,7 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
return buffer; return buffer;
} }
// don't use the standard RECYCLER as that only recycles up to 1MB and requires proper cleanup // don't use the standard RECYCLER as that only recycles up to 1MB and requires proper cleanup
return new DataOutputBuffer(cacheSizeThreshold * 2); return new DataOutputBuffer(switchIndexInfoToBufferThreshold * 2);
} }
@Override @Override
@ -210,7 +217,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
super.addUnfiltered(unfiltered); super.addUnfiltered(unfiltered);
// if we hit the column index size that we have to index after, go ahead and index it. // if we hit the column index size that we have to index after, go ahead and index it.
if (currentPosition() - startPosition >= indexSize) long sizeSinceLastIndexBlock = currentOffsetInPartition() - indexBlockStartOffset;
if (sizeSinceLastIndexBlock >= this.indexBlockThreshold)
addIndexBlock(); addIndexBlock();
} }
@ -231,10 +239,10 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
// we have to write the offsts to these here. The offsets have already been collected // we have to write the offsts to these here. The offsets have already been collected
// in indexOffsets[]. buffer is != null, if it exceeds Config.column_index_cache_size. // in indexOffsets[]. buffer is != null, if it exceeds Config.column_index_cache_size.
// In the other case, when buffer==null, the offsets are serialized in RowIndexEntry.IndexedEntry.serialize(). // In the other case, when buffer==null, the offsets are serialized in RowIndexEntry.IndexedEntry.serialize().
if (buffer != null) if (rowIndexEntryBuffer != null)
{ {
for (int i = 0; i < columnIndexCount; i++) for (int i = 0; i < columnIndexCount; i++)
buffer.writeInt(indexOffsets[i]); rowIndexEntryBuffer.writeInt(indexOffsets[i]);
} }
// we should always have at least one computed index block, but we only write it out if there is more than that. // we should always have at least one computed index block, but we only write it out if there is more than that.
@ -245,8 +253,8 @@ public class BigFormatPartitionWriter extends SortedTablePartitionWriter
public int indexInfoSerializedSize() public int indexInfoSerializedSize()
{ {
return buffer != null return rowIndexEntryBuffer != null
? buffer.buffer().limit() ? rowIndexEntryBuffer.buffer().limit()
: indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0); : indexSamplesSerializedSize + columnIndexCount * TypeSizes.sizeof(0);
} }

View File

@ -178,7 +178,7 @@ public class BigSSTableReaderLoadingBuilder extends SortedTableReaderLoadingBuil
checkNotNull(serializationHeader); checkNotNull(serializationHeader);
RowIndexEntry.IndexSerializer serializer = new RowIndexEntry.Serializer(descriptor.version, serializationHeader, tableMetrics); RowIndexEntry.IndexSerializer serializer = new RowIndexEntry.Serializer(descriptor.version, serializationHeader, tableMetrics);
return BigTableKeyReader.create(indexFile, serializer); return BigTableKeyReader.create(indexFile, serializer, false);
} }
/** /**

View File

@ -36,19 +36,23 @@ public class BigTableKeyReader implements KeyReader
private final RandomAccessReader indexFileReader; private final RandomAccessReader indexFileReader;
private final IndexSerializer rowIndexEntrySerializer; private final IndexSerializer rowIndexEntrySerializer;
private final long initialPosition; private final long initialPosition;
private final boolean detailed;
private ByteBuffer key; private ByteBuffer key;
private long dataPosition; private long dataPosition;
private long keyPosition; private long keyPosition;
/** only if detailed */
private RowIndexEntry rowIndexEntry;
private BigTableKeyReader(FileHandle indexFile, private BigTableKeyReader(FileHandle indexFile,
RandomAccessReader indexFileReader, RandomAccessReader indexFileReader,
IndexSerializer rowIndexEntrySerializer) IndexSerializer rowIndexEntrySerializer,
boolean detailed)
{ {
this.indexFile = indexFile; this.indexFile = indexFile;
this.indexFileReader = indexFileReader; this.indexFileReader = indexFileReader;
this.rowIndexEntrySerializer = rowIndexEntrySerializer; this.rowIndexEntrySerializer = rowIndexEntrySerializer;
this.initialPosition = indexFileReader.getFilePointer(); this.initialPosition = indexFileReader.getFilePointer();
this.detailed = detailed;
} }
public static BigTableKeyReader create(RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException public static BigTableKeyReader create(RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException
@ -58,7 +62,7 @@ public class BigTableKeyReader implements KeyReader
public static BigTableKeyReader create(FileHandle indexFile, RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException public static BigTableKeyReader create(FileHandle indexFile, RandomAccessReader indexFileReader, IndexSerializer serializer) throws IOException
{ {
BigTableKeyReader iterator = new BigTableKeyReader(indexFile, indexFileReader, serializer); BigTableKeyReader iterator = new BigTableKeyReader(indexFile, indexFileReader, serializer, false);
try try
{ {
iterator.advance(); iterator.advance();
@ -72,7 +76,7 @@ public class BigTableKeyReader implements KeyReader
} }
@SuppressWarnings({ "resource", "RedundantSuppression" }) // iFile and reader are closed in the BigTableKeyReader#close method @SuppressWarnings({ "resource", "RedundantSuppression" }) // iFile and reader are closed in the BigTableKeyReader#close method
public static BigTableKeyReader create(FileHandle indexFile, IndexSerializer serializer) throws IOException public static BigTableKeyReader create(FileHandle indexFile, IndexSerializer serializer, boolean detailed) throws IOException
{ {
FileHandle iFile = null; FileHandle iFile = null;
RandomAccessReader reader = null; RandomAccessReader reader = null;
@ -81,7 +85,7 @@ public class BigTableKeyReader implements KeyReader
{ {
iFile = indexFile.sharedCopy(); iFile = indexFile.sharedCopy();
reader = iFile.createReader(); reader = iFile.createReader();
iterator = new BigTableKeyReader(iFile, reader, serializer); iterator = new BigTableKeyReader(iFile, reader, serializer, detailed);
iterator.advance(); iterator.advance();
return iterator; return iterator;
} }
@ -116,7 +120,14 @@ public class BigTableKeyReader implements KeyReader
{ {
keyPosition = indexFileReader.getFilePointer(); keyPosition = indexFileReader.getFilePointer();
key = ByteBufferUtil.readWithShortLength(indexFileReader); key = ByteBufferUtil.readWithShortLength(indexFileReader);
if (detailed) {
rowIndexEntry = rowIndexEntrySerializer.deserialize(indexFileReader);
dataPosition = rowIndexEntry.getPosition();
}
else
{
dataPosition = rowIndexEntrySerializer.deserializePositionAndSkip(indexFileReader); dataPosition = rowIndexEntrySerializer.deserializePositionAndSkip(indexFileReader);
}
return true; return true;
} }
else else
@ -152,6 +163,15 @@ public class BigTableKeyReader implements KeyReader
return dataPosition; return dataPosition;
} }
public RowIndexEntry rowIndexEntry() {
assert detailed;
return rowIndexEntry;
}
public FileHandle indexFile() {
return indexFile;
}
public long indexPosition() public long indexPosition()
{ {
return indexFileReader.getFilePointer(); return indexFileReader.getFilePointer();

View File

@ -141,9 +141,9 @@ public class BigTableReader extends SSTableReaderWithFilter implements IndexSumm
} }
@Override @Override
public KeyReader keyReader() throws IOException public KeyReader keyReader(boolean detailed) throws IOException
{ {
return BigTableKeyReader.create(ifile, rowIndexEntrySerializer); return BigTableKeyReader.create(ifile, rowIndexEntrySerializer, detailed);
} }
@Override @Override

View File

@ -188,6 +188,7 @@ public class BigTableScanner extends SSTableScanner<BigTableReader, RowIndexEntr
"dfile=" + dfile + "dfile=" + dfile +
" ifile=" + ifile + " ifile=" + ifile +
" sstable=" + sstable + " sstable=" + sstable +
" rangeIterator=" + rangeIterator +
")"; ")";
} }
} }

View File

@ -18,17 +18,25 @@
package org.apache.cassandra.io.sstable.format.big; package org.apache.cassandra.io.sstable.format.big;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.time.Instant; import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.cassandra.db.ClusteringComparator;
import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.rows.BTreeRow;
import org.apache.cassandra.db.rows.Cell; import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.db.rows.Row; import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Unfiltered; import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.rows.UnfilteredSerializer;
import org.apache.cassandra.io.sstable.CorruptSSTableException;
import org.apache.cassandra.io.sstable.IVerifier; import org.apache.cassandra.io.sstable.IVerifier;
import org.apache.cassandra.io.sstable.IndexInfo;
import org.apache.cassandra.io.sstable.format.SSTableReader; import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SortedTableVerifier; import org.apache.cassandra.io.sstable.format.SortedTableVerifier;
import org.apache.cassandra.io.sstable.format.big.BigFormat.Components; import org.apache.cassandra.io.sstable.format.big.BigFormat.Components;
@ -82,7 +90,7 @@ public class BigTableVerifier extends SortedTableVerifier<BigTableReader> implem
{ {
try try
{ {
outputHandler.debug("Deserializing index summary for %s", sstable); if (outputHandler.isDebugEnabled()) outputHandler.debug("Deserializing index summary for %s", sstable);
deserializeIndexSummary(sstable); deserializeIndexSummary(sstable);
} }
catch (Throwable t) catch (Throwable t)
@ -99,6 +107,74 @@ public class BigTableVerifier extends SortedTableVerifier<BigTableReader> implem
super.verifyIndex(); super.verifyIndex();
} }
@Override
protected void deserializeIndex(SSTableReader sstable) throws IOException
{
DeserializationHelper deserializationHelper = new DeserializationHelper(sstable.metadata(),
sstable.descriptor.version.correspondingMessagingVersion(),
DeserializationHelper.Flag.LOCAL);
ClusteringComparator comparator = sstable.metadata().comparator;
try (BigTableKeyReader it = (BigTableKeyReader)sstable.keyReader(true))
{
if (it.isExhausted())
return;
ByteBuffer key;
boolean isFirst = true;
do
{
key = it.key();
if (outputHandler.isDebugEnabled()) outputHandler.debug("Key %s", sstable.metadata().partitionKeyType.getString(key));
if (isFirst && !Objects.equals(key, sstable.getFirst().getKey()))
throw new CorruptSSTableException(new IOException("First partition does not match index"), it.toString());
else
isFirst = false;
RowIndexEntry rowIndexEntry = it.rowIndexEntry();
if (outputHandler.isDebugEnabled()) outputHandler.debug("rowIndexEntry %s", rowIndexEntry.toString());
long partitionBase = it.dataPosition();
int blockCount = rowIndexEntry.blockCount();
if (blockCount > 0)
{
long expectedNextOffset = 0;
RowIndexEntry.IndexInfoRetriever indexInfoRetriever = rowIndexEntry.openWithIndex(it.indexFile());
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
{
IndexInfo indexInfo = indexInfoRetriever.columnsIndex(blockIndex);
if (outputHandler.isDebugEnabled()) outputHandler.debug("indexInfo %s", indexInfo.toString(sstable.metadata()));
long dataFileOffset = partitionBase + indexInfo.offset;
if (expectedNextOffset != 0)
{
if (expectedNextOffset != indexInfo.offset)
throw new CorruptSSTableException(new IOException("Row entry indexInfo offset + width should match next block offset:" + indexInfo.offset + " expected: " + expectedNextOffset), it.toString());
}
expectedNextOffset = indexInfo.offset + indexInfo.width;
dataFile.seek(dataFileOffset);
Unfiltered unfiltered = UnfilteredSerializer.serializer.deserialize(dataFile,
sstable.header,
deserializationHelper,
BTreeRow.sortedBuilder());
if (!Objects.equals(indexInfo.firstName, unfiltered.clustering()))
throw new CorruptSSTableException(new IOException("Unfiltered clustering in index does not match data:{info=" + indexInfo.toString(sstable.metadata()) + ", row:" + unfiltered.clustering().toString(sstable.metadata()) + "}"), it.toString());
if (comparator.compare(indexInfo.firstName, indexInfo.lastName) > 0)
throw new CorruptSSTableException(new IOException("First name is > Last name:{info=" + indexInfo.toString(sstable.metadata()) + "}"), it.toString());
}
}
}
// The verifier checks that the partition key/position in the index and data match, here we waant to verify
// the entries clustering keys match.
while (it.advance()); // no-op, just check if index is readable
if (!Objects.equals(key, sstable.getLast().getKey()))
throw new CorruptSSTableException(new IOException("Last partition does not match index"), it.toString());
}
dataFile.reset();
}
private void logDuplicates(DecoratedKey key, Row first, int duplicateRows, long minTimestamp, long maxTimestamp) private void logDuplicates(DecoratedKey key, Row first, int duplicateRows, long minTimestamp, long maxTimestamp)
{ {
String keyString = sstable.metadata().partitionKeyType.getString(key.getKey()); String keyString = sstable.metadata().partitionKeyType.getString(key.getKey());

View File

@ -87,7 +87,7 @@ public class BigTableWriter extends SortedTableWriter<BigFormatPartitionWriter,
@Override @Override
protected void onStartPartition(DecoratedKey key) protected void onStartPartition(DecoratedKey key)
{ {
notifyObservers(o -> o.startPartition(key, partitionWriter.getInitialPosition(), indexWriter.writer.position())); notifyObservers(o -> o.startPartition(key, partitionWriter.getPartitionStartPosition(), indexWriter.writer.position()));
} }
@Override @Override
@ -97,7 +97,7 @@ public class BigTableWriter extends SortedTableWriter<BigFormatPartitionWriter,
// serialized size to the index-writer position // serialized size to the index-writer position
long indexFilePosition = ByteBufferUtil.serializedSizeWithShortLength(key.getKey()) + indexWriter.writer.position(); long indexFilePosition = ByteBufferUtil.serializedSizeWithShortLength(key.getKey()) + indexWriter.writer.position();
RowIndexEntry entry = RowIndexEntry.create(partitionWriter.getInitialPosition(), RowIndexEntry entry = RowIndexEntry.create(partitionWriter.getPartitionStartPosition(),
indexFilePosition, indexFilePosition,
partitionLevelDeletion, partitionLevelDeletion,
partitionWriter.getHeaderLength(), partitionWriter.getHeaderLength(),
@ -229,16 +229,30 @@ public class BigTableWriter extends SortedTableWriter<BigFormatPartitionWriter,
return openInternal(null, openReason); return openInternal(null, openReason);
} }
@Override
public void setFirst(DecoratedKey key)
{
super.setFirst(key);
indexWriter.first = key;
}
@Override
public void setLast(DecoratedKey key)
{
super.setLast(key);
indexWriter.last = key;
}
/** /**
* Encapsulates writing the index and filter for an SSTable. The state of this object is not valid until it has been closed. * Encapsulates writing the index and filter for an SSTable. The state of this object is not valid until it has been closed.
*/ */
protected static class IndexWriter extends SortedTableWriter.AbstractIndexWriter public static class IndexWriter extends SortedTableWriter.AbstractIndexWriter
{ {
private final RowIndexEntry.IndexSerializer rowIndexEntrySerializer; private final RowIndexEntry.IndexSerializer rowIndexEntrySerializer;
final SequentialWriter writer; public final SequentialWriter writer;
final FileHandle.Builder builder; final FileHandle.Builder builder;
final IndexSummaryBuilder summary; public final IndexSummaryBuilder summary;
private DataPosition mark; private DataPosition mark;
private DecoratedKey first; private DecoratedKey first;
private DecoratedKey last; private DecoratedKey last;

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.io.sstable.format.big;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List; import java.util.List;
import com.codahale.metrics.Histogram; import com.codahale.metrics.Histogram;
@ -150,6 +151,7 @@ public class RowIndexEntry extends AbstractRowIndexEntry
static final Histogram indexInfoCountHistogram; static final Histogram indexInfoCountHistogram;
static final Histogram indexInfoGetsHistogram; static final Histogram indexInfoGetsHistogram;
static final Histogram indexInfoReadsHistogram; static final Histogram indexInfoReadsHistogram;
static static
{ {
MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, "RowIndexEntry"); MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME, "RowIndexEntry");
@ -259,7 +261,6 @@ public class RowIndexEntry extends AbstractRowIndexEntry
default RowIndexEntry deserialize(FileDataInput input) throws IOException default RowIndexEntry deserialize(FileDataInput input) throws IOException
{ {
return deserialize(input, input.getFilePointer()); return deserialize(input, input.getFilePointer());
} }
void serializeForCache(RowIndexEntry rie, DataOutputPlus out) throws IOException; void serializeForCache(RowIndexEntry rie, DataOutputPlus out) throws IOException;
@ -358,6 +359,9 @@ public class RowIndexEntry extends AbstractRowIndexEntry
int indexedPartSize = size - serializedSize(deletionTime, headerLength, columnsIndexCount, version); int indexedPartSize = size - serializedSize(deletionTime, headerLength, columnsIndexCount, version);
// We should never get here with 0/1 entries
/** See: {@link org.apache.cassandra.io.sstable.format.big.RowIndexEntry#create} */
assert columnsIndexCount > 1;
if (size <= DatabaseDescriptor.getColumnIndexCacheSize()) if (size <= DatabaseDescriptor.getColumnIndexCacheSize())
{ {
return new IndexedEntry(position, in, deletionTime, headerLength, columnsIndexCount, return new IndexedEntry(position, in, deletionTime, headerLength, columnsIndexCount,
@ -663,6 +667,21 @@ public class RowIndexEntry extends AbstractRowIndexEntry
in.readUnsignedVInt(); in.readUnsignedVInt();
} }
@Override
public String toString()
{
return "IndexedEntry{" +
"position=" + position +
", deletionTime=" + deletionTime +
", headerLength=" + headerLength +
", columnsIndex=" + Arrays.toString(columnsIndex) +
", offsets=" + Arrays.toString(offsets) +
", indexedPartSize=" + indexedPartSize +
", idxInfoSerializer=" + idxInfoSerializer +
", version=" + version +
'}';
}
} }
/** /**
@ -678,6 +697,7 @@ public class RowIndexEntry extends AbstractRowIndexEntry
BASE_SIZE = ObjectSizes.measure(new ShallowIndexedEntry(0, 0, DeletionTime.LIVE, 0, 10, 0, null, BigFormat.getInstance().getLatestVersion())); BASE_SIZE = ObjectSizes.measure(new ShallowIndexedEntry(0, 0, DeletionTime.LIVE, 0, 10, 0, null, BigFormat.getInstance().getLatestVersion()));
} }
// only for cache serialization
private final long indexFilePosition; private final long indexFilePosition;
private final DeletionTime deletionTime; private final DeletionTime deletionTime;
@ -701,8 +721,6 @@ public class RowIndexEntry extends AbstractRowIndexEntry
{ {
super(dataFilePosition); super(dataFilePosition);
assert columnIndexCount > 1;
this.indexFilePosition = indexFilePosition; this.indexFilePosition = indexFilePosition;
this.headerLength = headerLength; this.headerLength = headerLength;
this.deletionTime = deletionTime; this.deletionTime = deletionTime;
@ -810,6 +828,23 @@ public class RowIndexEntry extends AbstractRowIndexEntry
in.readUnsignedVInt(); in.readUnsignedVInt();
} }
@Override
public String toString()
{
return "ShallowIndexedEntry{" +
"position=" + position +
", indexFilePosition=" + indexFilePosition +
", deletionTime=" + deletionTime +
", headerLength=" + headerLength +
", columnsIndexCount=" + columnsIndexCount +
", indexedPartSize=" + indexedPartSize +
", offsetsOffset=" + offsetsOffset +
", idxInfoSerializer=" + idxInfoSerializer +
", fieldsSerializedSize=" + fieldsSerializedSize +
", version=" + version +
'}';
}
} }
private static final class ShallowInfoRetriever extends FileIndexInfoRetriever private static final class ShallowInfoRetriever extends FileIndexInfoRetriever
@ -900,4 +935,12 @@ public class RowIndexEntry extends AbstractRowIndexEntry
super(message); super(message);
} }
} }
@Override
public String toString()
{
return "RowIndexEntry{" +
"position=" + position +
'}';
}
} }

View File

@ -79,7 +79,7 @@ class BtiFormatPartitionWriter extends SortedTablePartitionWriter
super.addUnfiltered(unfiltered); super.addUnfiltered(unfiltered);
// if we hit the column index size that we have to index after, go ahead and index it. // if we hit the column index size that we have to index after, go ahead and index it.
if (currentPosition() - startPosition >= rowIndexBlockSize) if (currentOffsetInPartition() - indexBlockStartOffset >= rowIndexBlockSize)
addIndexBlock(); addIndexBlock();
} }
@ -112,7 +112,7 @@ class BtiFormatPartitionWriter extends SortedTablePartitionWriter
protected void addIndexBlock() throws IOException protected void addIndexBlock() throws IOException
{ {
IndexInfo cIndexInfo = new IndexInfo(startPosition, startOpenMarker); IndexInfo cIndexInfo = new IndexInfo(indexBlockStartOffset, startOpenMarker);
rowTrie.add(firstClustering, lastClustering, cIndexInfo); rowTrie.add(firstClustering, lastClustering, cIndexInfo);
firstClustering = null; firstClustering = null;
++rowIndexBlockCount; ++rowIndexBlockCount;

View File

@ -317,7 +317,7 @@ public class BtiTableReader extends SSTableReaderWithFilter
} }
@Override @Override
public PartitionIterator keyReader() throws IOException public PartitionIterator keyReader(boolean detailed) throws IOException
{ {
return PartitionIterator.create(partitionIndex, metadata().partitioner, rowIndexFile, dfile, descriptor.version); return PartitionIterator.create(partitionIndex, metadata().partitioner, rowIndexFile, dfile, descriptor.version);
} }

View File

@ -73,7 +73,7 @@ public class BtiTableWriter extends SortedTableWriter<BtiFormatPartitionWriter,
@Override @Override
protected TrieIndexEntry createRowIndexEntry(DecoratedKey key, DeletionTime partitionLevelDeletion, long finishResult) throws IOException protected TrieIndexEntry createRowIndexEntry(DecoratedKey key, DeletionTime partitionLevelDeletion, long finishResult) throws IOException
{ {
TrieIndexEntry entry = TrieIndexEntry.create(partitionWriter.getInitialPosition(), TrieIndexEntry entry = TrieIndexEntry.create(partitionWriter.getPartitionStartPosition(),
finishResult, finishResult,
partitionLevelDeletion, partitionLevelDeletion,
partitionWriter.getRowIndexBlockCount()); partitionWriter.getRowIndexBlockCount());

View File

@ -145,7 +145,7 @@ public class IndexSummaryBuilder implements AutoCloseable
*/ */
private static long getEntrySize(long keySize) private static long getEntrySize(long keySize)
{ {
return keySize + TypeSizes.sizeof(0L); return keySize + TypeSizes.LONG_SIZE;
} }
// the index file has been flushed to the provided position; stash it and use that to recalculate our max readable boundary // the index file has been flushed to the provided position; stash it and use that to recalculate our max readable boundary
@ -188,6 +188,34 @@ public class IndexSummaryBuilder implements AutoCloseable
{ {
return maybeAddEntry(decoratedKey, indexStart, 0, 0); return maybeAddEntry(decoratedKey, indexStart, 0, 0);
} }
/**
* @param keyBytes the key data for this record
* @param offset key data offset in the keyBytes array
* @param length key data length
* @param indexStart the position in the index file this record begins
*/
public IndexSummaryBuilder maybeAddEntry(byte[] keyBytes, int offset, int length, long indexStart) throws IOException
{
if (keysWritten == nextSamplePosition)
{
if ((entries.length() + getEntrySize(length)) <= Integer.MAX_VALUE)
{
offsets.writeInt((int) entries.length());
entries.write(keyBytes, offset, length);
entries.writeLong(indexStart);
setNextSamplePosition(keysWritten);
}
else
{
// we cannot fully sample this sstable due to too much memory in the index summary, so let's tell the user
logger.error("Memory capacity of index summary exceeded (2GiB), index summary will not cover full sstable, " +
"you should increase min_sampling_level");
}
}
keysWritten++;
return this;
}
/** /**
* *

View File

@ -23,6 +23,8 @@ import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.sstable.ClusteringDescriptor;
import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus; import com.clearspring.analytics.stream.cardinality.HyperLogLogPlus;
import com.clearspring.analytics.stream.cardinality.ICardinality; import com.clearspring.analytics.stream.cardinality.ICardinality;
import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Clustering;
@ -105,7 +107,7 @@ public class MetadataCollector implements PartitionStatisticsCollector
} }
protected EstimatedHistogram estimatedPartitionSize = defaultPartitionSizeHistogram(); protected EstimatedHistogram estimatedPartitionSize = defaultPartitionSizeHistogram();
// TODO: cound the number of row per partition (either with the number of cells, or instead) // TODO: count the number of row per partition (either with the number of cells, or instead)
protected EstimatedHistogram estimatedCellPerPartitionCount = defaultCellPerPartitionCountHistogram(); protected EstimatedHistogram estimatedCellPerPartitionCount = defaultCellPerPartitionCountHistogram();
protected IntervalSet<CommitLogPosition> commitLogIntervals = IntervalSet.empty(); protected IntervalSet<CommitLogPosition> commitLogIntervals = IntervalSet.empty();
protected final MinMaxLongTracker timestampTracker = new MinMaxLongTracker(); protected final MinMaxLongTracker timestampTracker = new MinMaxLongTracker();
@ -122,6 +124,8 @@ public class MetadataCollector implements PartitionStatisticsCollector
* be a corresponding start bound that is smaller). * be a corresponding start bound that is smaller).
*/ */
private ClusteringPrefix<?> minClustering = ClusteringBound.MAX_START; private ClusteringPrefix<?> minClustering = ClusteringBound.MAX_START;
private final ClusteringDescriptor minClusteringDescriptor;
/** /**
* The largest clustering prefix for any {@link Unfiltered} in the sstable. * The largest clustering prefix for any {@link Unfiltered} in the sstable.
* *
@ -129,6 +133,7 @@ public class MetadataCollector implements PartitionStatisticsCollector
* be a corresponding end bound that is bigger). * be a corresponding end bound that is bigger).
*/ */
private ClusteringPrefix<?> maxClustering = ClusteringBound.MIN_END; private ClusteringPrefix<?> maxClustering = ClusteringBound.MIN_END;
private final ClusteringDescriptor maxClusteringDescriptor;
protected boolean hasLegacyCounterShards = false; protected boolean hasLegacyCounterShards = false;
private boolean hasPartitionLevelDeletions = false; private boolean hasPartitionLevelDeletions = false;
@ -159,6 +164,9 @@ public class MetadataCollector implements PartitionStatisticsCollector
{ {
this.comparator = comparator; this.comparator = comparator;
this.originatingHostId = originatingHostId; this.originatingHostId = originatingHostId;
AbstractType<?>[] clusteringTypes = comparator.subtypes().toArray(AbstractType[]::new);
this.minClusteringDescriptor = new ClusteringDescriptor(clusteringTypes).resetMaxStart();
this.maxClusteringDescriptor = new ClusteringDescriptor(clusteringTypes).resetMinEnd();
} }
public MetadataCollector(Iterable<SSTableReader> sstables, ClusteringComparator comparator) public MetadataCollector(Iterable<SSTableReader> sstables, ClusteringComparator comparator)
@ -185,6 +193,14 @@ public class MetadataCollector implements PartitionStatisticsCollector
return this; return this;
} }
public MetadataCollector addKey(byte[] key, int offset, int length)
{
long hashed = MurmurHash.hash2_64(key, offset, length, 0);
cardinality.offerHashed(hashed);
totalTombstones = 0;
return this;
}
public MetadataCollector addPartitionSizeInBytes(long partitionSize) public MetadataCollector addPartitionSizeInBytes(long partitionSize)
{ {
estimatedPartitionSize.add(partitionSize); estimatedPartitionSize.add(partitionSize);
@ -236,6 +252,15 @@ public class MetadataCollector implements PartitionStatisticsCollector
updateTombstoneCount(); updateTombstoneCount();
} }
/**
* Cell level stats, if we accept that LDT and LET are the same...
*/
public void updateCellLiveness(LivenessInfo newInfo)
{
++currentPartitionCells;
update(newInfo);
}
public void updatePartitionDeletion(DeletionTime dt) public void updatePartitionDeletion(DeletionTime dt)
{ {
if (!dt.isLive()) if (!dt.isLive())
@ -299,6 +324,32 @@ public class MetadataCollector implements PartitionStatisticsCollector
return this; return this;
} }
public void updateClusteringValues(ClusteringDescriptor newClustering) {
// In a SSTable, every opening marker will be closed, so the start of a range tombstone marker will never be
// the maxClustering (the corresponding close might though) and there is no point in doing the comparison
// (and vice-versa for the close). By the same reasoning, a boundary will never be either the min or max
// clustering, and we can save on comparisons.
if (newClustering == null || newClustering.clusteringKind().isBoundary())
return;
// In case of monotonically growing stream of clusterings, we will usually require only one comparison
// because if we detected X is greater than the current MAX, then it cannot be lower than the current MIN
// at the same time. The only case when we need to update MIN when the current MAX was detected to be updated
// is the case when MIN was not yet initialized and still point the ClusteringBound.MAX_START
if (ClusteringComparator.compare(newClustering, maxClusteringDescriptor) > 0)
{
maxClusteringDescriptor.copy(newClustering);
if (minClusteringDescriptor.isMaxStart()) // min is unset
{
minClusteringDescriptor.copy(newClustering);
}
}
else if (ClusteringComparator.compare(newClustering, minClusteringDescriptor) < 0)
{
minClusteringDescriptor.copy(newClustering);
}
}
public void updateClusteringValues(Clustering<?> clustering) public void updateClusteringValues(Clustering<?> clustering)
{ {
if (clustering == Clustering.STATIC_CLUSTERING) if (clustering == Clustering.STATIC_CLUSTERING)
@ -361,6 +412,13 @@ public class MetadataCollector implements PartitionStatisticsCollector
Map<MetadataType, MetadataComponent> components = new EnumMap<>(MetadataType.class); Map<MetadataType, MetadataComponent> components = new EnumMap<>(MetadataType.class);
components.put(MetadataType.VALIDATION, new ValidationMetadata(partitioner, bloomFilterFPChance)); components.put(MetadataType.VALIDATION, new ValidationMetadata(partitioner, bloomFilterFPChance));
Slice coveredClustering;
if (!minClusteringDescriptor.isMaxStart()) // min is end only if the descriptors are unused
{
minClustering = minClusteringDescriptor.toClusteringPrefix(comparator.subtypes());
maxClustering = maxClusteringDescriptor.toClusteringPrefix(comparator.subtypes());
}
coveredClustering = Slice.make(minClustering.retainable().asStartBound(), maxClustering.retainable().asEndBound());
components.put(MetadataType.STATS, new StatsMetadata(estimatedPartitionSize, components.put(MetadataType.STATS, new StatsMetadata(estimatedPartitionSize,
estimatedCellPerPartitionCount, estimatedCellPerPartitionCount,
commitLogIntervals, commitLogIntervals,
@ -374,7 +432,7 @@ public class MetadataCollector implements PartitionStatisticsCollector
estimatedTombstoneDropTime.build(), estimatedTombstoneDropTime.build(),
sstableLevel, sstableLevel,
comparator.subtypes(), comparator.subtypes(),
Slice.make(minClustering.retainable().asStartBound(), maxClustering.retainable().asEndBound()), coveredClustering,
hasLegacyCounterShards, hasLegacyCounterShards,
repairedAt, repairedAt,
totalColumnsSet, totalColumnsSet,

View File

@ -0,0 +1,151 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.util;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.apache.cassandra.utils.vint.VIntCoding;
import org.jctools.util.Pow2;
public class ResizableByteBuffer
{
private int length = 0;
private byte[] bytes = new byte[64];
private ByteBuffer buffer = ByteBuffer.wrap(bytes);
/**
* Loads the length (UnsignedShort) from the data reader, and then loads the new data into the buffer.
*
* @param dataReader data source
* @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise
* @throws IOException
*/
public final boolean loadShortLength(RandomAccessReader dataReader) throws IOException
{
int newLength = dataReader.readUnsignedShort();
return load(dataReader, newLength);
}
/**
* Loads the new data (of the given length) into the buffer. Buffer is rewritten.
*
* @param dataReader data source
* @param newLength new data length
* @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise
* @throws IOException
*/
public final boolean load(RandomAccessReader dataReader, int newLength) throws IOException
{
boolean resize = maybeResizeForOverwrite(newLength);
dataReader.readFully(bytes, 0, newLength);
setLength(newLength);
return resize;
}
public final void resetBuffer() {
setLength(0);
}
protected final byte[] bytes()
{
return bytes;
}
protected final ByteBuffer buffer()
{
return buffer;
}
protected final int length()
{
return length;
}
protected final void overwrite(byte[] newBytes, int newLength)
{
maybeResizeForOverwrite(newLength);
System.arraycopy(newBytes, 0, bytes, 0, newLength);
setLength(newLength);
}
private void setLength(int newLength)
{
int currentLength = length;
if (newLength != currentLength)
{
length = newLength;
buffer.limit(newLength);
}
}
/**
* Loads new partial data (of the given length) into the buffer. New data is appended.
*
* @param dataReader data source
* @param partLength new data length
* @return true if the underlying array/buffer had to be resized to accomodate the new data, false otherwise
* @throws IOException
*/
public final boolean loadPart(RandomAccessReader dataReader, int partLength) throws IOException
{
int newLength = length + partLength;
boolean resize = maybeResizeForAppend(newLength);
dataReader.readFully(bytes, length, partLength);
setLength(newLength);
return resize;
}
public final boolean writeUnsignedVInt(long value)
{
int currentPosition = length;
int newLength = currentPosition + 9;
boolean resize = maybeResizeForAppend(newLength);
VIntCoding.writeUnsignedVInt(value, buffer.position(currentPosition).limit(newLength));
setLength(buffer.position());
buffer.position(0);
return resize;
}
private boolean maybeResizeForOverwrite(int newLength)
{
if (newLength > bytes.length)
{
bytes = new byte[Pow2.roundToPowerOfTwo(newLength)];
buffer = ByteBuffer.wrap(bytes);
return true;
}
return false;
}
private boolean maybeResizeForAppend(int newLength)
{
if (newLength > bytes.length)
{
int currLength = length;
byte[] currBytes = bytes;
bytes = new byte[Pow2.roundToPowerOfTwo(newLength)];
if (currLength != 0)
System.arraycopy(currBytes,0 , bytes, 0, currLength);
buffer = ByteBuffer.wrap(bytes);
return true;
}
return false;
}
}

View File

@ -436,8 +436,15 @@ public final class JsonTransformer
else else
{ {
AbstractType<?> type = column.cellValueType(); AbstractType<?> type = column.cellValueType();
try
{
json.writeRawValue(type.toJSONString(clustering.get(i), clustering.accessor(), ProtocolVersion.CURRENT)); json.writeRawValue(type.toJSONString(clustering.get(i), clustering.accessor(), ProtocolVersion.CURRENT));
} }
catch (Exception e)
{
json.writeString("unsupported conversion to JSON");
}
}
} }
json.writeEndArray(); json.writeEndArray();
objectIndenter.setCompact(false); objectIndenter.setCompact(false);
@ -552,8 +559,14 @@ public final class JsonTransformer
else else
{ {
json.writeFieldName("value"); json.writeFieldName("value");
try
{
json.writeRawValue(cellType.toJSONString(cell.value(), cell.accessor(), ProtocolVersion.CURRENT)); json.writeRawValue(cellType.toJSONString(cell.value(), cell.accessor(), ProtocolVersion.CURRENT));
} }
catch (Exception e) {
json.writeString("unsupported conversion to JSON");
}
}
if (liveInfo.isEmpty() || cell.timestamp() != liveInfo.timestamp()) if (liveInfo.isEmpty() || cell.timestamp() != liveInfo.timestamp())
{ {
json.writeFieldName("tstamp"); json.writeFieldName("tstamp");

View File

@ -101,6 +101,22 @@ public class BloomFilter extends WrappedSharedCloseable implements IFilter
return indexes; return indexes;
} }
private long[] indexes(byte[] key, int offset, int length)
{
// we use the same array both for storing the hash result, and for storing the indexes we return,
// so that we do not need to allocate two arrays.
long[] indexes = reusableIndexes.get();
return indexes(key, offset, length, indexes);
}
private long[] indexes(byte[] key, int offset, int length, long[] indexes)
{
MurmurHash.hash3_x64_128(key, offset, length, 0, indexes);
setIndexes(indexes[1], indexes[0], hashCount, bitset.capacity(), indexes);
return indexes;
}
@Inline @Inline
private void setIndexes(long base, long inc, int count, long max, long[] results) private void setIndexes(long base, long inc, int count, long max, long[] results)
{ {
@ -121,6 +137,24 @@ public class BloomFilter extends WrappedSharedCloseable implements IFilter
} }
} }
public void add(byte[] key, int offset, int length)
{
long[] indexes = indexes(key, offset, length);
for (int i = 0; i < hashCount; i++)
{
bitset.set(indexes[i]);
}
}
public void add(byte[] key, int offset, int length, long[] indexes)
{
indexes(key, offset, length, indexes);
for (int i = 0; i < hashCount; i++)
{
bitset.set(indexes[i]);
}
}
@Override @Override
public final boolean isPresent(FilterKey key) public final boolean isPresent(FilterKey key)
{ {

View File

@ -46,6 +46,11 @@ public class ByteArrayUtil
return FastByteOperations.compareUnsigned(o1, off1, len, o2, off2, len); return FastByteOperations.compareUnsigned(o1, off1, len, o2, off2, len);
} }
public static int compareUnsigned(byte[] o1, int off1, int len1, byte[] o2, int off2, int len2)
{
return FastByteOperations.compareUnsigned(o1, off1, len1, o2, off2, len2);
}
public static byte[] bytes(byte b) public static byte[] bytes(byte b)
{ {
return new byte[] {b}; return new byte[] {b};
@ -234,6 +239,16 @@ public class ByteArrayUtil
out.write(buffer); out.write(buffer);
} }
public static void writeWithShortLength(byte[] buffer, int offset, int length, DataOutput out) throws IOException
{
assert length <= buffer.length;
assert offset <= length;
assert length <= FBUtilities.MAX_UNSIGNED_SHORT
: String.format("Attempted serializing to buffer exceeded maximum of %s bytes: %s", FBUtilities.MAX_UNSIGNED_SHORT, length);
out.writeShort(length);
out.write(buffer, offset, length);
}
public static void writeWithVIntLength(byte[] bytes, DataOutputPlus out) throws IOException public static void writeWithVIntLength(byte[] bytes, DataOutputPlus out) throws IOException
{ {
out.writeUnsignedVInt32(bytes.length); out.writeUnsignedVInt32(bytes.length);

View File

@ -93,6 +93,66 @@ public class MurmurHash
return h; return h;
} }
/**
* We can go all in and do a unified unsafe implementation to serve arrays/direct memory and layer the byte buffer
* types on top. Left for later.
*/
public static long hash2_64(byte[] key, int offset, int length, long seed)
{
long m64 = 0xc6a4a7935bd1e995L;
int r64 = 47;
long h64 = (seed & 0xffffffffL) ^ (m64 * length);
int lenLongs = length >> 3;
for (int i = 0; i < lenLongs; ++i)
{
int i_8 = i << 3;
long k64 = ((long) key[offset+i_8+0] & 0xff) + (((long) key[offset+i_8+1] & 0xff)<<8) +
(((long) key[offset+i_8+2] & 0xff)<<16) + (((long) key[offset+i_8+3] & 0xff)<<24) +
(((long) key[offset+i_8+4] & 0xff)<<32) + (((long) key[offset+i_8+5] & 0xff)<<40) +
(((long) key[offset+i_8+6] & 0xff)<<48) + (((long) key[offset+i_8+7] & 0xff)<<56);
k64 *= m64;
k64 ^= k64 >>> r64;
k64 *= m64;
h64 ^= k64;
h64 *= m64;
}
int rem = length & 0x7;
switch (rem)
{
case 0:
break;
case 7:
h64 ^= (long) key[offset + length - rem + 6] << 48;
case 6:
h64 ^= (long) key[offset + length - rem + 5] << 40;
case 5:
h64 ^= (long) key[offset + length - rem + 4] << 32;
case 4:
h64 ^= (long) key[offset + length - rem + 3] << 24;
case 3:
h64 ^= (long) key[offset + length - rem + 2] << 16;
case 2:
h64 ^= (long) key[offset + length - rem + 1] << 8;
case 1:
h64 ^= (long) key[offset + length - rem];
h64 *= m64;
}
h64 ^= h64 >>> r64;
h64 *= m64;
h64 ^= h64 >>> r64;
return h64;
}
public static long hash2_64(ByteBuffer key, int offset, int length, long seed) public static long hash2_64(ByteBuffer key, int offset, int length, long seed)
{ {
long m64 = 0xc6a4a7935bd1e995L; long m64 = 0xc6a4a7935bd1e995L;
@ -159,6 +219,16 @@ public class MurmurHash
(((long) key.get(blockOffset + 6) & 0xff) << 48) + (((long) key.get(blockOffset + 7) & 0xff) << 56); (((long) key.get(blockOffset + 6) & 0xff) << 48) + (((long) key.get(blockOffset + 7) & 0xff) << 56);
} }
protected static long getBlock(byte[] key, int offset, int index)
{
int i_8 = index << 3;
int blockOffset = offset + i_8;
return ((long) key[blockOffset + 0] & 0xff) + (((long) key[blockOffset + 1] & 0xff) << 8) +
(((long) key[blockOffset + 2] & 0xff) << 16) + (((long) key[blockOffset + 3] & 0xff) << 24) +
(((long) key[blockOffset + 4] & 0xff) << 32) + (((long) key[blockOffset + 5] & 0xff) << 40) +
(((long) key[blockOffset + 6] & 0xff) << 48) + (((long) key[blockOffset + 7] & 0xff) << 56);
}
protected static long rotl64(long v, int n) protected static long rotl64(long v, int n)
{ {
return ((v << n) | (v >>> (64 - n))); return ((v << n) | (v >>> (64 - n)));
@ -251,6 +321,82 @@ public class MurmurHash
result[1] = h2; result[1] = h2;
} }
public static void hash3_x64_128(byte[] key, int offset, int length, long seed, long[] result)
{
final int nblocks = length >> 4; // Process as 128-bit blocks.
long h1 = seed;
long h2 = seed;
long c1 = 0x87c37b91114253d5L;
long c2 = 0x4cf5ad432745937fL;
//----------
// body
for(int i = 0; i < nblocks; i++)
{
long k1 = getBlock(key, offset, i * 2 + 0);
long k2 = getBlock(key, offset, i * 2 + 1);
k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1;
h1 = rotl64(h1,27); h1 += h2; h1 = h1*5+0x52dce729;
k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2;
h2 = rotl64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5;
}
//----------
// tail
// Advance offset to the unprocessed tail of the data.
offset += nblocks * 16;
long k1 = 0;
long k2 = 0;
switch(length & 15)
{
case 15: k2 ^= ((long) key[offset+14]) << 48;
case 14: k2 ^= ((long) key[offset+13]) << 40;
case 13: k2 ^= ((long) key[offset+12]) << 32;
case 12: k2 ^= ((long) key[offset+11]) << 24;
case 11: k2 ^= ((long) key[offset+10]) << 16;
case 10: k2 ^= ((long) key[offset+9]) << 8;
case 9: k2 ^= ((long) key[offset+8]) << 0;
k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2;
case 8: k1 ^= ((long) key[offset+7]) << 56;
case 7: k1 ^= ((long) key[offset+6]) << 48;
case 6: k1 ^= ((long) key[offset+5]) << 40;
case 5: k1 ^= ((long) key[offset+4]) << 32;
case 4: k1 ^= ((long) key[offset+3]) << 24;
case 3: k1 ^= ((long) key[offset+2]) << 16;
case 2: k1 ^= ((long) key[offset+1]) << 8;
case 1: k1 ^= ((long) key[offset]);
k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1;
};
//----------
// finalization
h1 ^= length; h2 ^= length;
h1 += h2;
h2 += h1;
h1 = fmix(h1);
h2 = fmix(h2);
h1 += h2;
h2 += h1;
result[0] = h1;
result[1] = h2;
}
protected static long invRotl64(long v, int n) protected static long invRotl64(long v, int n)
{ {
return ((v >>> n) | (v << (64 - n))); return ((v >>> n) | (v << (64 - n)));

View File

@ -36,7 +36,7 @@ public interface OutputHandler
void debug(String msg); void debug(String msg);
default void debug(String msg, Object ... args) default void debug(String msg, Object ... args)
{ {
debug(String.format(msg, args)); if (isDebugEnabled()) debug(String.format(msg, args));
} }
// called when the user needs to be warn // called when the user needs to be warn
@ -55,6 +55,8 @@ public interface OutputHandler
warn(String.format(msg, args)); warn(String.format(msg, args));
} }
boolean isDebugEnabled();
class LogOutput implements OutputHandler class LogOutput implements OutputHandler
{ {
private static Logger logger = LoggerFactory.getLogger(LogOutput.class); private static Logger logger = LoggerFactory.getLogger(LogOutput.class);
@ -66,7 +68,7 @@ public interface OutputHandler
public void debug(String msg) public void debug(String msg)
{ {
logger.trace(msg); logger.debug(msg);
} }
public void warn(String msg) public void warn(String msg)
@ -78,6 +80,18 @@ public interface OutputHandler
{ {
logger.warn(msg, th); logger.warn(msg, th);
} }
public boolean isDebugEnabled() { return logger.isDebugEnabled();}
}
class NullOutput implements OutputHandler
{
public void output(String msg) {}
public void debug(String msg, Object ... args) {}
public void debug(String msg) {}
public void warn(String msg) {}
public void warn(Throwable th, String msg) {}
public boolean isDebugEnabled() { return false;}
} }
class SystemOutput implements OutputHandler class SystemOutput implements OutputHandler
@ -120,5 +134,10 @@ public interface OutputHandler
if (printStack && th != null) if (printStack && th != null)
th.printStackTrace(warnOut); th.printStackTrace(warnOut);
} }
public boolean isDebugEnabled()
{
return debug;
}
} }
} }

View File

@ -230,6 +230,7 @@ public class VIntCoding
{ {
return getUnsignedVInt(input, readerIndex, input.limit()); return getUnsignedVInt(input, readerIndex, input.limit());
} }
public static long getUnsignedVInt(ByteBuffer input, int readerIndex, int readerLimit) public static long getUnsignedVInt(ByteBuffer input, int readerIndex, int readerLimit)
{ {
if (readerIndex < 0) if (readerIndex < 0)

View File

@ -136,6 +136,6 @@ CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/test/conf/"
CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/classes/" CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/classes/"
CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/lib/jars/*" CLASSPATH="$CLASSPATH:$CASSANDRA_HOME/build/test/lib/jars/*"
exec $NUMACTL "$JAVA" -cp "$CLASSPATH" org.openjdk.jmh.Main -jvmArgs="$cassandra_parms $JVM_OPTS" "$@" exec $NUMACTL "$JAVA" -Xmx8g -XX:+UseParallelGC -cp "$CLASSPATH" org.openjdk.jmh.Main -jvmArgs="-Djmh.shutdownTimeout=120 $cassandra_parms $JVM_OPTS" "$@"
# vi:ai sw=4 ts=4 tw=0 et # vi:ai sw=4 ts=4 tw=0 et

View File

@ -338,6 +338,12 @@ public class FailingRepairTest extends TestBaseImpl implements Serializable
return Collections.emptySet(); return Collections.emptySet();
} }
@Override
public boolean isFullRange()
{
return false;
}
public TableMetadata metadata() public TableMetadata metadata()
{ {
return null; return null;

View File

@ -299,7 +299,7 @@ public abstract class AccordMigrationReadRaceTestBase extends AccordTestBase
@Test @Test
public void testRangeRouting() throws Throwable public void testRangeRouting() throws Throwable
{ {
String cql = "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(pk) > " + Murmur3Partitioner.MINIMUM.token; String cql = "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(pk) > " + Murmur3Partitioner.MINIMUM.getLongValue();
testSplitAndRetry(cql, this::loadData, result -> { testSplitAndRetry(cql, this::loadData, result -> {
assertThat(result).isDeepEqualTo(dataFlat); assertThat(result).isDeepEqualTo(dataFlat);
}); });

View File

@ -120,7 +120,7 @@ public class InteropTokenRangeTest extends TestBaseImpl
{ {
NavigableSet<Long> set = new TreeSet<>(); NavigableSet<Long> set = new TreeSet<>();
while (result.hasNext()) while (result.hasNext())
set.add(Murmur3Partitioner.instance.getToken(result.next().get("pk")).token); set.add(Murmur3Partitioner.instance.getToken(result.next().get("pk")).getLongValue());
return set; return set;
} }

View File

@ -405,10 +405,10 @@ public class SingleNodeTokenConflictTest extends StatefulASTBase
for (ByteBuffer bb : values) for (ByteBuffer bb : values)
{ {
var token = Murmur3Partitioner.instance.getToken(bb); var token = Murmur3Partitioner.instance.getToken(bb);
if (token.token > Long.MIN_VALUE + 1) if (token.getLongValue() > Long.MIN_VALUE + 1)
neighbors.add(keyForToken(token.token - 1)); neighbors.add(keyForToken(token.getLongValue() - 1));
if (token.token < Long.MAX_VALUE) if (token.getLongValue() < Long.MAX_VALUE)
neighbors.add(keyForToken(token.token + 1)); neighbors.add(keyForToken(token.getLongValue() + 1));
} }
return neighbors.build(); return neighbors.build();
} }

View File

@ -1005,9 +1005,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
SinglePartitionReadCommand sprc = (SinglePartitionReadCommand) command; SinglePartitionReadCommand sprc = (SinglePartitionReadCommand) command;
Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) sprc.partitionKey().getToken(); Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) sprc.partitionKey().getToken();
assert node.cluster.state.get().isReadReplicaFor(requestToken.token, node.matcher) : assert node.cluster.state.get().isReadReplicaFor(requestToken.getLongValue(), node.matcher) :
String.format("Node %s is not a read replica for %s. Replicas: %s", String.format("Node %s is not a read replica for %s. Replicas: %s",
node, requestToken.token, node.cluster.state.get().readReplicasFor(requestToken.token)); node, requestToken.getLongValue(), node.cluster.state.get().readReplicasFor(requestToken.getLongValue()));
} }
@Override @Override
@ -1048,7 +1048,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
Murmur3Partitioner.LongToken leftToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().left.getToken(); Murmur3Partitioner.LongToken leftToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().left.getToken();
Murmur3Partitioner.LongToken rightToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().right.getToken(); Murmur3Partitioner.LongToken rightToken = (Murmur3Partitioner.LongToken) prrc.dataRange().keyRange().right.getToken();
assert node.cluster.state.get().isReadReplicaFor(leftToken.token, rightToken.token, node.matcher); assert node.cluster.state.get().isReadReplicaFor(leftToken.getLongValue(), rightToken.getLongValue(), node.matcher);
} }
@Override @Override
@ -1085,8 +1085,8 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
{ {
Mutation command = request.payload; Mutation command = request.payload;
Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) command.key().getToken(); Murmur3Partitioner.LongToken requestToken = (Murmur3Partitioner.LongToken) command.key().getToken();
assert node.cluster.state.get().isWriteTargetFor(requestToken.token, node.matcher) : String.format("Node %s is not a write target for %s. Write placements: %s", assert node.cluster.state.get().isWriteTargetFor(requestToken.getLongValue(), node.matcher) : String.format("Node %s is not a write target for %s. Write placements: %s",
node.node.idx(), requestToken, node.cluster.state.get().writePlacementsFor(requestToken.token)); node.node.idx(), requestToken, node.cluster.state.get().writePlacementsFor(requestToken.getLongValue()));
} }
public Message<NoPayload> respondTo(Message<Mutation> request) public Message<NoPayload> respondTo(Message<Mutation> request)
@ -1145,7 +1145,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
public static long token(Object... pk) public static long token(Object... pk)
{ {
return ((Murmur3Partitioner.LongToken) DatabaseDescriptor.getPartitioner().decorateKey(toBytes(pk)).getToken()).token; return ((Murmur3Partitioner.LongToken) DatabaseDescriptor.getPartitioner().decorateKey(toBytes(pk)).getToken()).getLongValue();
} }
public static ByteBuffer toBytes(Object... pk) public static ByteBuffer toBytes(Object... pk)

View File

@ -71,7 +71,7 @@ public class GossipDeadlockTest extends TestBaseImpl
ExecutorPlus e = ExecutorFactory.Global.executorFactory().pooled("BounceMove", 2); ExecutorPlus e = ExecutorFactory.Global.executorFactory().pooled("BounceMove", 2);
long startToken = cluster.get(2).callOnInstance(() -> { long startToken = cluster.get(2).callOnInstance(() -> {
NodeId nodeId = ClusterMetadata.current().myNodeId(); NodeId nodeId = ClusterMetadata.current().myNodeId();
return ((Murmur3Partitioner.LongToken)ClusterMetadata.current().tokenMap.tokens(nodeId).get(0)).token; return ((Murmur3Partitioner.LongToken)ClusterMetadata.current().tokenMap.tokens(nodeId).get(0)).getLongValue();
}); });
AtomicBoolean stop = new AtomicBoolean(false); AtomicBoolean stop = new AtomicBoolean(false);
Future<Integer> moves = e.submit(() -> { Future<Integer> moves = e.submit(() -> {

View File

@ -555,7 +555,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
Assert.assertEquals(replication, sut.rf.asKeyspaceParams().replication); Assert.assertEquals(replication, sut.rf.asKeyspaceParams().replication);
Assert.assertEquals(modelState.simulatedPlacements.nodes.stream().map(Node::token).collect(Collectors.toSet()), Assert.assertEquals(modelState.simulatedPlacements.nodes.stream().map(Node::token).collect(Collectors.toSet()),
actualMetadata.tokenMap.tokens().stream().map(t -> ((LongToken) t).token).collect(Collectors.toSet())); actualMetadata.tokenMap.tokens().stream().map(t -> ((LongToken) t).getLongValue()).collect(Collectors.toSet()));
for (Map.Entry<ReplicationParams, DataPlacement> e : actualMetadata.placements.asMap().entrySet()) for (Map.Entry<ReplicationParams, DataPlacement> e : actualMetadata.placements.asMap().entrySet())
{ {

View File

@ -66,8 +66,8 @@ public class LCSStreamingKeepLevelTest extends TestBaseImpl
{ {
populate(cluster); populate(cluster);
long tokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(3).getToken()).token; long tokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(3).getToken()).getLongValue();
long prevTokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(2).getToken()).token; long prevTokenVal = ((Murmur3Partitioner.LongToken)cluster.tokens().get(2).getToken()).getLongValue();
// move node 4 to the middle point between its current position and the previous node // move node 4 to the middle point between its current position and the previous node
long newToken = (tokenVal + prevTokenVal) / 2; long newToken = (tokenVal + prevTokenVal) / 2;
cluster.get(4).nodetoolResult("move", String.valueOf(newToken)).asserts().success(); cluster.get(4).nodetoolResult("move", String.valueOf(newToken)).asserts().success();

View File

@ -46,7 +46,7 @@ public class CMSPlacementAfterMoveTest extends TestBaseImpl
long node1Token = cluster.get(1).callOnInstance(() -> { long node1Token = cluster.get(1).callOnInstance(() -> {
ClusterMetadata metadata = ClusterMetadata.current(); ClusterMetadata metadata = ClusterMetadata.current();
ImmutableList<Token> tokens = ClusterMetadata.current().tokenMap.tokens(metadata.myNodeId()); ImmutableList<Token> tokens = ClusterMetadata.current().tokenMap.tokens(metadata.myNodeId());
return ((Murmur3Partitioner.LongToken) tokens.get(0)).token; return ((Murmur3Partitioner.LongToken) tokens.get(0)).getLongValue();
}); });
long newNode4Token = node1Token + 100; // token after node1s token should be in cms long newNode4Token = node1Token + 100; // token after node1s token should be in cms
cluster.get(4).nodetoolResult("move", String.valueOf(newNode4Token)); cluster.get(4).nodetoolResult("move", String.valueOf(newNode4Token));

View File

@ -238,9 +238,9 @@ public abstract class ForwardingSSTableReader extends SSTableReader
} }
@Override @Override
public KeyReader keyReader() throws IOException public KeyReader keyReader(boolean detailed) throws IOException
{ {
return delegate.keyReader(); return delegate.keyReader(detailed);
} }
public KeyReader keyReader(PartitionPosition key) throws IOException public KeyReader keyReader(PartitionPosition key) throws IOException

View File

@ -304,7 +304,7 @@ public class AccordJournalBurnTest extends BurnTestBase
while (ci.hasNext()) while (ci.hasNext())
writer.append(ci.next()); writer.append(ci.next());
ci.setTargetDirectory(writer.getSStableDirectory().path()); ci.setTargetDirectory(writer.getSStableDirectoryPath());
// point of no return // point of no return
newSStables = writer.finish(); newSStables = writer.finish();
} }

View File

@ -0,0 +1,251 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.harry.test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import accord.utils.Invariants;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.harry.ColumnSpec;
import org.apache.cassandra.harry.SchemaSpec;
import org.apache.cassandra.harry.dsl.HistoryBuilder;
import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor;
import org.apache.cassandra.harry.execution.CQLVisitExecutor;
import org.apache.cassandra.harry.execution.CompiledStatement;
import org.apache.cassandra.harry.execution.DataTracker;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.op.Operations;
import org.apache.cassandra.harry.op.Visit;
import org.apache.cassandra.harry.util.BitSet;
import org.apache.cassandra.harry.util.ThrowingRunnable;
import org.apache.cassandra.io.sstable.HarrySSTableWriter;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.service.StorageService;
import static org.apache.cassandra.harry.checker.TestHelper.withRandom;
public class HarryCompactionTest extends CQLTester
{
private static final AtomicInteger idGen = new AtomicInteger(0);
public static final int TEST_REPS = 100;
public static final int PARTITIONS_RANGE = 100;
public static final int COLUMNS_RANGE = 10;
private final Generator<BitSet> staticColumnsGenerator = BitSet.generator(3);
private final Generator<BitSet> regularColumnsGenerator = BitSet.generator(9);
private String keyspace;
private String table;
private String qualifiedTable;
private File dataDir;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
public void perTestSetup() throws IOException
{
keyspace = "cql_keyspace" + idGen.incrementAndGet();
table = "table" + idGen.incrementAndGet();
qualifiedTable = keyspace + '.' + table;
dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table);
assert dataDir.tryCreateDirectories();
ServerTestUtils.prepareServerNoRegister();
StorageService.instance.initServer();
requireNetwork();
}
private final Generator<SchemaSpec> simple_schema = rng -> {
return new SchemaSpec(rng.next(),
1000,
keyspace,
table,
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, true)),
Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType),
ColumnSpec.regularColumn("r2", ColumnSpec.int64Type),
ColumnSpec.regularColumn("r3", ColumnSpec.int8Type),
ColumnSpec.regularColumn("r4", ColumnSpec.doubleType),
ColumnSpec.regularColumn("r5", ColumnSpec.floatType),
ColumnSpec.regularColumn("r6", ColumnSpec.int32Type),
ColumnSpec.regularColumn("r7", ColumnSpec.booleanType),
ColumnSpec.regularColumn("r8", ColumnSpec.int16Type),
ColumnSpec.regularColumn("r9", ColumnSpec.textType)),
Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("s2", ColumnSpec.int64Type),
ColumnSpec.staticColumn("s3", ColumnSpec.asciiType)));
};
@Test
public void testFlushAndCompact1() throws IOException {
testFlushAndCompact(1);
}
@Test
public void testFlushAndCompact2() throws IOException {
testFlushAndCompact(2);
}
@Test
public void testFlushAndCompact3() throws IOException {
testFlushAndCompact(3);
}
@Test
public void testFlushAndCompact4() throws IOException {
testFlushAndCompact(4);
}
@Test
public void testFlushAndCompact5() throws IOException
{
testFlushAndCompact(5);
}
public void testFlushAndCompact(int flushcount) throws IOException
{
for (int i = 0; i < TEST_REPS; i++)
testFlushAndCompactOnce(flushcount);
}
public void testFlushAndCompactOnce(int flushcount) throws IOException
{
perTestSetup();
withRandom( rng -> {
SchemaSpec schema = simple_schema.generate(rng);
schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace));
createTable(schema.compile());
HistoryBuilder history = new HistoryBuilder(schema.valueGenerators);
history.customThrowing(() -> {
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
}, "disable compaction");
AtomicReference<HarrySSTableWriter> sstableWriter = new AtomicReference<>();
ThrowingRunnable flushAndChangeWriter = () -> {
HarrySSTableWriter prev = sstableWriter.get();
if (prev != null)
{
prev.close();
StorageService.instance.bulkLoad(dataDir.absolutePath());
dataDir.forEach(file -> file.delete());
}
Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder()
.forTable(schema.compile())
.inDirectory(dataDir)
.build()) == prev);
};
flushAndChangeWriter.run();
for (int sstablesFlushed = 0; sstablesFlushed < flushcount; sstablesFlushed++)
{
for (int i = 0; i < PARTITIONS_RANGE; i++)
{
for (int j = 0; j < COLUMNS_RANGE; j++)
{
history.insert(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * COLUMNS_RANGE)); // some overlap, but not all
history.deleteRow(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * COLUMNS_RANGE)); // some overlap, but not all
history.deleteColumns(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 20),
regularColumnsGenerator.generate(rng),
staticColumnsGenerator.generate(rng));
}
history.deletePartition(rng.nextInt(0, 2 * PARTITIONS_RANGE)); // some overlap, but not all
}
history.customThrowing(flushAndChangeWriter, "flush sstable" + sstablesFlushed);
}
history.customThrowing(() -> {
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.forceMajorCompaction();
}, "major compaction");
for (int i = 0; i < 2*PARTITIONS_RANGE; i++)
history.selectPartition(i);
replay(schema, history, sstableWriter::get);
});
}
public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
CQLVisitExecutor executor = create(schema, historyBuilder, writer);
for (Visit visit : historyBuilder)
executor.execute(visit);
}
public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
DataTracker tracker = new DataTracker.SequentialDataTracker();
return new CQLTesterVisitExecutor(schema, tracker,
new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder),
statement -> {
if (logger.isTraceEnabled())
logger.trace(statement.toString());
return execute(statement.cql(), statement.bindings());
})
{
@Override
protected void executeMutatingVisit(Visit visit, CompiledStatement statement)
{
try
{
writer.get().addRow(statement.cql(), statement.bindings());
}
catch (IOException e)
{
throw new UncheckedIOException(e);
}
}
@Override
protected void executeValidatingVisit(Visit visit, List<Operations.SelectStatement> selects, CompiledStatement compiledStatement)
{
super.executeValidatingVisit(visit, selects, compiledStatement);
}
@Override
public void execute(Visit visit)
{
if (visit.visitedPartitions.size() > 1)
throw new IllegalStateException("SSTable Generator does not support batch statements and transactions");
super.execute(visit);
}
};
}
}

View File

@ -0,0 +1,256 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.harry.test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import accord.utils.Invariants;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.harry.ColumnSpec;
import org.apache.cassandra.harry.SchemaSpec;
import org.apache.cassandra.harry.dsl.HistoryBuilder;
import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor;
import org.apache.cassandra.harry.execution.CQLVisitExecutor;
import org.apache.cassandra.harry.execution.CompiledStatement;
import org.apache.cassandra.harry.execution.DataTracker;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.op.Operations;
import org.apache.cassandra.harry.op.Visit;
import org.apache.cassandra.harry.util.BitSet;
import org.apache.cassandra.harry.util.ThrowingRunnable;
import org.apache.cassandra.io.sstable.HarrySSTableWriter;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.service.StorageService;
import static org.apache.cassandra.harry.checker.TestHelper.withRandom;
public class HarryCompactionWithRangeDeletionsTest extends CQLTester
{
private static final AtomicInteger idGen = new AtomicInteger(0);
public static final int TEST_REPS = 100;
public static final int PARTITIONS_RANGE = 100;
public static final int ROWS_RANGE = 10;
public static final int STATIC_COLS = 3;
public static final int REG_COLS = 9;
private final Generator<BitSet> staticColumnsGenerator = BitSet.generator(STATIC_COLS);
private final Generator<BitSet> regularColumnsGenerator = BitSet.generator(REG_COLS);
private String keyspace;
private String table;
private String qualifiedTable;
private File dataDir;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
public void perTestSetup() throws IOException
{
keyspace = "cql_keyspace" + idGen.incrementAndGet();
table = "table" + idGen.incrementAndGet();
qualifiedTable = keyspace + '.' + table;
dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table);
assert dataDir.tryCreateDirectories();
ServerTestUtils.prepareServerNoRegister();
StorageService.instance.initServer();
requireNetwork();
}
private final Generator<SchemaSpec> schemaSpecGenerator = rng -> {
return new SchemaSpec(rng.next(),
1000,
keyspace,
table,
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, true)),
Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType),
ColumnSpec.regularColumn("r2", ColumnSpec.int64Type),
ColumnSpec.regularColumn("r3", ColumnSpec.int8Type),
ColumnSpec.regularColumn("r4", ColumnSpec.doubleType),
ColumnSpec.regularColumn("r5", ColumnSpec.floatType),
ColumnSpec.regularColumn("r6", ColumnSpec.int32Type),
ColumnSpec.regularColumn("r7", ColumnSpec.booleanType),
ColumnSpec.regularColumn("r8", ColumnSpec.int16Type),
ColumnSpec.regularColumn("r9", ColumnSpec.textType)),
Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("s2", ColumnSpec.int64Type),
ColumnSpec.staticColumn("s3", ColumnSpec.asciiType)));
};
@Test
public void testFlushAndCompact1() throws IOException {
testFlushAndCompact(1);
}
@Test
public void testFlushAndCompact2() throws IOException {
testFlushAndCompact(2);
}
@Test
public void testFlushAndCompact3() throws IOException {
testFlushAndCompact(3);
}
@Test
public void testFlushAndCompact4() throws IOException {
testFlushAndCompact(4);
}
@Test
public void testFlushAndCompact5() throws IOException
{
testFlushAndCompact(5);
}
public void testFlushAndCompact(int flushcount) throws IOException
{
for (int i = 0; i < TEST_REPS; i++)
testFlushAndCompactOnce(flushcount);
}
public void testFlushAndCompactOnce(int flushcount) throws IOException
{
perTestSetup();
withRandom(205413964293041L, rng -> {
SchemaSpec schema = schemaSpecGenerator.generate(rng);
schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace));
createTable(schema.compile());
HistoryBuilder history = new HistoryBuilder(schema.valueGenerators);
history.customThrowing(() -> {
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
}, "disable compaction");
AtomicReference<HarrySSTableWriter> sstableWriter = new AtomicReference<>();
ThrowingRunnable flushAndChangeWriter = () -> {
HarrySSTableWriter prev = sstableWriter.get();
if (prev != null)
{
prev.close();
StorageService.instance.bulkLoad(dataDir.absolutePath());
dataDir.forEach(file -> file.delete());
}
Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder()
.forTable(schema.compile())
.inDirectory(dataDir)
.build()) == prev);
};
flushAndChangeWriter.run();
for (int sstablesFlushed = 0; sstablesFlushed < flushcount; sstablesFlushed++)
{
for (int i = 0; i < PARTITIONS_RANGE; i++)
{
for (int j = 0; j < ROWS_RANGE; j++)
{
history.insert(rng.nextInt(0, 2 * PARTITIONS_RANGE), rng.nextInt(0, 2 * ROWS_RANGE)); // some overlap, but not all
}
}
int lowerBoundRowIdx = rng.nextInt(ROWS_RANGE);
int upperBoundRowIdx = rng.nextInt(lowerBoundRowIdx, 2 * ROWS_RANGE);
history.deleteRowRange(rng.nextInt(0, 2 * PARTITIONS_RANGE),
lowerBoundRowIdx,
upperBoundRowIdx,
rng.nextInt(REG_COLS),
rng.nextBoolean(),
rng.nextBoolean());
history.customThrowing(flushAndChangeWriter, "flush sstable" + sstablesFlushed);
}
history.customThrowing(() -> {
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.forceMajorCompaction();
}, "major compaction");
for (int i = 0; i < 2 * PARTITIONS_RANGE; i++)
history.selectPartition(i);
replay(schema, history, sstableWriter::get);
});
}
public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
CQLVisitExecutor executor = create(schema, historyBuilder, writer);
for (Visit visit : historyBuilder)
executor.execute(visit);
}
public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
DataTracker tracker = new DataTracker.SequentialDataTracker();
return new CQLTesterVisitExecutor(schema, tracker,
new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder),
statement -> {
if (logger.isTraceEnabled())
logger.trace(statement.toString());
return execute(statement.cql(), statement.bindings());
})
{
@Override
protected void executeMutatingVisit(Visit visit, CompiledStatement statement)
{
try
{
writer.get().addRow(statement.cql(), statement.bindings());
}
catch (IOException e)
{
throw new UncheckedIOException(e);
}
}
@Override
protected void executeValidatingVisit(Visit visit, List<Operations.SelectStatement> selects, CompiledStatement compiledStatement)
{
super.executeValidatingVisit(visit, selects, compiledStatement);
}
@Override
public void execute(Visit visit)
{
if (visit.visitedPartitions.size() > 1)
throw new IllegalStateException("SSTable Generator does not support batch statements and transactions");
super.execute(visit);
}
};
}
}

View File

@ -0,0 +1,190 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.harry.test;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import accord.utils.Invariants;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.harry.ColumnSpec;
import org.apache.cassandra.harry.SchemaSpec;
import org.apache.cassandra.harry.dsl.HistoryBuilder;
import org.apache.cassandra.harry.execution.CQLTesterVisitExecutor;
import org.apache.cassandra.harry.execution.CQLVisitExecutor;
import org.apache.cassandra.harry.execution.CompiledStatement;
import org.apache.cassandra.harry.execution.DataTracker;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.op.Operations;
import org.apache.cassandra.harry.op.Visit;
import org.apache.cassandra.harry.util.ThrowingRunnable;
import org.apache.cassandra.io.sstable.HarrySSTableWriter;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.service.StorageService;
import static org.apache.cassandra.harry.checker.TestHelper.withRandom;
public class HarrySSTableWriterTest extends CQLTester
{
private static final AtomicInteger idGen = new AtomicInteger(0);
private static final int NUMBER_WRITES_IN_RUNNABLE = 10;
private String keyspace;
private String table;
private String qualifiedTable;
private File dataDir;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Before
public void perTestSetup() throws IOException
{
keyspace = "cql_keyspace" + idGen.incrementAndGet();
table = "table" + idGen.incrementAndGet();
qualifiedTable = keyspace + '.' + table;
dataDir = new File(tempFolder.newFolder().getAbsolutePath() + File.pathSeparator() + keyspace + File.pathSeparator() + table);
assert dataDir.tryCreateDirectories();
ServerTestUtils.prepareServerNoRegister();
StorageService.instance.initServer();
requireNetwork();
}
private final Generator<SchemaSpec> simple_schema = rng -> {
return new SchemaSpec(rng.next(),
1000,
keyspace,
table,
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("r1", ColumnSpec.asciiType),
ColumnSpec.regularColumn("r2", ColumnSpec.int64Type),
ColumnSpec.regularColumn("r3", ColumnSpec.asciiType)),
Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("s2", ColumnSpec.int64Type),
ColumnSpec.staticColumn("s3", ColumnSpec.asciiType)));
};
@Test
public void generateSSTableTest()
{
withRandom(rng -> {
SchemaSpec schema = simple_schema.generate(rng);
schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", schema.keyspace));
createTable(schema.compile());
HistoryBuilder history = new HistoryBuilder(schema.valueGenerators);
for (int i = 0; i < 100; i++)
history.insert(1);
AtomicReference<HarrySSTableWriter> sstableWriter = new AtomicReference<>();
ThrowingRunnable resetWriter = () -> {
HarrySSTableWriter prev = sstableWriter.get();
if (prev != null)
{
prev.close();
StorageService.instance.bulkLoad(dataDir.absolutePath());
}
Invariants.require(sstableWriter.getAndSet(HarrySSTableWriter.builder()
.forTable(schema.compile())
.inDirectory(dataDir)
.build()) == prev);
};
resetWriter.run();
for (int i = 0; i < 100; i++)
{
for (int j = 0; j < 10; j++)
history.insert(i, j);
}
history.customThrowing(resetWriter, "flush sstable");
for (int i = 0; i < 100; i++)
history.selectPartition(i);
replay(schema, history, sstableWriter::get);
});
}
public void replay(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
CQLVisitExecutor executor = create(schema, historyBuilder, writer);
for (Visit visit : historyBuilder)
executor.execute(visit);
}
public CQLVisitExecutor create(SchemaSpec schema, HistoryBuilder historyBuilder, Supplier<HarrySSTableWriter> writer)
{
DataTracker tracker = new DataTracker.SequentialDataTracker();
return new CQLTesterVisitExecutor(schema, tracker,
new QuiescentChecker(schema.valueGenerators, tracker, historyBuilder),
statement -> {
if (logger.isTraceEnabled())
logger.trace(statement.toString());
return execute(statement.cql(), statement.bindings());
})
{
@Override
protected void executeMutatingVisit(Visit visit, CompiledStatement statement)
{
try
{
writer.get().addRow(statement.cql(), statement.bindings());
}
catch (IOException e)
{
throw new UncheckedIOException(e);
}
}
@Override
protected void executeValidatingVisit(Visit visit, List<Operations.SelectStatement> selects, CompiledStatement compiledStatement)
{
super.executeValidatingVisit(visit, selects, compiledStatement);
}
@Override
public void execute(Visit visit)
{
if (visit.visitedPartitions.size() > 1)
throw new IllegalStateException("SSTable Generator does not support batch statements and transactions");
super.execute(visit);
}
};
}
}

View File

@ -0,0 +1,656 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.io.sstable;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NavigableSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UpdateParameters;
import org.apache.cassandra.cql3.functions.types.TypeCodec;
import org.apache.cassandra.cql3.statements.ModificationStatement;
import org.apache.cassandra.cql3.statements.schema.CreateIndexStatement;
import org.apache.cassandra.cql3.statements.schema.CreateTableStatement;
import org.apache.cassandra.cql3.statements.schema.CreateTypeStatement;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Slice;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.index.sai.StorageAttachedIndexGroup;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.Keyspaces;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.SchemaTransformation;
import org.apache.cassandra.schema.SchemaTransformations;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableMetadataRef;
import org.apache.cassandra.schema.Tables;
import org.apache.cassandra.schema.Types;
import org.apache.cassandra.schema.UserFunctions;
import org.apache.cassandra.schema.Views;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.transformations.AlterSchema;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.JavaDriverUtils;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
public class HarrySSTableWriter implements Closeable
{
public static final ByteBuffer UNSET_VALUE = ByteBufferUtil.UNSET_BYTE_BUFFER;
static
{
CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.setBoolean(true);
DatabaseDescriptor.clientInitialization(false);
// Partitioner is not set in client mode.
if (DatabaseDescriptor.getPartitioner() == null)
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
ClusterMetadataService.initializeForClients();
}
private final AbstractSSTableSimpleWriter writer;
private HarrySSTableWriter(AbstractSSTableSimpleWriter writer)
{
this.writer = writer;
}
public static Builder builder()
{
return new Builder();
}
public HarrySSTableWriter addRow(String cql, Object... values) throws IOException
{
ModificationStatement statement = prepare(cql);
List<ColumnSpecification> boundNames = statement.getBindVariables();
// TODO: avoid materializing this
List<TypeCodec<Object>> typeCodecs = boundNames.stream()
.map(bn -> JavaDriverUtils.codecFor(JavaDriverUtils.driverType(bn.type)))
.collect(Collectors.toList());
int size = Math.min(values.length, boundNames.size());
List<ByteBuffer> rawValues = new ArrayList<>(size);
for (int i = 0; i < size; i++)
{
Object value = values[i];
rawValues.add(serialize(value, typeCodecs.get(i), boundNames.get(i)));
}
return rawAddRow(statement, rawValues, boundNames);
}
private ModificationStatement prepare(String cql)
{
ModificationStatement.Parsed statement = QueryProcessor.parseStatement(cql,
ModificationStatement.Parsed.class,
"INSERT/UPDATE/DELETE");
ClientState state = ClientState.forInternalCalls();
ModificationStatement preparedModificationStatement = statement.prepare(state);
preparedModificationStatement.validate(state);
if (preparedModificationStatement.hasConditions())
throw new IllegalArgumentException("Conditional statements are not supported");
if (preparedModificationStatement.isCounter())
throw new IllegalArgumentException("Counter modification statements are not supported");
if (preparedModificationStatement.getBindVariables().isEmpty())
throw new IllegalArgumentException("Provided preparedModificationStatement statement has no bind variables");
return preparedModificationStatement;
}
/**
* Adds a new row to the writer given already serialized values.
* <p>
* This is a shortcut for {@code rawAddRow(Arrays.asList(values))}.
*
* @param values the row values (corresponding to the bind variables of the
* modification statement used when creating by this writer) as binary.
* @return this writer.
*/
public HarrySSTableWriter rawAddRow(ModificationStatement modificationStatement, List<ByteBuffer> values, List<ColumnSpecification> boundNames) throws InvalidRequestException, IOException
{
if (values.size() != boundNames.size())
throw new InvalidRequestException(String.format("Invalid number of arguments, expecting %d values but got %d", boundNames.size(), values.size()));
QueryOptions options = QueryOptions.forInternalCalls(null, values);
ClientState state = ClientState.forInternalCalls();
List<ByteBuffer> keys = modificationStatement.buildPartitionKeyNames(options, state);
long now = currentTimeMillis();
// Note that we asks indexes to not validate values (the last 'false' arg below) because that triggers a 'Keyspace.open'
// and that forces a lot of initialization that we don't want.
UpdateParameters params = new UpdateParameters(modificationStatement.metadata,
ClientState.forInternalCalls(),
options,
modificationStatement.getTimestamp(TimeUnit.MILLISECONDS.toMicros(now), options),
options.getNowInSec((int) TimeUnit.MILLISECONDS.toSeconds(now)),
modificationStatement.getTimeToLive(options),
Collections.emptyMap());
try
{
if (modificationStatement.hasSlices())
{
Slices slices = modificationStatement.createSlices(options);
for (ByteBuffer key : keys)
{
for (Slice slice : slices)
modificationStatement.addUpdateForKey(writer.getUpdateFor(key), slice, params);
}
}
else
{
NavigableSet<Clustering<?>> clusterings = modificationStatement.createClustering(options, state);
for (ByteBuffer key : keys)
{
for (Clustering clustering : clusterings)
modificationStatement.addUpdateForKey(writer.getUpdateFor(key), clustering, params);
}
}
return this;
}
catch (SSTableSimpleUnsortedWriter.SyncException e)
{
// If we use a BufferedWriter and had a problem writing to disk, the IOException has been
// wrapped in a SyncException (see BufferedWriter below). We want to extract that IOE.
throw (IOException) e.getCause();
}
}
/**
* Close this writer.
* <p>
* This method should be called, otherwise the produced sstables are not
* guaranteed to be complete (and won't be in practice).
*/
public void close() throws IOException
{
writer.close();
}
private ByteBuffer serialize(Object value, TypeCodec codec, ColumnSpecification columnSpecification)
{
if (value == null || value == UNSET_VALUE)
return (ByteBuffer) value;
try
{
return codec.serialize(value, ProtocolVersion.CURRENT);
}
catch (ClassCastException cce)
{
// For backwards-compatibility with consumers that may be passing
// an Integer for a Date field, for example.
return ((AbstractType) columnSpecification.type).decompose(value);
}
}
/**
* A Builder for a CQLSSTableWriter object.
*/
public static class Builder
{
private static final Logger logger = LoggerFactory.getLogger(Builder.class);
private static final long DEFAULT_BUFFER_SIZE_IN_MIB_FOR_UNSORTED = 128L;
protected SSTableFormat<?, ?> format = null;
private final List<CreateTypeStatement.Raw> typeStatements;
private final List<CreateIndexStatement.Raw> indexStatements;
private File directory;
private CreateTableStatement.Raw schemaStatement;
private IPartitioner partitioner;
private boolean sorted = false;
private long maxSSTableSizeInMiB = -1L;
private boolean buildIndexes = true;
private Consumer<Collection<SSTableReader>> sstableProducedListener;
private boolean openSSTableOnProduced = false;
protected Builder()
{
this.typeStatements = new ArrayList<>();
this.indexStatements = new ArrayList<>();
}
/**
* The directory where to write the sstables.
* <p>
* This is a mandatory option.
*
* @param directory the directory to use, which should exists and be writable.
* @return this builder.
* @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable.
*/
public Builder inDirectory(String directory)
{
return inDirectory(new File(directory));
}
/**
* The directory where to write the sstables (mandatory option).
* <p>
* This is a mandatory option.
*
* @param directory the directory to use, which should exists and be writable.
* @return this builder.
* @throws IllegalArgumentException if {@code directory} doesn't exist or is not writable.
*/
public Builder inDirectory(File directory)
{
if (!directory.exists())
throw new IllegalArgumentException(directory + " doesn't exists");
if (!directory.isWritable())
throw new IllegalArgumentException(directory + " exists but is not writable");
this.directory = directory;
return this;
}
public Builder withType(String typeDefinition) throws SyntaxException
{
typeStatements.add(QueryProcessor.parseStatement(typeDefinition, CreateTypeStatement.Raw.class, "CREATE TYPE"));
return this;
}
/**
* The schema (CREATE TABLE statement) for the table for which sstable are to be created.
* <p>
* Please note that the provided CREATE TABLE statement <b>must</b> use a fully-qualified
* table name, one that include the keyspace name.
* <p>
* This is a mandatory option.
*
* @param schema the schema of the table for which sstables are to be created.
* @return this builder.
* @throws IllegalArgumentException if {@code schema} is not a valid CREATE TABLE statement
* or does not have a fully-qualified table name.
*/
public Builder forTable(String schema)
{
this.schemaStatement = QueryProcessor.parseStatement(schema, CreateTableStatement.Raw.class, "CREATE TABLE");
return this;
}
/**
* The schema (CREATE INDEX statement) for index to be created for the table. Only SAI indexes are supported.
*
* @param indexes CQL statements representing SAI indexes to be created.
* @return this builder
*/
public Builder withIndexes(String... indexes)
{
for (String index : indexes)
indexStatements.add(QueryProcessor.parseStatement(index, CreateIndexStatement.Raw.class, "CREATE INDEX"));
return this;
}
/**
* The partitioner to use.
* <p>
* By default, {@code Murmur3Partitioner} will be used. If this is not the partitioner used
* by the cluster for which the SSTables are created, you need to use this method to
* provide the correct partitioner.
*
* @param partitioner the partitioner to use.
* @return this builder.
*/
public Builder withPartitioner(IPartitioner partitioner)
{
this.partitioner = partitioner;
return this;
}
/**
* Defines the maximum SSTable size in mebibytes when using the sorted writer.
* By default, i.e. not specified, there is no maximum size limit for the produced SSTable
*
* @param size the maximum sizein mebibytes of each individual SSTable allowed
* @return this builder
*/
public Builder withMaxSSTableSizeInMiB(int size)
{
if (size <= 0)
{
logger.warn("A non-positive value for maximum SSTable size is specified, " +
"which disables the size limiting effectively. Please supply a positive value in order " +
"to enforce size limiting for the produced SSTables.");
}
this.maxSSTableSizeInMiB = size;
return this;
}
/**
* The size of the buffer to use.
* <p>
* This defines how much data will be buffered before being written as
* a new SSTable. This corresponds roughly to the data size that will have the created
* sstable.
* <p>
* The default is 128MiB, which should be reasonable for a 1GiB heap. If you experience
* OOM while using the writer, you should lower this value.
*
* @param size the size to use in MiB.
* @return this builder.
* @deprecated This method is deprecated in favor of the new withMaxSSTableSizeInMiB(int size)
*/
@Deprecated(since = "5.0")
public Builder withBufferSizeInMiB(int size)
{
return withMaxSSTableSizeInMiB(size);
}
/**
* The size of the buffer to use.
* <p>
* This defines how much data will be buffered before being written as
* a new SSTable. This corresponds roughly to the data size that will have the created
* sstable.
* <p>
* The default is 128MiB, which should be reasonable for a 1GiB heap. If you experience
* OOM while using the writer, you should lower this value.
*
* @param size the size to use in MiB.
* @return this builder.
* @deprecated This method is deprecated in favor of the new withBufferSizeInMiB(int size). See CASSANDRA-17675
*/
@Deprecated(since = "4.1")
public Builder withBufferSizeInMB(int size)
{
return withBufferSizeInMiB(size);
}
/**
* Creates a CQLSSTableWriter that expects sorted inputs.
* <p>
* If this option is used, the resulting writer will expect rows to be
* added in SSTable sorted order (and an exception will be thrown if that
* is not the case during modification). The SSTable sorted order means that
* rows are added such that their partition key respect the partitioner
* order.
* <p>
* You should thus only use this option is you know that you can provide
* the rows in order, which is rarely the case. If you can provide the
* rows in order however, using this sorted might be more efficient.
* <p>
* Note that if used, some option like withBufferSizeInMiB will be ignored.
*
* @return this builder.
*/
public Builder sorted()
{
this.sorted = true;
return this;
}
/**
* Whether indexes should be built and serialized to disk along data. Defaults to true.
*
* @param buildIndexes true if indexes should be built, false otherwise
* @return this builder
*/
public Builder withBuildIndexes(boolean buildIndexes)
{
this.buildIndexes = buildIndexes;
return this;
}
/**
* Set the listener to receive notifications on sstable produced
* <p>
* Note that if listener is registered, the sstables are opened into {@link SSTableReader}.
* The consumer is responsible for releasing the {@link SSTableReader}
*
* @param sstableProducedListener receives the produced sstables
* @return this builder
*/
public Builder withSSTableProducedListener(Consumer<Collection<SSTableReader>> sstableProducedListener)
{
this.sstableProducedListener = sstableProducedListener;
return this;
}
/**
* Whether the produced sstable should be open or not.
* By default, the writer does not open the produced sstables
*
* @return this builder
*/
public Builder openSSTableOnProduced()
{
this.openSSTableOnProduced = true;
return this;
}
public HarrySSTableWriter build()
{
if (directory == null)
throw new IllegalStateException("No ouptut directory specified, you should provide a directory with inDirectory()");
if (schemaStatement == null)
throw new IllegalStateException("Missing schema, you should provide the schema for the SSTable to create with forTable()");
Preconditions.checkState(Sets.difference(SchemaConstants.LOCAL_SYSTEM_KEYSPACE_NAMES, Schema.instance.getKeyspaces()).isEmpty(),
"Local keyspaces were not loaded. If this is running as a client, please make sure to add %s=true system property.",
CassandraRelevantProperties.FORCE_LOAD_LOCAL_KEYSPACES.getKey());
// Assign the default max SSTable size if not defined in builder
if (isMaxSSTableSizeUnset())
{
maxSSTableSizeInMiB = sorted ? -1L : DEFAULT_BUFFER_SIZE_IN_MIB_FOR_UNSORTED;
}
synchronized (HarrySSTableWriter.class)
{
String keyspaceName = schemaStatement.keyspace();
String tableName = schemaStatement.table();
Schema.instance.submit(SchemaTransformations.addKeyspace(KeyspaceMetadata.create(keyspaceName,
KeyspaceParams.simple(1),
Tables.none(),
Views.none(),
Types.none(),
UserFunctions.none()), true));
KeyspaceMetadata ksm = KeyspaceMetadata.create(keyspaceName,
KeyspaceParams.simple(1),
Tables.none(),
Views.none(),
Types.none(),
UserFunctions.none());
TableMetadata tableMetadata = Schema.instance.getTableMetadata(keyspaceName, tableName);
if (tableMetadata == null)
{
Types types = createTypes(keyspaceName);
Schema.instance.submit(SchemaTransformations.addTypes(types, true));
tableMetadata = createTable(types, ksm.userFunctions);
Schema.instance.submit(SchemaTransformations.addTable(tableMetadata, true));
if (buildIndexes && !indexStatements.isEmpty())
{
// we need to commit keyspace metadata first so applyIndexes sees that keyspace from TCM
commitKeyspaceMetadata(ksm.withSwapped(ksm.tables.with(tableMetadata)));
applyIndexes(keyspaceName);
}
KeyspaceMetadata keyspaceMetadata = ClusterMetadata.current().schema.getKeyspaceMetadata(keyspaceName);
tableMetadata = keyspaceMetadata.tables.getNullable(tableName);
Schema.instance.submit(SchemaTransformations.addTable(tableMetadata, true));
}
KeyspaceMetadata keyspaceMetadata = ClusterMetadata.current().schema.getKeyspaceMetadata(keyspaceName);
Keyspace keyspace = Keyspace.mockKS(keyspaceMetadata);
Directories directories = new Directories(tableMetadata, Collections.singleton(new Directories.DataDirectory(new File(directory.toPath()))));
ColumnFamilyStore cfs = ColumnFamilyStore.createColumnFamilyStore(keyspace,
tableName,
tableMetadata,
directories,
false,
false);
keyspace.initCfCustom(cfs);
// this is the empty directory / leftover from times we initialized ColumnFamilyStore
// it will automatically create directories for keyspace and table on disk after initialization
// we set that directory to the destination of generated SSTables so we just remove empty directories here
try
{
new File(directory, keyspaceName).deleteRecursive();
}
catch (UncheckedIOException ex)
{
if (!(ex.getCause() instanceof NoSuchFileException))
{
throw ex;
}
}
TableMetadataRef ref = tableMetadata.ref;
AbstractSSTableSimpleWriter writer = sorted
? new SSTableSimpleWriter(directory, ref, cfs.metadata.get().regularAndStaticColumns(), maxSSTableSizeInMiB)
: new SSTableSimpleUnsortedWriter(directory, ref, cfs.metadata.get().regularAndStaticColumns(), maxSSTableSizeInMiB);
if (format != null)
writer.setSSTableFormatType(format);
if (buildIndexes && !indexStatements.isEmpty() && cfs != null)
{
StorageAttachedIndexGroup saiGroup = StorageAttachedIndexGroup.getIndexGroup(cfs);
if (saiGroup != null)
writer.addIndexGroup(saiGroup);
}
if (sstableProducedListener != null)
writer.setSSTableProducedListener(sstableProducedListener);
writer.setShouldOpenProducedSSTable(openSSTableOnProduced);
return new HarrySSTableWriter(writer);
}
}
private boolean isMaxSSTableSizeUnset()
{
return maxSSTableSizeInMiB <= 0;
}
private Types createTypes(String keyspace)
{
Types.RawBuilder builder = Types.rawBuilder(keyspace);
for (CreateTypeStatement.Raw st : typeStatements)
st.addToRawBuilder(builder);
return builder.build();
}
/**
* Applies any provided index definitions to the target table
*
* @param keyspaceName name of the keyspace to apply indexes for
* @return table metadata reflecting applied indexes
*/
private void applyIndexes(String keyspaceName)
{
ClientState state = ClientState.forInternalCalls();
for (CreateIndexStatement.Raw statement : indexStatements)
{
Keyspaces keyspaces = statement.prepare(state).apply(ClusterMetadata.current());
commitKeyspaceMetadata(keyspaces.getNullable(keyspaceName));
}
}
private void commitKeyspaceMetadata(KeyspaceMetadata keyspaceMetadata)
{
SchemaTransformation schemaTransformation = new SchemaTransformation()
{
@Override
public Keyspaces apply(ClusterMetadata metadata)
{
return metadata.schema.getKeyspaces().withAddedOrUpdated(keyspaceMetadata);
}
@Override
public boolean compatibleWith(ClusterMetadata metadata)
{
return true;
}
};
ClusterMetadataService.instance().commit(new AlterSchema(schemaTransformation));
}
/**
* Creates the table according to schema statement
*
* @param types types this table should be created with
*/
private TableMetadata createTable(Types types, UserFunctions functions)
{
ClientState state = ClientState.forInternalCalls();
CreateTableStatement statement = schemaStatement.prepare(state);
statement.validate(ClientState.forInternalCalls());
TableMetadata.Builder builder = statement.builder(types, functions);
if (partitioner != null)
builder.partitioner(partitioner);
return builder.build();
}
}
}

View File

@ -23,64 +23,87 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Directories; import org.apache.cassandra.db.Directories;
import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.db.commitlog.CommitLog;
import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.service.snapshot.SnapshotManager; import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.annotations.*;
@BenchmarkMode(Mode.AverageTime) @BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS) @OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS) @Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1) @Fork(value = 1)
@Threads(1) @Threads(1)
@State(Scope.Benchmark) @State(Scope.Benchmark)
public class CompactionBench extends CQLTester public class CompactionBench extends CQLTester
{ {
static String keyspace; protected static String keyspace;
String table; protected String table;
String writeStatement; protected String writeStatement;
String readStatement; protected ColumnFamilyStore cfs;
ColumnFamilyStore cfs; protected List<File> snapshotFiles;
List<File> snapshotFiles;
List<Descriptor> liveFiles; @Param("2")
protected int sstableCount = 2;
@Param("50000")
protected int rowCount = 50000;
@Param("NONE")
protected String overlap = "NONE";
@Param("true")
protected boolean isCursor = true;
@Setup(Level.Trial) @Setup(Level.Trial)
public void setup() throws Throwable public void setup() throws Throwable
{ {
CQLTester.prepareServer(); CQLTester.prepareServer();
DatabaseDescriptor.setCursorCompactionEnabled(isCursor);
DatabaseDescriptor.setCompactionThroughputMebibytesPerSec(10*1024); // no rate limiting
createSStables();
takeSnapshot();
}
protected void createSStables()
{
keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false"); keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, commentid bigint, PRIMARY KEY(userid, picid))"); table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, commentid bigint, PRIMARY KEY(userid, picid))");
execute("use "+keyspace+";"); execute("use "+keyspace+";");
writeStatement = "INSERT INTO "+table+"(userid,picid,commentid)VALUES(?,?,?)"; writeStatement = "INSERT INTO "+table+"(userid,picid,commentid)VALUES(?,?,?)";
readStatement = "SELECT * from "+table+" limit 100";
Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction())); Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction()));
cfs = Keyspace.open(keyspace).getColumnFamilyStore(table); cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction(); cfs.disableAutoCompaction();
//Warm up for (int j = 0; j < sstableCount; j++)
System.err.println("Writing 50k"); {
for (long i = 0; i < 50000; i++) int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount;
execute(writeStatement, i, i, i ); int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount;
for (long i = 0; i < rowCount; i++)
{
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); execute(writeStatement, (pPrefix + i), (rPrefix + i), j * rowCount + i);
}
System.err.println("Writing 50k again...");
for (long i = 0; i < 50000; i++)
execute(writeStatement, i, i, i );
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED); cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
}
}
private void takeSnapshot()
{
SnapshotManager.instance.takeSnapshot("originals", cfs.getKeyspaceTableName()); SnapshotManager.instance.takeSnapshot("originals", cfs.getKeyspaceTableName());
snapshotFiles = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).snapshots("originals").listFiles(); snapshotFiles = cfs.getDirectories().sstableLister(Directories.OnTxnErr.IGNORE).snapshots("originals").listFiles();
long[] sum = new long[1];
snapshotFiles.forEach(f -> sum[0] += f.length());
System.out.println("Total input size: " + sum[0]);
} }
@TearDown(Level.Trial) @TearDown(Level.Trial)
@ -95,28 +118,36 @@ public class CompactionBench extends CQLTester
System.err.println("Thread "+t.getName()); System.err.println("Thread "+t.getName());
} }
CommitLog.instance.shutdownBlocking();
ClusterMetadataService.instance().log().close();
CQLTester.tearDownClass();
CQLTester.cleanup(); CQLTester.cleanup();
} }
@TearDown(Level.Invocation) @TearDown(Level.Invocation)
public void resetSnapshot() public void resetSnapshot() throws IOException, InterruptedException
{ {
cfs.truncateBlocking(); cfs.truncateBlocking();
List<File> directories = cfs.getDirectories().getCFDirectories(); List<File> directories = cfs.getDirectories().getCFDirectories();
// Sometimes deletes are unreliable...
int deleted = 0;
do
{
deleted = 0;
for (File file : directories) for (File file : directories)
{ {
for (File f : file.tryList()) for (File f : file.tryList())
{ {
if (f.isDirectory()) if (f.isDirectory())
continue; continue;
f.tryDelete();
FileUtils.delete(f); deleted++;
} }
} }
Thread.sleep(10);
} while (deleted != 0);
for (File file : snapshotFiles) for (File file : snapshotFiles)
FileUtils.createHardLink(file, new File(new File(file.toPath().getParent().getParent().getParent()), file.name())); FileUtils.createHardLink(file, new File(new File(file.toPath().getParent().getParent().getParent()), file.name()));

View File

@ -0,0 +1,78 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.test.microbench.sstable;
import java.nio.ByteBuffer;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.test.microbench.CompactionBench;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1)
@Threads(1)
@State(Scope.Benchmark)
public class CompactionLargeCellBench extends CompactionBench
{
@Param("128")
int blobSize = 128;
protected void createSStables()
{
keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
table = createTable(keyspace, "CREATE TABLE %s ( userid bigint, picid bigint, b blob, PRIMARY KEY(userid, picid))");
execute("use "+keyspace+";");
writeStatement = "INSERT INTO "+table+"(userid,picid,b)VALUES(?,?,?)";
Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction()));
cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
byte[] blob = new byte[blobSize];
for (int j = 0; j < sstableCount; j++)
{
int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount;
int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount;
for (long i = 0; i < rowCount; i++)
{
ThreadLocalRandom.current().nextBytes(blob);
execute(writeStatement, (pPrefix + i), (rPrefix + i), ByteBuffer.wrap(blob));
}
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
}
}
}

View File

@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.test.microbench.sstable;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.test.microbench.CompactionBench;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1)
@Threads(1)
@State(Scope.Benchmark)
public class CompactionStringsBench extends CompactionBench
{
@Param("128")
int stringSizeMin = 128;
@Param("256")
int stringSizeMax = 256;
protected void createSStables()
{
keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
table = createTable(keyspace, "CREATE TABLE %s ( pk ascii, ck ascii, c ascii, PRIMARY KEY(pk, ck))");
execute("use "+keyspace+";");
writeStatement = "INSERT INTO "+table+"(pk,ck,c)VALUES(?,?,?)";
Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction()));
cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
for (int j = 0; j < sstableCount; j++)
{
Random r = new Random(42L);
int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount;
int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount;
for (long i = 0; i < rowCount; i++)
{
execute(writeStatement, (pPrefix + i) + getNextString(r), (rPrefix + i) + getNextString(r), getNextString(r));
}
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
}
}
private String getNextString(Random r)
{
int rangeDelta = stringSizeMax - stringSizeMin;
int rangeRandom = rangeDelta > 0 ? r.nextInt(rangeDelta) : 0;
byte[] blob = new byte[stringSizeMin + rangeRandom];
r.nextBytes(blob);
for (int i = 0; i < blob.length; i++)
{
if (blob[i] < 0)
blob[i] = 0;
}
String nextString = new String(blob);
return nextString;
}
}

View File

@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.test.microbench.sstable;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.test.microbench.CompactionBench;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 25, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 1)
@Threads(1)
@State(Scope.Benchmark)
public class CompactionWideRowBench extends CompactionBench
{
@Param("1")
int rowPerPkCount = 1;
@Param("1")
int ckCount = 1;
@Param("1")
int colCount = 1;
protected void createSStables()
{
keyspace = createKeyspace("CREATE KEYSPACE %s with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 } and durable_writes = false");
String tableCreate = "CREATE TABLE %s ( userid bigint";
for (int i=0;i<ckCount;i++)
tableCreate += ",ck"+i+ " bigint";
for (int i=0;i<colCount;i++)
tableCreate += ",c"+i+ " bigint";
tableCreate += ", PRIMARY KEY(userid";
for (int i=0;i<ckCount;i++)
tableCreate += ",ck"+i;
tableCreate +="))";
table = createTable(keyspace, tableCreate);
execute("use "+keyspace+";");
writeStatement = "INSERT INTO "+table+"(userid";
for (int i=0;i<ckCount;i++)
writeStatement += ",ck"+i;
for (int i=0;i<colCount;i++)
writeStatement += ",c"+i;
writeStatement += ")VALUES(?";
for (int i=0;i<ckCount+colCount;i++)
writeStatement += ",?";
writeStatement += ")";
Object[] values = new Object[1+ckCount+colCount];
Keyspace.system().forEach(k -> k.getColumnFamilyStores().forEach(c -> c.disableAutoCompaction()));
cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
cfs.disableAutoCompaction();
int pkCount = rowCount/rowPerPkCount;
for (int j = 0; j < sstableCount; j++)
{
int pPrefix = overlap.startsWith("PK") ? 0 : j * rowCount;
int rPrefix = overlap.startsWith("PK.ROW") ? 0 : j * rowCount;
for (long pkIndex = 0; pkIndex < pkCount; pkIndex++)
{
for (long rowIndex = 0; rowIndex < rowPerPkCount; rowIndex++)
{
values[0] = (pPrefix + pkIndex);
Arrays.fill(values, 1, ckCount + 1, (rPrefix + rowIndex));
Arrays.fill(values, 1 + ckCount, values.length, j * rowCount + pkIndex);
execute(writeStatement, values);
}
}
cfs.forceBlockingFlush(ColumnFamilyStore.FlushReason.USER_FORCED);
}
}
}

Some files were not shown because too many files have changed in this diff Show More