Mutation tracking journal integration, read, and write path

Patch by Blake Eggleston & Aleksey Yeshchenko
Reviewed by Blake Eggleston, Aleksey Yeshchenko, & Abe Ratnofsky
 for CASSANDRA-20304, CASSANDRA-20305, & CASSANDRA-20308, CASSANDRA-20373

Co-authored-by: Blake Eggleston <blake@ultrablake.com>
Co-authored-by: Aleksey Yeschenko <aleksey@apache.org>
This commit is contained in:
Blake Eggleston 2025-05-15 16:18:41 -07:00
parent b824304867
commit c71f976955
135 changed files with 11033 additions and 886 deletions

View File

@ -1,3 +1,9 @@
cep-45-mutation-tracking
* Fix mutation tracking startup (CASSANDRA-20540)
* Mutation tracking journal integration, read, and write path (CASSANDRA-20304, CASSANDRA-20305, CASSANDRA-20308)
* Introduce MutationJournal for coordinator logs (CASSANDRA-20353)
* Copy over Journal and dependencies from cep-15-accord (CASSANDRA-20321)
6.0-alpha2
* Fix a removed TTLed row re-appearance in a materialized view after a cursor compaction (CASSANDRA-21152)
* Rework ZSTD dictionary compression logic to create a trainer per training (CASSANDRA-21209)
@ -7,7 +13,6 @@ Merged from 4.1:
Merged from 4.0:
* Rate limit password changes (CASSANDRA-21202)
6.0-alpha1
* Improve performance when calculating settled placements during range movements (CASSANDRA-21144)
* Make shadow gossip round parameters configurable for testing (CASSANDRA-21149)
@ -107,6 +112,8 @@ Merged from 4.0:
* Optimize DataPlacement lookup by ReplicationParams (CASSANDRA-20804)
* Fix ShortPaxosSimulationTest and AccordSimulationRunner do not execute from the cli (CASSANDRA-20805)
* Allow overriding arbitrary settings via environment variables (CASSANDRA-20749)
5.1
* Optimize MessagingService.getVersionOrdinal (CASSANDRA-20816)
* Optimize TrieMemtable#getFlushSet (CASSANDRA-20760)
* Support manual secondary index selection at the CQL level (CASSANDRA-18112)

View File

@ -794,7 +794,7 @@ class TestCqlshOutput(BaseTestCase):
self.assertNoHasColors(output)
# Since CASSANDRA-7622 'DESC FULL SCHEMA' also shows all VIRTUAL keyspaces
self.assertIn('VIRTUAL KEYSPACE system_virtual_schema', output)
self.assertIn("\nCREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true AND fast_path = 'simple';\n",
self.assertIn("\nCREATE KEYSPACE system_auth WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true AND fast_path = 'simple' AND replication_type = 'untracked';\n",
output)
self.assertRegex(output, r'.*\s*$')

View File

@ -694,6 +694,8 @@ public class Config
public boolean dynamic_data_masking_enabled = false;
public boolean mutation_tracking_enabled = false;
/**
* Time in milliseconds after a warning will be emitted to the log and to the client that a UDF runs too long.
* (Only valid, if user_defined_functions_threads_enabled==true)

View File

@ -6025,6 +6025,20 @@ public class DatabaseDescriptor
}
}
public static boolean getMutationTrackingEnabled()
{
return conf.mutation_tracking_enabled;
}
public static void setMutationTrackingEnabled(boolean enabled)
{
if (enabled != conf.mutation_tracking_enabled)
{
logger.info("Setting mutation_tracking_enabled to {}", enabled);
conf.mutation_tracking_enabled = enabled;
}
}
public static OptionalDouble getSeverityDuringDecommission()
{
return conf.severity_during_decommission > 0 ?

View File

@ -542,7 +542,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
updatePartitionsPerBatchMetrics(mutations.size());
boolean mutateAtomic = (isLogged() && mutations.size() > 1);
StorageProxy.mutateWithTriggers(mutations, cl, mutateAtomic, requestTime, preserveTimestamp);
StorageProxy.mutateWithoutConditions(mutations, cl, mutateAtomic, requestTime, preserveTimestamp);
ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations);
}

View File

@ -38,6 +38,7 @@ import org.apache.cassandra.db.commitlog.CommitLogSegment;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.virtual.VirtualMutation;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
@ -224,7 +225,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
PartitionUpdate update = updateEntry.getValue().build();
updates.put(updateEntry.getKey(), update);
}
return new Mutation(keyspaceName, key, updates.build(), createdAt, potentialTxnConflicts);
return new Mutation(MutationId.none(), keyspaceName, key, updates.build(), createdAt, potentialTxnConflicts);
}
public PartitionUpdate.Builder get(TableId tableId)

View File

@ -72,9 +72,11 @@ import org.apache.cassandra.cql3.transactions.ReferenceOperation;
import org.apache.cassandra.db.CBuilder;
import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.CounterMutation;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.RegularAndStaticColumns;
@ -107,6 +109,8 @@ import org.apache.cassandra.locator.NetworkTopologyStrategy;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.metrics.ClientRequestSizeMetrics;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@ -676,11 +680,11 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
false,
options.getTimestamp(queryState),
options.getNowInSeconds(queryState),
requestTime
);
requestTime);
if (!mutations.isEmpty())
{
StorageProxy.mutateWithTriggers(mutations, cl, false, requestTime, attrs.isTimestampSet() ? PreserveTimestamp.yes : PreserveTimestamp.no);
StorageProxy.mutateWithoutConditions(mutations, cl, false, requestTime, attrs.isTimestampSet() ? PreserveTimestamp.yes : PreserveTimestamp.no);
if (!SchemaConstants.isSystemKeyspace(metadata.keyspace))
ClientRequestSizeMetrics.recordRowAndColumnCountMetrics(mutations);
@ -842,8 +846,28 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
{
long timestamp = options.getTimestamp(queryState);
long nowInSeconds = options.getNowInSeconds(queryState);
for (IMutation mutation : getMutations(queryState.getClientState(), options, true, timestamp, nowInSeconds, requestTime))
List<? extends IMutation> mutations = getMutations(queryState.getClientState(), options, true, timestamp, nowInSeconds, requestTime);
boolean isTracked = !mutations.isEmpty() && Schema.instance.getKeyspaceMetadata(mutations.get(0).getKeyspaceName()).params.replicationType.isTracked();
if (isTracked)
{
if (mutations.stream().anyMatch(m -> m instanceof CounterMutation))
throw new InvalidRequestException("Mutation tracking is currently unsupported with counters");
if (mutations.size() > 1)
throw new InvalidRequestException("Mutation tracking is currently unsupported with unlogged batches");
Mutation mutation = (Mutation) mutations.get(0);
String keyspaceName = mutation.getKeyspaceName();
Token token = mutation.key().getToken();
MutationId id = MutationTrackingService.instance.nextMutationId(keyspaceName, token);
mutation = mutation.withMutationId(id);
mutation.apply();
}
else
{
for (IMutation mutation : mutations)
mutation.apply();
}
return null;
}

View File

@ -29,6 +29,7 @@ import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.KeyspaceParams.Option;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.schema.ReplicationParams;
import org.apache.cassandra.service.accord.fastpath.FastPathStrategy;
@ -103,18 +104,26 @@ public final class KeyspaceAttributes extends PropertyDefinitions
{
boolean durableWrites = getBoolean(Option.DURABLE_WRITES.toString(), KeyspaceParams.DEFAULT_DURABLE_WRITES);
FastPathStrategy fastPath = getFastPathStrategy();
return KeyspaceParams.create(durableWrites, getAllReplicationOptions(), fastPath != null ? fastPath : FastPathStrategy.simple());
String rtypeName = getString(Option.REPLICATION_TYPE.toString());
ReplicationType replicationType = rtypeName != null ? ReplicationType.valueOf(rtypeName) : KeyspaceParams.DEFAULT_REPLICATION_TYPE;
return KeyspaceParams.create(durableWrites, getAllReplicationOptions(), fastPath != null ? fastPath : FastPathStrategy.simple(), replicationType);
}
KeyspaceParams asAlteredKeyspaceParams(KeyspaceParams previous)
{
boolean durableWrites = getBoolean(Option.DURABLE_WRITES.toString(), previous.durableWrites);
String rtypeName = getString(Option.REPLICATION_TYPE.toString());
ReplicationType replicationType = rtypeName != null ? ReplicationType.valueOf(rtypeName) : previous.replicationType;
Map<String, String> previousOptions = previous.replication.options;
ReplicationParams replication = getReplicationStrategyClass() == null
? previous.replication
: ReplicationParams.fromMapWithDefaults(getAllReplicationOptions(), previousOptions);
FastPathStrategy fastPath = getFastPathStrategy();
return new KeyspaceParams(durableWrites, replication, fastPath != null ? fastPath : previous.fastPath);
return new KeyspaceParams(durableWrites, replication, fastPath != null ? fastPath : previous.fastPath, replicationType);
}
public boolean hasOption(Option option)

View File

@ -0,0 +1,179 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.InvalidRoutingException;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.metrics.TCMMetrics;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
public abstract class AbstractReadCommandVerbHandler<T> implements IVerbHandler<T>
{
protected abstract void performRead(Message<T> message, ClusterMetadata metadata);
protected abstract ReadCommand getCommand(T payload);
public final void doVerb(Message<T> message)
{
ClusterMetadata metadata = ClusterMetadata.current();
if (message.epoch().isAfter(Epoch.EMPTY))
{
metadata = checkTokenOwnership(metadata, message);
metadata = checkSchemaVersion(metadata, message);
}
MessageParams.reset();
long timeout = message.expiresAtNanos() - message.createdAtNanos();
ReadCommand command = getCommand(message.payload);
command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS));
if (message.trackWarnings())
command.trackWarnings();
performRead(message, metadata);
}
private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message<T> message)
{
ReadCommand readCommand = getCommand(message.payload);
if (SchemaConstants.isSystemKeyspace(readCommand.metadata().keyspace) ||
readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes
return metadata;
Keyspace ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
ColumnFamilyStore cfs = ks != null ? ks.getColumnFamilyStore(readCommand.metadata().id) : null;
Epoch localComparisonEpoch = metadata.epoch;
if (cfs != null)
localComparisonEpoch = cfs.metadata().epoch;
if (localComparisonEpoch.isBefore(readCommand.serializedAtEpoch()))
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
else if (localComparisonEpoch.isAfter(readCommand.serializedAtEpoch()))
{
TCMMetrics.instance.coordinatorBehindSchema.mark();
throw new CoordinatorBehindException(String.format("Coordinator schema for %s.%s with epoch %s is behind our schema %s",
readCommand.metadata().keyspace,
readCommand.metadata().name,
readCommand.serializedAtEpoch(),
localComparisonEpoch));
}
ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
if (ks == null || ks.getColumnFamilyStore(readCommand.metadata().id) == null)
throw new IllegalStateException("Unknown table " + readCommand.metadata().id +" after fetching remote log entries");
return metadata;
}
private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message<T> message)
{
ReadCommand command = getCommand(message.payload);
if (command.metadata().isVirtual())
return metadata;
// Some read commands may be sent using an older Epoch intentionally so validating using the current Epoch
// doesn't work
if (command.potentialTxnConflicts().allowed)
return metadata;
if (command.isTopK())
return metadata;
if (command instanceof SinglePartitionReadCommand)
{
Token token = ((SinglePartitionReadCommand) command).partitionKey().getToken();
Replica localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
if (localReplica == null)
{
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
}
if (localReplica == null)
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, command);
}
if (!command.acceptsTransient() && localReplica.isTransient())
{
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
command.acceptsTransient() ? "transient" : "full",
localReplica.isTransient() ? "transient" : "full",
this));
}
}
else
{
AbstractBounds<PartitionPosition> range = command.dataRange().keyRange();
// TODO: preexisting issue: for the range queries or queries that span multiple replicas, we can only make requests where the right token is owned, but not the left one
Replica maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
if (maxTokenLocalReplica == null)
{
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
}
if (maxTokenLocalReplica == null)
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, command);
}
// TODO: preexisting issue: we should change the whole range for transient-ness, not just the right token
if (command.acceptsTransient() != maxTokenLocalReplica.isTransient())
{
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
command.acceptsTransient() ? "transient" : "full",
maxTokenLocalReplica.isTransient() ? "transient" : "full",
this));
}
}
return metadata;
}
private static Replica getLocalReplica(ClusterMetadata metadata, Token token, String keyspace)
{
return metadata.placements
.get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication)
.reads
.forToken(token)
.get()
.lookup(FBUtilities.getBroadcastAddressAndPort());
}
}

View File

@ -53,6 +53,7 @@ import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.tracing.Tracing;
@ -64,6 +65,7 @@ import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
public class CounterMutation implements IMutation
@ -81,6 +83,18 @@ public class CounterMutation implements IMutation
this.consistency = consistency;
}
@Override
public MutationId id()
{
return mutation.id();
}
@Override
public CounterMutation withMutationId(MutationId mutationId)
{
return new CounterMutation(mutation.withMutationId(mutationId), consistency);
}
public String getKeyspaceName()
{
return mutation.getKeyspaceName();
@ -148,7 +162,7 @@ public class CounterMutation implements IMutation
*/
public Mutation applyCounterMutation() throws WriteTimeoutException
{
Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(getKeyspaceName(), key());
Mutation.PartitionUpdateCollector resultBuilder = new Mutation.PartitionUpdateCollector(id(), getKeyspaceName(), key());
Keyspace keyspace = Keyspace.open(getKeyspaceName());
List<Lock> locks = new ArrayList<>();
@ -374,6 +388,7 @@ public class CounterMutation implements IMutation
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
private int serializedSize52;
public int serializedSize(int version)
{
@ -391,6 +406,10 @@ public class CounterMutation implements IMutation
if (serializedSize51 == 0)
serializedSize51 = (int) serializer.serializedSize(this, VERSION_60);
return serializedSize51;
case VERSION_61:
if (serializedSize52 == 0)
serializedSize52 = (int) serializer.serializedSize(this, VERSION_61);
return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version: " + version);
}

View File

@ -27,6 +27,7 @@ import javax.annotation.Nullable;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.ClientState;
@ -34,6 +35,8 @@ public interface IMutation
{
long MAX_MUTATION_SIZE = DatabaseDescriptor.getMaxMutationSize();
MutationId id();
IMutation withMutationId(MutationId mutationId);
void apply();
String getKeyspaceName();
Collection<TableId> getTableIds();

View File

@ -33,6 +33,7 @@ import java.util.concurrent.locks.Lock;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import org.slf4j.Logger;
@ -44,6 +45,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.lifecycle.SSTableSet;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.repair.CassandraKeyspaceRepairManager;
import org.apache.cassandra.db.tracked.TrackedKeyspaceWriteHandler;
import org.apache.cassandra.db.view.ViewManager;
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.exceptions.WriteTimeoutException;
@ -54,6 +56,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.metrics.KeyspaceMetrics;
import org.apache.cassandra.repair.KeyspaceRepairManager;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@ -106,6 +109,7 @@ public class Keyspace
public final ViewManager viewManager;
private final KeyspaceWriteHandler writeHandler;
private final TrackedKeyspaceWriteHandler trackedWriteHandler;
private final KeyspaceRepairManager repairManager;
private final SchemaProvider schema;
private final String name;
@ -285,6 +289,7 @@ public class Keyspace
this.repairManager = new CassandraKeyspaceRepairManager(this);
this.writeHandler = new CassandraKeyspaceWriteHandler(this);
this.trackedWriteHandler = new TrackedKeyspaceWriteHandler();
}
public Keyspace(KeyspaceMetadata metadata)
@ -296,6 +301,7 @@ public class Keyspace
this.viewManager = new ViewManager(this);
this.repairManager = new CassandraKeyspaceRepairManager(this);
this.writeHandler = new CassandraKeyspaceWriteHandler(this);
this.trackedWriteHandler = new TrackedKeyspaceWriteHandler();
this.metadataRef = new KeyspaceMetadataRef(metadata, schema);
}
@ -395,13 +401,9 @@ public class Keyspace
public Future<?> applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes)
{
return applyInternal(mutation, writeCommitLog, updateIndexes, true, true, new AsyncPromise<>());
}
public Future<?> applyFuture(Mutation mutation, boolean writeCommitLog, boolean updateIndexes, boolean isDroppable,
boolean isDeferrable)
{
return applyInternal(mutation, writeCommitLog, updateIndexes, isDroppable, isDeferrable, new AsyncPromise<>());
return getMetadata().useMutationTracking()
? applyInternalTracked(mutation, new AsyncPromise<>())
: applyInternal(mutation, writeCommitLog, updateIndexes, true, true, new AsyncPromise<>());
}
public void apply(Mutation mutation, boolean writeCommitLog, boolean updateIndexes)
@ -431,7 +433,10 @@ public class Keyspace
boolean updateIndexes,
boolean isDroppable)
{
applyInternal(mutation, makeDurable, updateIndexes, isDroppable, false, null);
if (getMetadata().useMutationTracking())
applyInternalTracked(mutation, null);
else
applyInternal(mutation, makeDurable, updateIndexes, isDroppable, false, null);
}
/**
@ -451,6 +456,8 @@ public class Keyspace
boolean isDeferrable,
Promise<?> future)
{
Preconditions.checkState(!getMetadata().useMutationTracking() && mutation.id().isNone());
if (TEST_FAIL_WRITES && getMetadata().name.equals(TEST_FAIL_WRITES_KS))
throw new RuntimeException("Testing write failures");
@ -598,6 +605,41 @@ public class Keyspace
}
}
/**
* Append the mutation to the mutation journal, then update memtables and indexes.
*/
private Future<?> applyInternalTracked(Mutation mutation, Promise<?> future)
{
Preconditions.checkState(getMetadata().useMutationTracking() && !mutation.id().isNone());
if (TEST_FAIL_WRITES && getMetadata().name.equals(TEST_FAIL_WRITES_KS))
throw new RuntimeException("Testing write failures");
try (WriteContext ctx = trackedWriteHandler.beginWrite(mutation, true))
{
MutationTrackingService.instance.startWriting(mutation);
for (PartitionUpdate upd : mutation.getPartitionUpdates())
{
ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(upd.metadata().id);
if (cfs == null)
{
logger.error("Attempting to mutate non-existant table {} ({}.{})", upd.metadata().id, upd.metadata().keyspace, upd.metadata().name);
continue;
}
cfs.getWriteHandler().write(upd, ctx, true);
}
}
MutationTrackingService.instance.finishWriting(mutation);
if (future != null)
future.trySuccess(null);
return future;
}
public AbstractReplicationStrategy getReplicationStrategy()
{
return getMetadata().replicationStrategy;

View File

@ -52,6 +52,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.TeeDataInputPlus;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
@ -63,6 +64,7 @@ import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
public class Mutation implements IMutation, Supplier<Mutation>
@ -71,6 +73,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
public static final int ALLOW_POTENTIAL_TRANSACTION_CONFLICTS = 0x01;
private final MutationId id;
// todo this is redundant
// when we remove it, also restore SerializationsTest.testMutationRead to not regenerate new Mutations each test
private final String keyspaceName;
@ -103,23 +106,29 @@ public class Mutation implements IMutation, Supplier<Mutation>
// because it is being applied by one or in a context where transaction conflicts don't occur
private PotentialTxnConflicts potentialTxnConflicts;
public Mutation(MutationId id, PartitionUpdate update)
{
this(id, update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, PotentialTxnConflicts.DISALLOW);
}
public Mutation(PartitionUpdate update)
{
this(update, PotentialTxnConflicts.DISALLOW);
this(MutationId.none(), update, PotentialTxnConflicts.DISALLOW);
}
public Mutation(PartitionUpdate update, PotentialTxnConflicts potentialTxnConflicts)
public Mutation(MutationId id, PartitionUpdate update, PotentialTxnConflicts potentialTxnConflicts)
{
this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, potentialTxnConflicts);
this(id, update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, potentialTxnConflicts);
}
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, PotentialTxnConflicts potentialTxnConflicts)
public Mutation(MutationId id, String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, PotentialTxnConflicts potentialTxnConflicts)
{
this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), potentialTxnConflicts);
this(id, keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), potentialTxnConflicts);
}
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, PotentialTxnConflicts potentialTxnConflicts)
public Mutation(MutationId id, String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, PotentialTxnConflicts potentialTxnConflicts)
{
this.id = id;
this.keyspaceName = keyspaceName;
this.key = key;
this.modifications = modifications;
@ -128,6 +137,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
this.potentialTxnConflicts = potentialTxnConflicts;
}
@Override
public MutationId id()
{
return id;
}
@Override
public Mutation withMutationId(MutationId mutationId)
{
return new Mutation(mutationId, keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled, potentialTxnConflicts);
}
private static boolean cdcEnabled(Iterable<PartitionUpdate> modifications)
{
boolean cdc = false;
@ -159,7 +180,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
Map<TableId, PartitionUpdate> updates = builder.build();
checkState(!updates.isEmpty(), "Updates should not be empty");
return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, potentialTxnConflicts);
return new Mutation(id, keyspaceName, key, builder.build(), approxCreatedAtNanos, potentialTxnConflicts);
}
public @Nullable Mutation without(TableId tableId)
@ -258,6 +279,8 @@ public class Mutation implements IMutation, Supplier<Mutation>
throw new IllegalArgumentException("Can't merge mutations with differing policies on allowing potential transaction conflicts");
potentialTxnConflicts = mutation.potentialTxnConflicts;
updatedTables.addAll(mutation.modifications.keySet());
if (!mutation.id().isNone())
throw new IllegalArgumentException();
if (ks != null && !ks.equals(mutation.keyspaceName))
throw new IllegalArgumentException();
if (key != null && !key.equals(mutation.key))
@ -283,7 +306,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
modifications.put(table, updates.size() == 1 ? updates.get(0) : PartitionUpdate.merge(updates));
updates.clear();
}
return new Mutation(ks, key, modifications.build(), approxTime.now(), potentialTxnConflicts);
return new Mutation(MutationId.none(), ks, key, modifications.build(), approxTime.now(), potentialTxnConflicts);
}
public Future<?> applyFuture()
@ -399,6 +422,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
private int serializedSize52;
public int serializedSize(int version)
{
@ -416,6 +440,10 @@ public class Mutation implements IMutation, Supplier<Mutation>
if (serializedSize51 == 0)
serializedSize51 = (int) serializer.serializedSize(this, VERSION_60);
return serializedSize51;
case VERSION_61:
if (serializedSize52 == 0)
serializedSize52 = (int) serializer.serializedSize(this, VERSION_61);
return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version: " + version);
}
@ -428,9 +456,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
* @param partitionKey the key of partition this if a mutation for.
* @return a newly created builder.
*/
public static SimpleBuilder simpleBuilder(String keyspaceName, DecoratedKey partitionKey)
public static SimpleBuilder simpleBuilder(MutationId mutationId, String keyspaceName, DecoratedKey partitionKey)
{
return new SimpleBuilders.MutationBuilder(keyspaceName, partitionKey);
return new SimpleBuilders.MutationBuilder(mutationId, keyspaceName, partitionKey);
}
/**
@ -554,9 +582,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
}
static void serializeInternal(PartitionUpdate.PartitionUpdateSerializer serializer,
Mutation mutation,
DataOutputPlus out,
int version) throws IOException
Mutation mutation,
DataOutputPlus out,
int version) throws IOException
{
Map<TableId, PartitionUpdate> modifications = mutation.modifications;
@ -567,6 +595,9 @@ public class Mutation implements IMutation, Supplier<Mutation>
out.write(flags);
}
if (version >= MessagingService.VERSION_61)
MutationId.serializer.serialize(mutation.id, out, version);
/* serialize the modifications in the mutation */
int size = modifications.size();
out.writeUnsignedVInt32(size);
@ -592,13 +623,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
int flags = teeIn.readByte();
potentialTxnConflicts = potentialTxnConflicts(flags);
}
MutationId id = version >= MessagingService.VERSION_61
? MutationId.serializer.deserialize(teeIn, version)
: MutationId.none();
int size = teeIn.readUnsignedVInt32();
assert size > 0;
PartitionUpdate update = PartitionUpdate.serializer.deserialize(teeIn, version, flag);
if (size == 1)
{
m = new Mutation(update, potentialTxnConflicts);
m = new Mutation(id, update, potentialTxnConflicts);
}
else
{
@ -611,7 +647,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
update = PartitionUpdate.serializer.deserialize(teeIn, version, flag);
modifications.put(update.metadata().id, update);
}
m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now(), potentialTxnConflicts);
m = new Mutation(id, update.metadata().keyspace, dk, modifications.build(), approxTime.now(), potentialTxnConflicts);
}
//Only cache serializations that don't hit the limit
@ -691,6 +727,8 @@ public class Mutation implements IMutation, Supplier<Mutation>
{
if (version >= VERSION_60)
size += TypeSizes.sizeof((byte)ALLOW_POTENTIAL_TRANSACTION_CONFLICTS); // flags
if (version >= MessagingService.VERSION_61)
size += MutationId.serializer.serializedSize(mutation.id, version);
size += TypeSizes.sizeofUnsignedVInt(mutation.modifications.size());
for (PartitionUpdate partitionUpdate : mutation.modifications.values())
size += serializer.serializedSize(partitionUpdate, version);
@ -706,20 +744,22 @@ public class Mutation implements IMutation, Supplier<Mutation>
public static class PartitionUpdateCollector
{
private final ImmutableMap.Builder<TableId, PartitionUpdate> modifications = new ImmutableMap.Builder<>();
private final MutationId mutationId;
private final String keyspaceName;
private final DecoratedKey key;
private final long approxCreatedAtNanos = approxTime.now();
private boolean empty = true;
private PotentialTxnConflicts potentialTxnConflicts;
private final PotentialTxnConflicts potentialTxnConflicts;
public PartitionUpdateCollector(String keyspaceName, DecoratedKey key)
public PartitionUpdateCollector(MutationId mutationId, String keyspaceName, DecoratedKey key)
{
this(keyspaceName, key, PotentialTxnConflicts.DISALLOW);
this(mutationId, keyspaceName, key, PotentialTxnConflicts.DISALLOW);
}
public PartitionUpdateCollector(String keyspaceName, DecoratedKey key, PotentialTxnConflicts potentialTxnConflicts)
public PartitionUpdateCollector(MutationId mutationId, String keyspaceName, DecoratedKey key, PotentialTxnConflicts potentialTxnConflicts)
{
this.mutationId = mutationId;
this.keyspaceName = keyspaceName;
this.key = key;
this.potentialTxnConflicts = potentialTxnConflicts;
@ -756,7 +796,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
public Mutation build()
{
return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, potentialTxnConflicts);
return new Mutation(mutationId, keyspaceName, key, modifications.build(), approxCreatedAtNanos, potentialTxnConflicts);
}
}
}

View File

@ -51,10 +51,14 @@ import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.tracked.PartialTrackedRangeRead;
import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@ -89,6 +93,11 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
this.requestedSlices = dataRange.clusteringIndexFilter.getSlices(metadata());
}
public Slices requestedSlices()
{
return requestedSlices;
}
private static PartitionRangeReadCommand create(Epoch serializedAtEpoch,
boolean isDigest,
int digestVersion,
@ -469,6 +478,22 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
}
}
@Override
protected PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
ReadExecutionController executionController,
Index.Searcher searcher,
ColumnFamilyStore cfs,
long startTimeNanos)
{
return PartialTrackedRangeRead.create(executionController, searcher, cfs, startTimeNanos, this, iterator);
}
@Override
protected MutationSummary createMutationSummaryInternal(boolean includePending)
{
return MutationTrackingService.instance.createSummaryForRange(dataRange.keyRange, metadata().id, includePending);
}
@Override
protected boolean intersects(SSTableReader sstable)
{

View File

@ -89,6 +89,7 @@ import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.ParamType;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@ -99,6 +100,7 @@ import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.accord.serializers.TableMetadatas;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
@ -122,10 +124,15 @@ import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
* General interface for storage-engine read commands (common to both range and
* single partition commands).
* <p>
* This contains all the informations needed to do a local read.
* This contains all the information needed to do a local read.
*/
public abstract class ReadCommand extends AbstractReadQuery
{
private interface ReadCompleter<T>
{
T complete(UnfilteredPartitionIterator iterator, ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos);
}
private static final int TEST_ITERATION_DELAY_MILLIS = CassandraRelevantProperties.TEST_READ_ITERATION_DELAY_MS.getInt();
protected static final Logger logger = LoggerFactory.getLogger(ReadCommand.class);
@ -428,6 +435,13 @@ public abstract class ReadCommand extends AbstractReadQuery
protected abstract UnfilteredPartitionIterator queryStorage(ColumnFamilyStore cfs, ReadExecutionController executionController);
protected abstract MutationSummary createMutationSummaryInternal(boolean includePending);
public MutationSummary createMutationSummary(boolean includePending)
{
return createMutationSummaryInternal(includePending);
}
/**
* Whether the underlying {@code ClusteringIndexFilter} is reversed or not.
*
@ -489,24 +503,10 @@ public abstract class ReadCommand extends AbstractReadQuery
selectOptions.validate(metadata(), IndexRegistry.obtain(metadata()), indexQueryPlan());
}
/**
* Executes this command on the local host.
*
* @param executionController the execution controller spanning this command
*
* @return an iterator over the result of executing this command locally.
*/
// iterators created inside the try as long as we do close the original resultIterator), or by closing the result.
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
{
return executeLocally(executionController, null);
}
// ClusterMetadata is null on startup when there are local reads from system tables before it's initialized
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController, @Nullable ClusterMetadata cm)
private <T> T beginRead(ReadExecutionController executionController, @Nullable ClusterMetadata cm, ReadCompleter<T> completer)
{
long startTimeNanos = nanoTime();
COMMAND.set(this);
try
{
@ -531,57 +531,12 @@ public abstract class ReadCommand extends AbstractReadQuery
.collect(Collectors.joining(",")));
}
if (searcher != null && metadata().replicationType().isTracked())
throw new UnsupportedOperationException("TODO: support tracked index reads");
UnfilteredPartitionIterator iterator = (null == searcher) ? queryStorage(cfs, executionController) : searcher.search(executionController);
iterator = RTBoundValidator.validate(iterator, Stage.MERGED, false);
try
{
iterator = withQuerySizeTracking(iterator);
iterator = maybeSlowDownForTesting(iterator);
iterator = withQueryCancellation(iterator);
iterator = maybeRecordPurgeableTombstones(iterator, cfs);
iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs, executionController), Stage.PURGED, false);
iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos);
// If we've used a 2ndary index, we know the result already satisfy the primary expression used, so
// no point in checking it again.
RowFilter filter = (null == searcher) ? rowFilter() : indexQueryPlan.postIndexQueryFilter();
/*
* TODO: We'll currently do filtering by the rowFilter here because it's convenient. However,
* we'll probably want to optimize by pushing it down the layer (like for dropped columns) as it
* would be more efficient (the sooner we discard stuff we know we don't care, the less useless
* processing we do on it).
*/
iterator = filter.filter(iterator, nowInSec());
// apply the limits/row counter; this transformation is stopping and would close the iterator as soon
// as the count is observed; if that happens in the middle of an open RT, its end bound will not be included.
// If tracking repaired data, the counter is needed for overreading repaired data, otherwise we can
// optimise the case where this.limit = DataLimits.NONE which skips an unnecessary transform
if (executionController.isTrackingRepairedStatus())
{
DataLimits.Counter limit =
limits().newCounter(nowInSec(), false, selectsFullPartition(), metadata().enforceStrictLiveness());
iterator = limit.applyTo(iterator);
// ensure that a consistent amount of repaired data is read on each replica. This causes silent
// overreading from the repaired data set, up to limits(). The extra data is not visible to
// the caller, only iterated to produce the repaired data digest.
iterator = executionController.getRepairedDataInfo().extend(iterator, limit);
}
else
{
iterator = limits().filter(iterator, nowInSec(), selectsFullPartition());
}
// because of the above, we need to append an aritifical end bound if the source iterator was stopped short by a counter.
return RTBoundCloser.close(iterator);
}
catch (RuntimeException | Error e)
{
iterator.close();
throw e;
}
return completer.complete(iterator, executionController, searcher, cfs, startTimeNanos);
}
finally
{
@ -589,6 +544,103 @@ public abstract class ReadCommand extends AbstractReadQuery
}
}
private UnfilteredPartitionIterator completeRead(UnfilteredPartitionIterator iterator, ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos)
{
COMMAND.set(this);
try
{
iterator = RTBoundValidator.validate(iterator, Stage.MERGED, false);
iterator = withQuerySizeTracking(iterator);
iterator = maybeSlowDownForTesting(iterator);
iterator = withQueryCancellation(iterator);
iterator = maybeRecordPurgeableTombstones(iterator, cfs);
iterator = RTBoundValidator.validate(withoutPurgeableTombstones(iterator, cfs, executionController), Stage.PURGED, false);
iterator = withMetricsRecording(iterator, cfs.metric, startTimeNanos);
// If we've used a 2ndary index, we know the result already satisfy the primary expression used, so
// no point in checking it again.
RowFilter filter = (null == searcher) ? rowFilter() : indexQueryPlan.postIndexQueryFilter();
/*
* TODO: We'll currently do filtering by the rowFilter here because it's convenient. However,
* we'll probably want to optimize by pushing it down the layer (like for dropped columns) as it
* would be more efficient (the sooner we discard stuff we know we don't care, the less useless
* processing we do on it).
*/
iterator = filter.filter(iterator, nowInSec());
// apply the limits/row counter; this transformation is stopping and would close the iterator as soon
// as the count is observed; if that happens in the middle of an open RT, its end bound will not be included.
// If tracking repaired data, the counter is needed for overreading repaired data, otherwise we can
// optimise the case where this.limit = DataLimits.NONE which skips an unnecessary transform
if (executionController.isTrackingRepairedStatus())
{
DataLimits.Counter limit =
limits().newCounter(nowInSec(), false, selectsFullPartition(), metadata().enforceStrictLiveness());
iterator = limit.applyTo(iterator);
// ensure that a consistent amount of repaired data is read on each replica. This causes silent
// overreading from the repaired data set, up to limits(). The extra data is not visible to
// the caller, only iterated to produce the repaired data digest.
iterator = executionController.getRepairedDataInfo().extend(iterator, limit);
}
else
{
iterator = limits().filter(iterator, nowInSec(), selectsFullPartition());
}
// because of the above, we need to append an aritifical end bound if the source iterator was stopped short by a counter.
return RTBoundCloser.close(iterator);
}
catch (RuntimeException | Error e)
{
iterator.close();
throw e;
}
finally
{
COMMAND.set(null);
}
}
/**
* Convert the initial iterator into a snapshot of data, either by capturing immutable references or materializing
* the contents of the iterator into memory. Once this method returns, subsequent writes against this table should
* not be reflected in the result of the InProgressRead
*/
protected abstract PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
ReadExecutionController executionController,
Index.Searcher searcher,
ColumnFamilyStore cfs,
long startTimeNanos);
public PartialTrackedRead beginTrackedRead(ReadExecutionController executionController)
{
return beginRead(executionController, null, this::createInProgressRead);
}
public UnfilteredPartitionIterator completeTrackedRead(UnfilteredPartitionIterator iterator, PartialTrackedRead read)
{
return completeRead(iterator, read.executionController(), read.searcher(), read.cfs(), read.startTimeNanos());
}
/**
* Executes this command on the local host.
*
* @param executionController the execution controller spanning this command
*
* @return an iterator over the result of executing this command locally.
*/
// iterators created inside the try as long as we do close the original resultIterator), or by closing the result.
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController)
{
return beginRead(executionController, null, this::completeRead);
}
public UnfilteredPartitionIterator executeLocally(ReadExecutionController executionController, @Nullable ClusterMetadata cm)
{
return beginRead(executionController, cm, this::completeRead);
}
protected abstract void recordLatency(TableMetrics metric, long latencyNanos);
public ReadExecutionController executionController(boolean trackRepairedStatus)

View File

@ -16,41 +16,32 @@
* limitations under the License.
*/
package org.apache.cassandra.db;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.InvalidRoutingException;
import org.apache.cassandra.exceptions.QueryCancelledException;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.metrics.TCMMetrics;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
public class ReadCommandVerbHandler extends AbstractReadCommandVerbHandler<ReadCommand>
{
public static final ReadCommandVerbHandler instance = new ReadCommandVerbHandler();
private static final Logger logger = LoggerFactory.getLogger(ReadCommandVerbHandler.class);
@Override
protected ReadCommand getCommand(ReadCommand payload)
{
return payload;
}
public ReadResponse doRead(ReadCommand command, boolean trackRepairedData)
{
ReadResponse response;
@ -63,25 +54,11 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
return response;
}
public void doVerb(Message<ReadCommand> message)
@Override
protected void performRead(Message<ReadCommand> message, ClusterMetadata metadata)
{
if (message.epoch().isAfter(Epoch.EMPTY))
{
ClusterMetadata metadata = ClusterMetadata.current();
metadata = checkTokenOwnership(metadata, message);
metadata = checkSchemaVersion(metadata, message);
}
MessageParams.reset();
long timeout = message.expiresAtNanos() - message.createdAtNanos();
ReadCommand command = message.payload;
command.setMonitoringTime(message.createdAtNanos(), message.isCrossNode(), timeout, DatabaseDescriptor.getSlowQueryTimeout(NANOSECONDS));
if (message.trackWarnings())
command.trackWarnings();
ReadResponse response;
ReadCommand command = message.payload;
try
{
response = doRead(command, message.trackRepairedData());
@ -132,117 +109,4 @@ public class ReadCommandVerbHandler implements IVerbHandler<ReadCommand>
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
}
}
private ClusterMetadata checkSchemaVersion(ClusterMetadata metadata, Message<ReadCommand> message)
{
ReadCommand readCommand = message.payload;
if (SchemaConstants.isSystemKeyspace(readCommand.metadata().keyspace) ||
readCommand.serializedAtEpoch() == null) // don't try to catch up with pre-5.0 nodes
return metadata;
Keyspace ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
ColumnFamilyStore cfs = ks != null ? ks.getColumnFamilyStore(readCommand.metadata().id) : null;
Epoch localComparisonEpoch = metadata.epoch;
if (cfs != null)
localComparisonEpoch = cfs.metadata().epoch;
if (localComparisonEpoch.isBefore(readCommand.serializedAtEpoch()))
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
else if (localComparisonEpoch.isAfter(readCommand.serializedAtEpoch()))
{
TCMMetrics.instance.coordinatorBehindSchema.mark();
throw new CoordinatorBehindException(String.format("Coordinator schema for %s.%s with epoch %s is behind our schema %s",
message.payload.metadata().keyspace,
message.payload.metadata().name,
readCommand.serializedAtEpoch(),
localComparisonEpoch));
}
ks = metadata.schema.getKeyspace(readCommand.metadata().keyspace);
if (ks == null || ks.getColumnFamilyStore(readCommand.metadata().id) == null)
throw new IllegalStateException("Unknown table " + readCommand.metadata().id +" after fetching remote log entries");
return metadata;
}
private ClusterMetadata checkTokenOwnership(ClusterMetadata metadata, Message<ReadCommand> message)
{
ReadCommand command = message.payload;
if (command.metadata().isVirtual())
return metadata;
// Some read commands may be sent using an older Epoch intentionally so validating using the current Epoch
// doesn't work
if (command.potentialTxnConflicts().allowed)
return metadata;
if (command.isTopK())
return metadata;
if (command instanceof SinglePartitionReadCommand)
{
Token token = ((SinglePartitionReadCommand)command).partitionKey().getToken();
Replica localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
if (localReplica == null)
{
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
localReplica = getLocalReplica(metadata, token, command.metadata().keyspace);
}
if (localReplica == null)
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
throw InvalidRoutingException.forTokenRead(message.from(), token, metadata.epoch, message.payload);
}
if (!command.acceptsTransient() && localReplica.isTransient())
{
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
command.acceptsTransient() ? "transient" : "full",
localReplica.isTransient() ? "transient" : "full",
this));
}
}
else
{
AbstractBounds<PartitionPosition> range = ((PartitionRangeReadCommand) command).dataRange().keyRange();
// TODO: preexisting issue: for the range queries or queries that span multiple replicas, we can only make requests where the right token is owned, but not the left one
Replica maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
if (maxTokenLocalReplica == null)
{
metadata = ClusterMetadataService.instance().fetchLogFromPeerOrCMS(metadata, message.from(), message.epoch());
maxTokenLocalReplica = getLocalReplica(metadata, range.right.getToken(), command.metadata().keyspace);
}
if (maxTokenLocalReplica == null)
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(command.metadata().keyspace).metric.outOfRangeTokenReads.inc();
throw InvalidRoutingException.forRangeRead(message.from(), range, metadata.epoch, message.payload);
}
// TODO: preexisting issue: we should change the whole range for transient-ness, not just the right token
if (command.acceptsTransient() != maxTokenLocalReplica.isTransient())
{
MessagingService.instance().metrics.recordDroppedMessage(message, message.elapsedSinceCreated(NANOSECONDS), NANOSECONDS);
throw new InvalidRequestException(String.format("Attempted to serve %s data request from %s node in %s",
command.acceptsTransient() ? "transient" : "full",
maxTokenLocalReplica.isTransient() ? "transient" : "full",
this));
}
}
return metadata;
}
private static Replica getLocalReplica(ClusterMetadata metadata, Token token, String keyspace)
{
return metadata.placements
.get(metadata.schema.getKeyspaces().getNullable(keyspace).params.replication)
.reads
.forToken(token)
.get()
.lookup(FBUtilities.getBroadcastAddressAndPort());
}
}

View File

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.cassandra.db.filter.DataLimits;
import org.apache.cassandra.index.Index;
@ -69,6 +70,7 @@ public class ReadExecutionController implements AutoCloseable
if (trackRepairedStatus)
{
Preconditions.checkArgument(!command.metadata().replicationType().isTracked(), "Tracking repaired status is not supported for tracked reads");
DataLimits.Counter repairedReadCount = command.limits().newCounter(command.nowInSec(),
false,
command.selectsFullPartition(),

View File

@ -39,6 +39,7 @@ import org.apache.cassandra.db.rows.BufferCell;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
@ -116,6 +117,7 @@ public abstract class SimpleBuilders
public static class MutationBuilder extends AbstractBuilder<Mutation.SimpleBuilder> implements Mutation.SimpleBuilder
{
private final MutationId mutationId;
private final String keyspaceName;
private final DecoratedKey key;
@ -123,8 +125,9 @@ public abstract class SimpleBuilders
private PotentialTxnConflicts potentialTxnConflicts = PotentialTxnConflicts.DISALLOW;
public MutationBuilder(String keyspaceName, DecoratedKey key)
public MutationBuilder(MutationId mutationId, String keyspaceName, DecoratedKey key)
{
this.mutationId = mutationId;
this.keyspaceName = keyspaceName;
this.key = key;
}
@ -163,9 +166,9 @@ public abstract class SimpleBuilders
assert !updateBuilders.isEmpty() : "Cannot create empty mutation";
if (updateBuilders.size() == 1)
return new Mutation(updateBuilders.values().iterator().next().build(), potentialTxnConflicts);
return new Mutation(mutationId, updateBuilders.values().iterator().next().build(), potentialTxnConflicts);
Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key, potentialTxnConflicts);
Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(mutationId, keyspaceName, key, potentialTxnConflicts);
for (PartitionUpdateBuilder builder : updateBuilders.values())
mutationBuilder.add(builder.build());
return mutationBuilder.build();

View File

@ -57,6 +57,7 @@ import org.apache.cassandra.db.partitions.PartitionIterators;
import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.BaseRowIterator;
import org.apache.cassandra.db.partitions.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Rows;
@ -78,12 +79,16 @@ import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.metrics.TableMetrics;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.reads.tracked.PartialTrackedRead;
import org.apache.cassandra.service.reads.tracked.PartialTrackedSinglePartitionRead;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@ -561,6 +566,16 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
return new SingletonUnfilteredPartitionIterator(partition);
}
@Override
protected PartialTrackedRead createInProgressRead(UnfilteredPartitionIterator iterator,
ReadExecutionController executionController,
Index.Searcher searcher,
ColumnFamilyStore cfs,
long startTimeNanos)
{
return PartialTrackedSinglePartitionRead.create(executionController, searcher, cfs, startTimeNanos, this, iterator);
}
/**
* Fetch the rows requested if in cache; if not, read it from disk and cache it.
* <p>
@ -926,6 +941,11 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
}
}
protected MutationSummary createMutationSummaryInternal(boolean includePending)
{
return MutationTrackingService.instance.createSummaryForKey(partitionKey, metadata().id, includePending);
}
@Override
protected boolean intersects(SSTableReader sstable)
{

View File

@ -64,13 +64,14 @@ public class CommitLogDescriptor
public static final int VERSION_40 = 7;
public static final int VERSION_50 = 8;
public static final int VERSION_60 = 9;
public static final int VERSION_61 = 10;
/**
* Increment this number if there is a changes in the commit log disc layout or MessagingVersion changes.
* Note: make sure to handle {@link #getMessagingVersion()}
*/
@VisibleForTesting
public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_60;
public static final int current_version = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_61;
final int version;
public final long id;
@ -228,6 +229,8 @@ public class CommitLogDescriptor
return MessagingService.VERSION_50;
case VERSION_60:
return MessagingService.VERSION_60;
case VERSION_61:
return MessagingService.VERSION_61;
default:
throw new IllegalStateException("Unknown commitlog version " + version);
}

View File

@ -313,7 +313,7 @@ public class CommitLogReplayer implements CommitLogReadHandler
if (commitLogReplayer.shouldReplay(update.metadata().id, new CommitLogPosition(segmentId, entryLocation)))
{
if (newPUCollector == null)
newPUCollector = new Mutation.PartitionUpdateCollector(mutation.getKeyspaceName(), mutation.key());
newPUCollector = new Mutation.PartitionUpdateCollector(mutation.id(), mutation.getKeyspaceName(), mutation.key());
newPUCollector.add(update);
commitLogReplayer.replayedCount.incrementAndGet();
}

View File

@ -0,0 +1,26 @@
/*
* 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.partitions;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.utils.AbstractIterator;
public abstract class AbstractPartitionIterator extends AbstractIterator<RowIterator> implements PartitionIterator
{
}

View File

@ -17,14 +17,20 @@
*/
package org.apache.cassandra.db.partitions;
import java.io.IOError;
import java.io.IOException;
import java.util.List;
import org.apache.cassandra.db.EmptyIterators;
import org.apache.cassandra.db.SinglePartitionReadQuery;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.RowIterators;
import org.apache.cassandra.db.transform.MorePartitions;
import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.AbstractIterator;
public abstract class PartitionIterators
@ -167,4 +173,41 @@ public abstract class PartitionIterators
iterator.close();
}
}
public static class Serializer
{
public static void serialize(PartitionIterator iterator, ColumnFilter selection, DataOutputPlus out, int version) throws IOException
{
while (iterator.hasNext())
{
try (RowIterator partition = iterator.next())
{
out.writeBoolean(true);
RowIterators.Serializer.serialize(partition, selection, out, version);
}
}
out.writeBoolean(false);
}
public static PartitionIterator deserialize(TableMetadata metadata, ColumnFilter selection, DataInputPlus in, int version) throws IOException
{
return new AbstractPartitionIterator()
{
@Override
protected RowIterator computeNext()
{
try
{
if (in.readBoolean())
return RowIterators.Serializer.deserialize(metadata, selection, in, version);
return endOfData();
}
catch (IOException e)
{
throw new IOError(e);
}
}
};
}
};
}

View File

@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.partitions;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.DeletionInfo;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.index.transactions.UpdateTransaction;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.btree.BTree;
import org.apache.cassandra.utils.btree.UpdateFunction;
import org.apache.cassandra.utils.memory.HeapCloner;
/**
* Single threaded btree partition, no waste or allocation tracking
*/
public class SimpleBTreePartition extends AbstractBTreePartition implements UpdateFunction<Row, Row>
{
private final TableMetadata metadata;
private final UpdateTransaction indexer;
private BTreePartitionData data = BTreePartitionData.EMPTY;
public SimpleBTreePartition(DecoratedKey partitionKey, TableMetadata metadata, UpdateTransaction indexer)
{
super(partitionKey);
this.metadata = metadata;
this.indexer = indexer;
}
@Override
protected BTreePartitionData holder()
{
return data;
}
@Override
public TableMetadata metadata()
{
return metadata;
}
@Override
protected boolean canHaveShadowedData()
{
return true;
}
@Override
public void onAllocatedOnHeap(long heapSize)
{
}
protected BTreePartitionData makeMergedPartition(BTreePartitionData current, PartitionUpdate update)
{
DeletionInfo newDeletionInfo = merge(current.deletionInfo, update.deletionInfo());
RegularAndStaticColumns columns = current.columns;
RegularAndStaticColumns newColumns = update.columns().mergeTo(columns);
Row newStatic = mergeStatic(current.staticRow, update.staticRow());
Object[] tree = BTree.update(current.tree, update.holder().tree, update.metadata().comparator, this);
EncodingStats newStats = current.stats.mergeWith(update.stats());
return new BTreePartitionData(newColumns, tree, newDeletionInfo, newStatic, newStats);
}
private Row mergeStatic(Row current, Row update)
{
if (update.isEmpty())
return current;
if (current.isEmpty())
return insert(update);
return merge(current, update);
}
private DeletionInfo merge(DeletionInfo existing, DeletionInfo update)
{
if (update.isLive() || !update.mayModify(existing))
return existing;
if (!update.getPartitionDeletion().isLive())
indexer.onPartitionDeletion(update.getPartitionDeletion());
if (update.hasRanges())
update.rangeIterator(false).forEachRemaining(indexer::onRangeTombstone);
// Like for rows, we have to clone the update in case internal buffers (when it has range tombstones) reference
// memory we shouldn't hold into. But we don't ever store this off-heap currently so we just default to the
// HeapAllocator (rather than using 'allocator').
DeletionInfo newInfo = existing.mutableCopy().add(update.clone(HeapCloner.instance));
return newInfo;
}
public Row insert(Row insert)
{
indexer.onInserted(insert);
return insert;
}
public Row merge(Row existing, Row update)
{
Row reconciled = Rows.merge(existing, update, ColumnData.noOp);
indexer.onUpdated(existing, reconciled);
return reconciled;
}
public void update(PartitionUpdate update)
{
data = makeMergedPartition(data, update);
}
}

View File

@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.rows;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.RegularAndStaticColumns;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.AbstractIterator;
public abstract class AbstractRowIterator extends AbstractIterator<Row> implements RowIterator
{
protected final TableMetadata metadata;
protected final DecoratedKey key;
protected final RegularAndStaticColumns columns;
protected final boolean isReversedOrder;
protected final Row staticRow;
public AbstractRowIterator(TableMetadata metadata, DecoratedKey key, RegularAndStaticColumns columns, boolean isReversedOrder, Row staticRow)
{
this.metadata = metadata;
this.key = key;
this.columns = columns;
this.isReversedOrder = isReversedOrder;
this.staticRow = staticRow;
}
@Override
public TableMetadata metadata()
{
return metadata;
}
@Override
public DecoratedKey partitionKey()
{
return key;
}
@Override
public RegularAndStaticColumns columns()
{
return columns;
}
@Override
public Row staticRow()
{
return staticRow;
}
@Override
public boolean isReverseOrder()
{
return isReversedOrder;
}
}

View File

@ -220,7 +220,7 @@ public class ComplexColumnData extends ColumnData implements Iterable<Cell<?>>
public ComplexColumnData withOnlyQueriedData(ColumnFilter filter)
{
return transformAndFilter(complexDeletion, (cell) -> filter.fetchedCellIsQueried(column, cell.path()) ? null : cell);
return transformAndFilter(complexDeletion, (cell) -> filter.fetchedCellIsQueried(column, cell.path()) ? cell : null);
}
public ComplexColumnData purgeDataOlderThan(long timestamp)

View File

@ -17,14 +17,25 @@
*/
package org.apache.cassandra.db.rows;
import com.google.common.base.Preconditions;
import net.nicoulaj.compilecommand.annotations.Inline;
import org.apache.cassandra.db.*;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.SearchIterator;
import org.apache.cassandra.utils.WrappedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Digest;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.schema.TableMetadata;
import java.io.IOError;
import java.io.IOException;
/**
* Static methods to work with row iterators.
*/
@ -101,4 +112,272 @@ public abstract class RowIterators
}
return Transformation.apply(iterator, new Log());
}
private static class RowSerializer
{
/*
* Row flags constants.
*/
private 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 HAS_ALL_COLUMNS = 0x02; // Whether the encoded row has all of the columns from the header present.
private final static int IS_STATIC = 0x04; // Whether the encoded row is a static. If there is no extended flag, the row is assumed not static.
public static void writeEndOfPartition(DataOutputPlus out) throws IOException
{
out.writeByte((byte) END_OF_PARTITION);
}
public static void serializeRow(Row row, SerializationHelper helper, DataOutputPlus out, int version)
throws IOException
{
int flags = 0;
boolean isStatic = row.isStatic();
SerializationHeader header = helper.header;
Preconditions.checkArgument(!header.isForSSTable());
boolean hasAllColumns = row.columnCount() == header.columns(isStatic).size();
if (isStatic)
flags |= IS_STATIC;
if (hasAllColumns)
flags |= HAS_ALL_COLUMNS;
out.writeByte((byte)flags);
if (!isStatic)
Clustering.serializer.serialize(row.clustering(), out, version, header.clusteringTypes());
serializeRowBody(row, flags, helper, out);
}
public static void serializeStaticRow(Row row, SerializationHelper helper, DataOutputPlus out, int version) throws IOException
{
Preconditions.checkArgument(row.isStatic());
serializeRow(row, helper, out, version);
}
@Inline
private static void serializeRowBody(Row row, int flags, SerializationHelper helper, DataOutputPlus out)
throws IOException
{
boolean isStatic = row.isStatic();
SerializationHeader header = helper.header;
Preconditions.checkState(!header.isForSSTable());
Columns headerColumns = header.columns(isStatic);
if ((flags & HAS_ALL_COLUMNS) == 0)
Columns.serializer.serializeSubset(row.columns(), headerColumns, out);
SearchIterator<ColumnMetadata, ColumnMetadata> si = helper.iterator(isStatic);
try
{
row.apply(cd -> {
// We can obtain the column for data directly from data.column(). However, if the cell/complex data
// originates from a sstable, the column we'll get will have the type used when the sstable was serialized,
// and if that type have been recently altered, that may not be the type we want to serialize the column
// with. So we use the ColumnMetadata from the "header" which is "current". Also see #11810 for what
// happens if we don't do that.
ColumnMetadata column = si.next(cd.column());
assert column != null : cd.column.toString();
try
{
if (cd.column.isSimple())
Cell.serializer.serialize((Cell<?>) cd, column, out, LivenessInfo.EMPTY, header);
else
UnfilteredSerializer.writeComplexColumn((ComplexColumnData) cd, column, false, LivenessInfo.EMPTY, header, out);
}
catch (IOException e)
{
throw new WrappedException(e);
}
});
}
catch (WrappedException e)
{
if (e.getCause() instanceof IOException)
throw (IOException) e.getCause();
throw e;
}
}
public static Row deserializeStaticRow(DataInputPlus in, SerializationHeader header, DeserializationHelper helper)
throws IOException
{
int flags = in.readUnsignedByte();
Preconditions.checkState(!isEndOfPartition(flags));
Preconditions.checkState(isStatic(flags));
Row.Builder builder = BTreeRow.sortedBuilder();
builder.newRow(Clustering.STATIC_CLUSTERING);
return deserializeRowBody(in, header, helper, flags, builder);
}
public static Row deserializeRow(DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder) throws IOException
{
int flags = in.readUnsignedByte();
if (isEndOfPartition(flags))
return null;
builder.newRow(Clustering.serializer.deserialize(in, helper.version, header.clusteringTypes()));
return deserializeRowBody(in, header, helper, flags, builder);
}
public static Row deserializeRowBody(DataInputPlus in,
SerializationHeader header,
DeserializationHelper helper,
int flags,
Row.Builder builder)
throws IOException
{
try
{
boolean isStatic = isStatic(flags);
boolean hasAllColumns = (flags & HAS_ALL_COLUMNS) != 0;
Columns headerColumns = header.columns(isStatic);
Preconditions.checkState(!header.isForSSTable());
Columns columns = hasAllColumns ? headerColumns : Columns.serializer.deserializeSubset(headerColumns, in);
try
{
DataInputPlus finalIn = in;
columns.apply(column -> {
try
{
if (column.isSimple())
UnfilteredSerializer.readSimpleColumn(column, finalIn, header, helper, builder, LivenessInfo.EMPTY);
else
UnfilteredSerializer.readComplexColumn(column, finalIn, header, helper, false, builder, LivenessInfo.EMPTY);
}
catch (IOException e)
{
throw new WrappedException(e);
}
});
}
catch (WrappedException e)
{
if (e.getCause() instanceof IOException)
throw (IOException) e.getCause();
throw e;
}
return builder.build();
}
catch (RuntimeException | AssertionError e)
{
// Corrupted data could be such that it triggers an assertion in the row Builder, or break one of its assumption.
// Of course, a bug in said builder could also trigger this, but it's impossible a priori to always make the distinction
// between a real bug and data corrupted in just the bad way. Besides, re-throwing as an IOException doesn't hide the
// exception, it just make we catch it properly and mark the sstable as corrupted.
throw new IOException("Error building row with data deserialized from " + in, e);
}
}
public static boolean isEndOfPartition(int flags)
{
return (flags & END_OF_PARTITION) != 0;
}
public static boolean isStatic(int extendedFlags)
{
return (extendedFlags & IS_STATIC) != 0;
}
}
public static class Serializer
{
public static final int IS_EMPTY = 0x01;
private static final int IS_REVERSED = 0x02;
private static final int HAS_STATIC_ROW = 0x04;
public static void serialize(RowIterator iterator, ColumnFilter selection, DataOutputPlus out, int version) throws IOException
{
ByteBufferUtil.writeWithVIntLength(iterator.partitionKey().getKey(), out);
SerializationHeader header = new SerializationHeader(false,
iterator.metadata(),
iterator.columns(),
EncodingStats.NO_STATS);
int flags = 0;
if (iterator.isReverseOrder())
flags |= IS_REVERSED;
if (iterator.isEmpty())
{
out.writeByte(flags | IS_EMPTY);
return;
}
Row staticRow = iterator.staticRow();
boolean hasStatic = staticRow != Rows.EMPTY_STATIC_ROW;
if (hasStatic)
flags |= HAS_STATIC_ROW;
out.writeByte((byte)flags);
SerializationHeader.serializer.serializeForMessaging(header, selection, out, hasStatic);
SerializationHelper helper = new SerializationHelper(header);
if (hasStatic)
RowSerializer.serializeStaticRow(staticRow, helper, out, version);
while (iterator.hasNext())
RowSerializer.serializeRow(iterator.next(), helper, out, version);
RowSerializer.writeEndOfPartition(out);
}
public static RowIterator deserialize(TableMetadata metadata, ColumnFilter selection, DataInputPlus in, int version) throws IOException
{
DecoratedKey key = metadata.partitioner.decorateKey(ByteBufferUtil.readWithVIntLength(in));
int flags = in.readUnsignedByte();
boolean isReversed = (flags & IS_REVERSED) != 0;
if ((flags & IS_EMPTY) != 0)
{
return EmptyIterators.row(metadata, key, isReversed);
}
boolean hasStatic = (flags & HAS_STATIC_ROW) != 0;
SerializationHeader header = SerializationHeader.serializer.deserializeForMessaging(in, metadata, selection, hasStatic);
DeserializationHelper helper = new DeserializationHelper(metadata, version, DeserializationHelper.Flag.FROM_REMOTE);
Row staticRow = hasStatic ? RowSerializer.deserializeStaticRow(in, header, helper) : Rows.EMPTY_STATIC_ROW;
return new AbstractRowIterator(metadata, key, header.columns(), isReversed, staticRow)
{
private final Row.Builder builder = BTreeRow.sortedBuilder();
@Override
protected Row computeNext()
{
try
{
Row row = RowSerializer.deserializeRow(in, header, helper, builder);
return row != null ? row : endOfData();
}
catch (IOException e)
{
throw new IOError(e);
}
}
@Override
public void close()
{
while (hasNext())
next();
super.close();
}
};
}
};
}

View File

@ -279,7 +279,7 @@ public class UnfilteredSerializer
}
}
private static void writeComplexColumn(ComplexColumnData data, ColumnMetadata column, boolean hasComplexDeletion, LivenessInfo rowLiveness, SerializationHeader header, DataOutputPlus out)
static void writeComplexColumn(ComplexColumnData data, ColumnMetadata column, boolean hasComplexDeletion, LivenessInfo rowLiveness, SerializationHeader header, DataOutputPlus out)
throws IOException
{
if (hasComplexDeletion)
@ -661,7 +661,7 @@ public class UnfilteredSerializer
}
}
private void readSimpleColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder, LivenessInfo rowLiveness)
static void readSimpleColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, Row.Builder builder, LivenessInfo rowLiveness)
throws IOException
{
if (helper.includes(column))
@ -676,7 +676,7 @@ public class UnfilteredSerializer
}
}
private void readComplexColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, boolean hasComplexDeletion, Row.Builder builder, LivenessInfo rowLiveness)
static void readComplexColumn(ColumnMetadata column, DataInputPlus in, SerializationHeader header, DeserializationHelper helper, boolean hasComplexDeletion, Row.Builder builder, LivenessInfo rowLiveness)
throws IOException
{
if (helper.includes(column))
@ -733,7 +733,7 @@ public class UnfilteredSerializer
in.skipBytesFully(markerSize);
}
private void skipComplexColumn(DataInputPlus in, ColumnMetadata column, SerializationHeader header, boolean hasComplexDeletion)
private static void skipComplexColumn(DataInputPlus in, ColumnMetadata column, SerializationHeader header, boolean hasComplexDeletion)
throws IOException
{
if (hasComplexDeletion)

View File

@ -48,6 +48,7 @@ import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.sstable.ISSTableScanner;
import org.apache.cassandra.io.sstable.SSTableTxnSingleStreamWriter;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.AccordTopology;
import org.apache.cassandra.service.accord.IAccordService;
@ -200,7 +201,7 @@ public class CassandraStreamReceiver implements StreamReceiver
//
// If the CFS has CDC, however, these updates need to be written to the CommitLog
// so they get archived into the cdc_raw folder
ks.apply(new Mutation(PartitionUpdate.fromIterator(throttledPartitions.next(), filter)),
ks.apply(new Mutation(MutationId.fixme(), PartitionUpdate.fromIterator(throttledPartitions.next(), filter)),
writeCDCCommitLog,
true,
false);

View File

@ -0,0 +1,82 @@
/*
* 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.tracked;
import org.apache.cassandra.db.CassandraWriteContext;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.KeyspaceWriteHandler;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.WriteContext;
import org.apache.cassandra.db.commitlog.CommitLogPosition;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.journal.RecordPointer;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.concurrent.OpOrder;
public class TrackedKeyspaceWriteHandler implements KeyspaceWriteHandler
{
@Override
public WriteContext beginWrite(Mutation mutation, boolean makeDurable) throws RequestExecutionException
{
OpOrder.Group group = null;
try
{
group = Keyspace.writeOrder.start();
Tracing.trace("Appending to mutation journal");
RecordPointer pointer = MutationJournal.instance.write(mutation.id(), mutation);
// TODO (preferred): update journal to return CommitLogPosition or otherwise remove requirement to allocate second object here
return new CassandraWriteContext(group, new CommitLogPosition(pointer.segment, pointer.position));
}
catch (Throwable t)
{
if (group != null)
group.close();
throw t;
}
}
@Override
public WriteContext createContextForIndexing()
{
return createEmptyContext();
}
@Override
public WriteContext createContextForRead()
{
return createEmptyContext();
}
private WriteContext createEmptyContext()
{
OpOrder.Group group = Keyspace.writeOrder.start();
try
{
return new CassandraWriteContext(group, null);
}
catch (Throwable t)
{
if (group != null)
group.close();
throw t;
}
}
}

View File

@ -41,6 +41,7 @@ import org.apache.cassandra.db.DeletionInfo;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.db.RangeTombstone;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.ReadQuery;
@ -555,7 +556,7 @@ public class TableViews extends AbstractCollection<View>
Collection<PartitionUpdate> updates = generator.generateViewUpdates();
List<Mutation> mutations = new ArrayList<>(updates.size());
for (PartitionUpdate update : updates)
mutations.add(new Mutation(update));
mutations.add(new Mutation(MutationId.fixme(), update));
generator.clear();
return mutations;
@ -570,7 +571,7 @@ public class TableViews extends AbstractCollection<View>
Mutation.PartitionUpdateCollector collector = mutations.get(key);
if (collector == null)
{
collector = new Mutation.PartitionUpdateCollector(baseTableMetadata.keyspace, key);
collector = new Mutation.PartitionUpdateCollector(MutationId.none(), baseTableMetadata.keyspace, key);
mutations.put(key, collector);
}
collector.add(update);

View File

@ -43,6 +43,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.ClientState;
@ -98,6 +99,18 @@ public final class VirtualMutation implements IMutation
this.modifications = modifications;
}
@Override
public MutationId id()
{
return MutationId.none();
}
@Override
public IMutation withMutationId(MutationId mutationId)
{
throw new IllegalArgumentException("MutationId cannot be used for virtual mutations");
}
@Override
public void apply()
{

View File

@ -71,7 +71,8 @@ final class HintsDescriptor
static final int VERSION_40 = 2;
static final int VERSION_50 = 3;
static final int VERSION_60 = 4;
static final int CURRENT_VERSION = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_60;
static final int VERSION_61 = 5;
static final int CURRENT_VERSION = DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? VERSION_40 : VERSION_61;
static final String COMPRESSION = "compression";
static final String ENCRYPTION = "encryption";
@ -261,6 +262,8 @@ final class HintsDescriptor
return MessagingService.VERSION_50;
case VERSION_60:
return MessagingService.VERSION_60;
case VERSION_61:
return MessagingService.VERSION_61;
default:
throw new AssertionError();
}

View File

@ -206,8 +206,11 @@ public class Journal<K, V> implements Shutdownable
public void start()
{
if (state.get() == State.NORMAL)
return;
Invariants.require(state.compareAndSet(State.UNINITIALIZED, State.INITIALIZING),
"Unexpected journal state during initialization", state);
"Unexpected journal state during initialization: %s", state);
metrics.register(flusher);
deleteTmpFiles();
@ -301,7 +304,7 @@ public class Journal<K, V> implements Shutdownable
releaser.awaitTermination(1, TimeUnit.MINUTES);
metrics.deregister();
Invariants.require(state.compareAndSet(State.SHUTDOWN, State.TERMINATED),
"Unexpected journal state while trying to shut down", state);
"Unexpected journal state while trying to shut down: %s", state);
}
catch (InterruptedException e)
{

View File

@ -49,6 +49,7 @@ public class ReadRepairMetrics
*/
public static final Meter repairedBlockingFromAccord = Metrics.meter(factory.createMetricName("RepairedBlockingFromAccord"));
public static final Meter reconcileRead = Metrics.meter(factory.createMetricName("ReconcileRead"));
public static final Meter trackedReconcile = Metrics.meter(factory.createMetricName("TrackedReconcile"));
/** @deprecated See CASSANDRA-13910 */
@Deprecated(since = "4.0")

View File

@ -59,6 +59,7 @@ import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.metrics.Sampler.SamplerType;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.reads.ReplicaFilteringProtection;
import org.apache.cassandra.utils.EstimatedHistogram;
import org.apache.cassandra.utils.ExpMovingAverage;
import org.apache.cassandra.utils.MovingAverage;
@ -377,7 +378,7 @@ public class TableMetrics
public final Meter replicaFilteringProtectionRequests;
/**
* This histogram records the maximum number of rows {@link org.apache.cassandra.service.reads.ReplicaFilteringProtection}
* This histogram records the maximum number of rows {@link ReplicaFilteringProtection}
* caches at a point in time per query. With no replica divergence, this is equivalent to the maximum number of
* cached rows in a single partition during a query. It can be helpful when choosing appropriate values for the
* replica_filtering_protection thresholds in cassandra.yaml.

View File

@ -61,6 +61,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
import static org.apache.cassandra.net.MessagingService.VERSION_40;
import static org.apache.cassandra.net.MessagingService.VERSION_50;
import static org.apache.cassandra.net.MessagingService.VERSION_60;
import static org.apache.cassandra.net.MessagingService.VERSION_61;
import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
import static org.apache.cassandra.utils.vint.VIntCoding.computeUnsignedVIntSize;
@ -246,6 +247,12 @@ public class Message<T> implements ResponseContext
return outWithParam(nextId(), verb, 0, payload, flag.addTo(0), null, null);
}
public static <T> Message<T> outWithParam(Verb verb, T payload, ParamType paramType, Object paramValue)
{
assert !verb.isResponse() : verb;
return outWithParam(nextId(), verb, payload, paramType, paramValue);
}
public static <T> Message<T> outWithFlags(Verb verb, T payload, MessageFlag flag1, MessageFlag flag2)
{
assert !verb.isResponse();
@ -1263,6 +1270,7 @@ public class Message<T> implements ResponseContext
private int serializedSize40;
private int serializedSize50;
private int serializedSize51;
private int serializedSize52;
/**
* Serialized size of the entire message, for the provided messaging version. Caches the calculated value.
@ -1283,6 +1291,10 @@ public class Message<T> implements ResponseContext
if (serializedSize51 == 0)
serializedSize51 = serializer.serializedSize(this, VERSION_60);
return serializedSize51;
case VERSION_61:
if (serializedSize52 == 0)
serializedSize52 = serializer.serializedSize(this, VERSION_61);
return serializedSize52;
default:
throw new IllegalStateException("Unknown serialization version " + version);
}
@ -1290,7 +1302,8 @@ public class Message<T> implements ResponseContext
private int payloadSize40 = -1;
private int payloadSize50 = -1;
private int payloadSize51 = -1;
private int payloadSize60 = -1;
private int payloadSize61 = -1;
private int payloadSize(int version)
{
@ -1305,9 +1318,13 @@ public class Message<T> implements ResponseContext
payloadSize50 = serializer.payloadSize(this, VERSION_50);
return payloadSize50;
case VERSION_60:
if (payloadSize51 < 0)
payloadSize51 = serializer.payloadSize(this, VERSION_60);
return payloadSize51;
if (payloadSize60 < 0)
payloadSize60 = serializer.payloadSize(this, VERSION_60);
return payloadSize60;
case VERSION_61:
if (payloadSize61 < 0)
payloadSize61 = serializer.payloadSize(this, VERSION_61);
return payloadSize61;
default:
throw new IllegalStateException("Unkown serialization version " + version);

View File

@ -225,7 +225,9 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
// c14227 TTL overflow, 'uint' timestamps
VERSION_50(13),
// TCM, index hints
VERSION_60(14);
VERSION_60(14),
// Mutation Tracking
VERSION_61(15);
public static final Version MIN_ACCORD_VERSION = Version.VERSION_60;
@ -266,8 +268,9 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
public static final int VERSION_40 = 12;
public static final int VERSION_50 = 13; // c14227 TTL overflow, 'uint' timestamps
public static final int VERSION_60 = 14; // TCM, index hints
public static final int VERSION_61 = 15; // Mutation Tracking
public static final int minimum_version = VERSION_40;
public static final int maximum_version = VERSION_60;
public static final int maximum_version = VERSION_61;
// we want to use a modified behavior for the tools and clients - that is, since they are not running a server, they
// should not need to run in a compatibility mode. They should be able to connect to the server regardless whether
// it uses messaving version 4 or 5
@ -309,7 +312,7 @@ public class MessagingService extends MessagingServiceMBeanImpl implements Messa
private static Version currentVersion()
{
return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? Version.VERSION_40 : Version.VERSION_60;
return DatabaseDescriptor.getStorageCompatibilityMode().isBefore(5) ? Version.VERSION_40 : Version.VERSION_61;
}
private static class MSHandle

View File

@ -131,6 +131,12 @@ import org.apache.cassandra.service.paxos.cleanup.PaxosStartPrepareCleanup;
import org.apache.cassandra.service.paxos.cleanup.PaxosUpdateLowBallot;
import org.apache.cassandra.service.paxos.v1.PrepareVerbHandler;
import org.apache.cassandra.service.paxos.v1.ProposeVerbHandler;
import org.apache.cassandra.service.reads.tracked.ReadReconcileNotify;
import org.apache.cassandra.service.reads.tracked.ReadReconcileReceive;
import org.apache.cassandra.service.reads.tracked.ReadReconcileSend;
import org.apache.cassandra.service.reads.tracked.TrackedDataResponse;
import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.service.reads.tracked.TrackedSummaryResponse;
import org.apache.cassandra.streaming.DataMovement;
import org.apache.cassandra.streaming.DataMovementVerbHandler;
import org.apache.cassandra.streaming.ReplicationDoneVerbHandler;
@ -314,6 +320,18 @@ public enum Verb
TCM_FETCH_PEER_LOG_RSP (818, P0, shortTimeout, FETCH_METADATA, MessageSerializers::logStateSerializer, RESPONSE_HANDLER ),
TCM_FETCH_PEER_LOG_REQ (819, P0, rpcTimeout, FETCH_METADATA, () -> FetchPeerLog.serializer, () -> FetchPeerLog.Handler.instance, TCM_FETCH_PEER_LOG_RSP ),
// tracked replication
READ_RECONCILE_SEND (901, P0, rpcTimeout, READ, () -> ReadReconcileSend.serializer, () -> ReadReconcileSend.verbHandler ),
READ_RECONCILE_RCV (902, P0, rpcTimeout, MUTATION, () -> ReadReconcileReceive.serializer, () -> ReadReconcileReceive.verbHandler ),
READ_RECONCILE_NOTIFY (903, P0, rpcTimeout, REQUEST_RESPONSE, () -> ReadReconcileNotify.serializer, () -> ReadReconcileNotify.verbHandler ),
TRACKED_PARTITION_READ_RSP (906, P2, readTimeout, REQUEST_RESPONSE, () -> TrackedDataResponse.serializer, RESPONSE_HANDLER ),
TRACKED_PARTITION_READ_REQ (907, P3, readTimeout, READ, () -> TrackedRead.DataRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_PARTITION_READ_RSP),
TRACKED_RANGE_READ_RSP (908, P2, rangeTimeout, REQUEST_RESPONSE, () -> TrackedDataResponse.serializer, RESPONSE_HANDLER ),
TRACKED_RANGE_READ_REQ (909, P3, rangeTimeout, READ, () -> TrackedRead.DataRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_RANGE_READ_RSP ),
TRACKED_SUMMARY_RSP (910, P2, readTimeout, REQUEST_RESPONSE, () -> TrackedSummaryResponse.serializer, () -> TrackedSummaryResponse.verbHandler ),
TRACKED_SUMMARY_REQ (911, P3, readTimeout, READ, () -> TrackedRead.SummaryRequest.serializer, () -> TrackedRead.verbHandler, TRACKED_SUMMARY_RSP ),
INITIATE_DATA_MOVEMENTS_RSP (814, P1, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),
INITIATE_DATA_MOVEMENTS_REQ (815, P1, rpcTimeout, MISC, () -> DataMovement.serializer, () -> DataMovementVerbHandler.instance, INITIATE_DATA_MOVEMENTS_RSP ),
DATA_MOVEMENT_EXECUTED_RSP (816, P1, rpcTimeout, MISC, () -> NoPayload.serializer, RESPONSE_HANDLER ),

View File

@ -0,0 +1,240 @@
/*
* 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.replication;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.TableId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
public abstract class CoordinatorLog
{
private static final Logger logger = LoggerFactory.getLogger(CoordinatorLog.class);
protected final int localHostId;
protected final CoordinatorLogId logId;
protected final Participants participants;
/**
* State machines and an Id <-> token index for unreconciled mutation ids that exist oh this host.
*/
private final LocalMutationStates unreconciledMutations;
protected final Offsets.Mutable[] witnessedIds;
protected final Offsets.Mutable reconciledIds;
protected final ReadWriteLock lock;
CoordinatorLog(int localHostId, CoordinatorLogId logId, Participants participants)
{
this.localHostId = localHostId;
this.logId = logId;
this.participants = participants;
this.unreconciledMutations = new LocalMutationStates();
this.lock = new ReentrantReadWriteLock();
Offsets.Mutable[] ids = new Offsets.Mutable[participants.size()];
for (int i = 0; i < participants.size(); i++)
ids[i] = new Offsets.Mutable(logId);
witnessedIds = ids;
reconciledIds = new Offsets.Mutable(logId);
}
static CoordinatorLog create(int localHostId, CoordinatorLogId id, Participants participants)
{
return id.hostId == localHostId ? new CoordinatorLogPrimary(localHostId, id, participants)
: new CoordinatorLogReplica(localHostId, id, participants);
}
void witnessedRemoteMutation(MutationId mutationId, int onHostId)
{
logger.trace("witnessed remote mutation {} from {}", mutationId, onHostId);
lock.writeLock().lock();
try
{
if (!get(onHostId).add(mutationId.offset()))
return; // already witnessed
if (!getLocal().contains(mutationId.offset()))
return; // local host hasn't witnessed -> no cleanup needed
// see if any other replicas haven't witnessed the id yet
boolean allOtherReplicasWitnessed = true;
for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
{
int hostId = participants.get(i);
if (hostId != onHostId && hostId != localHostId && !get(hostId).contains(mutationId.offset()))
allOtherReplicasWitnessed = false;
}
if (allOtherReplicasWitnessed)
{
logger.trace("marking mutation {} as fully reconciled", mutationId);
// if all replicas have now witnessed the id, remove it from the index
unreconciledMutations.remove(mutationId.offset());
reconciledIds.add(mutationId.offset());
}
}
finally
{
lock.writeLock().unlock();
}
}
void startWriting(Mutation mutation)
{
lock.writeLock().lock();
try
{
if (getLocal().contains(mutation.id().offset()))
return; // already witnessed; shouldn't get to this path often (duplicate mutation)
unreconciledMutations.startWriting(mutation);
}
finally
{
lock.writeLock().unlock();
}
}
void finishWriting(Mutation mutation)
{
logger.trace("witnessed local mutation {}", mutation.id());
lock.writeLock().lock();
try
{
if (!getLocal().add(mutation.id().offset()))
throw new IllegalStateException("finishWriting() called on a reconciled mutation");
// see if any other replicas haven't witnessed the id yet
boolean allOtherReplicasWitnessed = true;
for (int i = 0; i < participants.size() && allOtherReplicasWitnessed; i++)
{
int hostId = participants.get(i);
if (hostId != localHostId && !get(hostId).contains(mutation.id().offset()))
allOtherReplicasWitnessed = false;
}
// if some replicas also haven't witnessed the mutation yet, we should update local mutation state;
// otherwise we are the last node to witness this mutation, and can clean it up
if (allOtherReplicasWitnessed)
reconciledIds.add(mutation.id().offset());
else
unreconciledMutations.finishWriting(mutation);
}
finally
{
lock.writeLock().unlock();
}
}
/**
* Look up unreconciled sequence ids of mutations witnessed by this host in this coordinataor log.
* Adds the ids to the supplied collection, so it can be reused to aggregate lookups for multiple logs.
*/
boolean collectOffsetsFor(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever unreconciledInto, Offsets.OffsetReciever reconciledInto)
{
lock.readLock().lock();
try
{
reconciledInto.addAll(reconciledIds);
return unreconciledMutations.collect(token, tableId, includePending, unreconciledInto);
}
finally
{
lock.readLock().unlock();
}
}
/**
* Look up unreconciled sequence ids of mutations witnessed by this host in this coordinataor log.
* Adds the ids to the supplied collection, so it can be reused to aggregate lookups for multiple logs.
*/
boolean collectOffsetsFor(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever unreconciledInto, Offsets.OffsetReciever reconciledInto)
{
lock.readLock().lock();
try
{
reconciledInto.addAll(reconciledIds);
return unreconciledMutations.collect(range, tableId, includePending, unreconciledInto);
}
finally
{
lock.readLock().unlock();
}
}
protected Offsets.Mutable get(int hostId)
{
return witnessedIds[participants.indexOf(hostId)];
}
protected Offsets.Mutable getLocal()
{
return witnessedIds[participants.indexOf(localHostId)];
}
public static class CoordinatorLogPrimary extends CoordinatorLog
{
AtomicLong sequenceId = new AtomicLong(-1);
CoordinatorLogPrimary(int localHostId, CoordinatorLogId logId, Participants participants)
{
super(localHostId, logId, participants);
}
MutationId nextId()
{
return new MutationId(logId.asLong(), nextSequenceId());
}
private long nextSequenceId()
{
while (true)
{
long prev = sequenceId.get();
int prevOffset = MutationId.offset(prev);
int prevTimestamp = MutationId.timestamp(prev);
int nextOffset = prevOffset + 1;
int nextTimestamp = Math.max(prevTimestamp + 1, (int) (currentTimeMillis() / 1000L));
long next = MutationId.sequenceId(nextOffset, nextTimestamp);
if (sequenceId.compareAndSet(prev, next))
return next;
}
}
}
public static class CoordinatorLogReplica extends CoordinatorLog
{
CoordinatorLogReplica(int localHostId, CoordinatorLogId logId, Participants participants)
{
super(localHostId, logId, participants);
}
}
}

View File

@ -0,0 +1,127 @@
/*
* 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.replication;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import java.io.IOException;
import java.io.Serializable;
import java.util.Comparator;
public class CoordinatorLogId implements Serializable
{
/** TCM host ID */
protected final int hostId;
/**
* Host log ID (unique within the host).
* Allocated anew on host restart - one per token range replicated by the host.
* Persisted on allocation, unique within the host.
*/
protected final int hostLogId;
CoordinatorLogId(long id)
{
this(hostId(id), hostLogId(id));
}
CoordinatorLogId(int hostId, int hostLogId)
{
this.hostId = hostId;
this.hostLogId = hostLogId;
}
public int hostId()
{
return hostId;
}
public int hostLogId()
{
return hostLogId;
}
public long asLong()
{
return asLong(hostId, hostLogId);
}
static long asLong(int hostId, int hostLogId)
{
return ((long) hostId << 32) | hostLogId;
}
static int hostId(long coordinatorLogId)
{
return (int) (coordinatorLogId >>> 32);
}
static int hostLogId(long coordinatorLogId)
{
return (int) coordinatorLogId;
}
@Override
public String toString()
{
return "CoordinatorLogId{" + hostId + ", " + hostLogId + '}';
}
@Override
public boolean equals(Object o)
{
if (o == null || getClass() != o.getClass()) return false;
CoordinatorLogId logId = (CoordinatorLogId) o;
return hostId == logId.hostId && hostLogId == logId.hostLogId;
}
@Override
public int hashCode()
{
return Integer.hashCode(hostLogId) + 31 * Integer.hashCode(hostId);
}
public static final Comparator<CoordinatorLogId> comparator = (l, r) -> Long.compareUnsigned(l.asLong(), r.asLong());
public static final IVersionedSerializer<CoordinatorLogId> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(CoordinatorLogId logId, DataOutputPlus out, int version) throws IOException
{
out.writeInt(logId.hostId);
out.writeInt(logId.hostLogId);
}
@Override
public CoordinatorLogId deserialize(DataInputPlus in, int version) throws IOException
{
int hostId = in.readInt();
int hostLogId = in.readInt();
return new CoordinatorLogId(hostId, hostLogId);
}
@Override
public long serializedSize(CoordinatorLogId logId, int version)
{
return TypeSizes.sizeof(logId.hostId) + TypeSizes.sizeof(logId.hostLogId);
}
};
}

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.replication;
import java.util.Collection;
import java.util.Comparator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.agrona.collections.Int2ObjectHashMap;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.schema.TableId;
/**
* Tracks unreconciled local mutations - the subset of all unreconciled mutations
* that have been witnessed, or are currently being written to, on the local node.
*/
class LocalMutationStates
{
private final Int2ObjectHashMap<Entry> statesMap = new Int2ObjectHashMap<>();
private final SortedSet<Entry> statesSet = new TreeSet<>(Entry.comparator);
enum Visibility
{
PENDING, // written to the journal, but not yet to LSM
VISIBLE, // written to both the journal and LSM
}
private static final class Entry
{
private static final Comparator<Entry> comparator = (left, right) ->
{
int cmp = left.token.compareTo(right.token);
return (cmp != 0) ? cmp : Integer.compare(left.offset, right.offset);
};
final Token token;
final int offset;
final Object tableOrTables;
private Visibility visibility;
Entry(Token token, int offset, Object tableOrTables, Visibility visibility)
{
this.token = token;
this.offset = offset;
this.tableOrTables = tableOrTables;
this.visibility = visibility;
}
static Entry create(Mutation mutation)
{
Collection<TableId> ids = mutation.getTableIds();
Preconditions.checkArgument(!ids.isEmpty());
return new Entry(mutation.key().getToken(), mutation.id().offset(), tableOrTables(mutation), Visibility.PENDING);
}
private static Object tableOrTables(Mutation mutation)
{
Collection<TableId> ids = mutation.getTableIds();
Preconditions.checkArgument(!ids.isEmpty());
return ids.size() == 1 ? ids.iterator().next() : Sets.newHashSet(mutation.getTableIds());
}
boolean contains(TableId tableId)
{
return tableOrTables instanceof Set
? ((Set<?>) tableOrTables).contains(tableId)
: tableId.equals(tableOrTables);
}
boolean isVisible()
{
return visibility == Visibility.VISIBLE;
}
static Entry start(Token token, boolean isInclusive)
{
return new Entry(token, isInclusive ? 0 : Integer.MAX_VALUE, null, null);
}
static Entry end(Token token, boolean isInclusive)
{
return new Entry(token, isInclusive ? Integer.MAX_VALUE : 0, null, null);
}
@Override
public boolean equals(Object o)
{
if (!(o instanceof Entry))
return false;
Entry that = (Entry) o;
return this.offset == that.offset && this.token.equals(that.token);
}
}
void startWriting(Mutation mutation)
{
Entry entry = Entry.create(mutation);
statesMap.put(entry.offset, entry);
statesSet.add(entry);
}
void finishWriting(Mutation mutation)
{
Entry entry = statesMap.get(mutation.id().offset());
Preconditions.checkNotNull(entry);
entry.visibility = Visibility.VISIBLE;
}
void remove(int offset)
{
Entry state = statesMap.remove(offset);
Preconditions.checkNotNull(state);
statesSet.remove(state);
}
boolean collect(Token token, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
SortedSet<Entry> subset = statesSet.subSet(Entry.start(token, true), Entry.end(token, true));
return collect(subset, tableId, includePending, into);
}
boolean collect(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
Entry start = Entry.start(range.left.getToken(), range.left.kind() != PartitionPosition.Kind.MAX_BOUND);
Entry end = Entry.end(range.right.getToken(), range.right.kind() != PartitionPosition.Kind.MIN_BOUND);
return collect(start, end, tableId, includePending, into);
}
private boolean collect(SortedSet<Entry> subset, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
boolean found = false;
for (Entry entry : subset)
{
if (entry.contains(tableId) && (includePending || entry.isVisible()))
{
into.add(entry.offset);
found = true;
}
}
return found;
}
private boolean collect(Entry start, Entry end, TableId tableId, boolean includePending, Offsets.OffsetReciever into)
{
int cmp = start.token.compareTo(end.token);
if (cmp == 0)
{
// full range
return collect(statesSet, tableId, includePending, into);
}
else if (cmp > 0)
{
// wrap around range
boolean lFound = collect(statesSet.headSet(end), tableId, includePending, into);
boolean rFound = collect(statesSet.tailSet(start), tableId, includePending, into);
return lFound || rFound;
}
else
{
// contiguous range
return collect(statesSet.subSet(start, end), tableId, includePending, into);
}
}
}

View File

@ -0,0 +1,230 @@
/*
* 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.replication;
import java.io.IOException;
import java.util.Iterator;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import org.agrona.collections.Long2ObjectHashMap;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
public abstract class Log2OffsetsMap<T extends Offsets> implements Iterable<ShortMutationId>
{
abstract Long2ObjectHashMap<T> offsetMap();
@Override
public Iterator<ShortMutationId> iterator()
{
return Iterables.concat(offsetMap().values()).iterator();
}
public int idCount()
{
int count = 0;
for (T offsets : offsetMap().values())
count += offsets.offsetCount();
return count;
}
public boolean isEmpty()
{
for (T offsets : offsetMap().values())
if (!offsets.isEmpty())
return false;
return true;
}
private static abstract class AbstractMutable<T extends Offsets.AbstractMutable<T>> extends Log2OffsetsMap<T>
{
protected final Long2ObjectHashMap<T> offsetMap = new Long2ObjectHashMap<>();
@Override
Long2ObjectHashMap<T> offsetMap()
{
return offsetMap;
}
protected abstract T createOrNull(Offsets.RangeIterator iterator);
protected abstract T create(CoordinatorLogId logId);
protected abstract T copy(Offsets offsets);
protected T create(long logId)
{
return create(new CoordinatorLogId(logId));
}
public void add(ShortMutationId id)
{
T offsets = offsetMap.computeIfAbsent(id.logId(), this::create);
offsets.add(id.offset());
}
public void add(Offsets offsets)
{
if (offsets.isEmpty())
return;
T existing = offsetMap.get(offsets.logId().asLong());
if (existing == null)
{
offsetMap.put(offsets.logId().asLong(), copy(offsets));
return;
}
existing.addAll(offsets);
}
public void addAll(Log2OffsetsMap<?> that)
{
for (Offsets offsets : that.offsetMap().values())
add(offsets);
}
public void remove(Offsets offsets)
{
T existing = offsetMap.get(offsets.logId().asLong());
if (existing == null)
return;
if (existing.isEmpty())
{
offsetMap.remove(offsets.logId().asLong());
return;
}
T next = createOrNull(Offsets.difference(existing.rangeIterator(), offsets.rangeIterator()));
if (next == null)
offsetMap.remove(offsets.logId().asLong());
else
offsetMap.put(offsets.logId().asLong(), next);
}
public void removeAll(Log2OffsetsMap<?> that)
{
for (Offsets offsets : that.offsetMap().values())
remove(offsets);
}
}
public static class Mutable extends AbstractMutable<Offsets.Mutable>
{
@Override
protected Offsets.Mutable createOrNull(Offsets.RangeIterator iterator)
{
return Offsets.Mutable.createOrNull(iterator);
}
@Override
protected Offsets.Mutable create(CoordinatorLogId logId)
{
return new Offsets.Mutable(logId);
}
@Override
protected Offsets.Mutable copy(Offsets offsets)
{
return Offsets.Mutable.copy(offsets);
}
}
public static class Immutable extends Log2OffsetsMap<Offsets.Immutable>
{
private final Long2ObjectHashMap<Offsets.Immutable> offsetMap;
private Immutable(Long2ObjectHashMap<Offsets.Immutable> offsetMap)
{
this.offsetMap = offsetMap;
}
@Override
Long2ObjectHashMap<Offsets.Immutable> offsetMap()
{
return offsetMap;
}
public static class Builder extends AbstractMutable<Offsets.Immutable.Builder>
{
@Override
protected Offsets.Immutable.Builder createOrNull(Offsets.RangeIterator iterator)
{
return Offsets.Immutable.Builder.createOrNull(iterator);
}
@Override
protected Offsets.Immutable.Builder create(CoordinatorLogId logId)
{
return new Offsets.Immutable.Builder(logId);
}
@Override
protected Offsets.Immutable.Builder copy(Offsets offsets)
{
return Offsets.Immutable.Builder.copy(offsets);
}
public Log2OffsetsMap.Immutable build()
{
Long2ObjectHashMap<Offsets.Immutable> result = new Long2ObjectHashMap<>();
offsetMap.forEachLong((key, builder) -> result.put(key, builder.build()));
return new Immutable(result);
}
}
public static final IVersionedSerializer<Log2OffsetsMap.Immutable> serializer = new IVersionedSerializer<Log2OffsetsMap.Immutable>()
{
@Override
public void serialize(Log2OffsetsMap.Immutable mo, DataOutputPlus out, int version) throws IOException
{
out.writeInt(mo.offsetMap.size());
for (Offsets.Immutable offsets : mo.offsetMap().values())
Offsets.serializer.serialize(offsets, out, version);
}
@Override
public Log2OffsetsMap.Immutable deserialize(DataInputPlus in, int version) throws IOException
{
Long2ObjectHashMap<Offsets.Immutable> offsetMap = new Long2ObjectHashMap<>();
int size = in.readInt();
for (int i=0; i<size; i++)
{
Offsets.Immutable offsets = Offsets.serializer.deserialize(in, version);
long key = offsets.logId().asLong();
Preconditions.checkState(!offsetMap.containsKey(key));
offsetMap.put(key, offsets);
}
return new Immutable(offsetMap);
}
@Override
public long serializedSize(Log2OffsetsMap.Immutable mo, int version)
{
long size = TypeSizes.INT_SIZE;
for (Offsets.Immutable offsets : mo.offsetMap.values())
size += Offsets.serializer.serializedSize(offsets, version);
return size;
}
};
}
}

View File

@ -0,0 +1,135 @@
/*
* 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.replication;
import java.io.IOException;
import java.util.Comparator;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
/**
* Full mutation id, with the addition of timestamp component.
* <p>
* equals() and hashCode() are intentionally not overridden by this class, since log id and offset alone
* are meant to uniquely identify a mutation.
*/
public class MutationId extends ShortMutationId
{
private static final long NONE_LOG_ID = Long.MIN_VALUE;
private static final long NONE_SEQUENCE_ID = Long.MIN_VALUE;
private static final int NONE_OFFSET = offset(NONE_SEQUENCE_ID);
private static final int NONE_TIMESTAMP = timestamp(NONE_SEQUENCE_ID);
private static final MutationId NONE = new MutationId(NONE_LOG_ID, NONE_SEQUENCE_ID);
/**
* 4 byte timestamp. The timestamp is monotonically non-decreasing.
* The offset alone is sufficient to identify the entry within a coordinator
* log, the timestamp is added for correlation purposes.
*/
protected final int timestamp;
public MutationId(long logId, long sequenceId)
{
super(logId, offset(sequenceId));
this.timestamp = timestamp(sequenceId);
}
public long sequenceId()
{
return sequenceId(offset, timestamp);
}
public int timestamp()
{
return timestamp;
}
public static long sequenceId(int offset, int timestamp)
{
return ((long) offset << 32) | timestamp;
}
public static int offset(long sequenceId)
{
return (int) (0xffffffffL & (sequenceId >> 32));
}
public static int timestamp(long sequenceId)
{
return (int) (0xffffffffL & sequenceId);
}
// FIXME: used in place of figuring out if we should use a mutation id or not
public static MutationId fixme()
{
return none();
}
public static MutationId none()
{
return NONE;
}
public boolean isNone()
{
if (this == NONE)
return true;
return logId() == NONE_LOG_ID && offset() == NONE_OFFSET && timestamp() == NONE_TIMESTAMP;
}
@Override
public String toString()
{
return "MutationId{" + hostId() + ", " + hostLogId() + ", " + offset() + ", " + timestamp() + '}';
}
/**
* The comparator is intentionally not overridden by this class, since log id and offset alone
* are meant to uniquely identify a mutation, and only offset determines the order within a log.
*/
public static final Comparator<MutationId> comparator = ShortMutationId.comparator::compare;
public static final IVersionedSerializer<MutationId> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(MutationId id, DataOutputPlus out, int version) throws IOException
{
out.writeLong(id.logId());
out.writeLong(id.sequenceId());
}
@Override
public MutationId deserialize(DataInputPlus in, int version) throws IOException
{
long logId = in.readLong();
long sequenceId = in.readLong();
if (logId == NONE_LOG_ID && sequenceId == NONE_SEQUENCE_ID)
return none();
return new MutationId(logId, sequenceId);
}
@Override
public long serializedSize(MutationId id, int version)
{
return TypeSizes.sizeof(id.logId()) + TypeSizes.sizeof(id.sequenceId());
}
};
}

View File

@ -15,38 +15,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.service.tracking;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.zip.Checksum;
import javax.annotation.Nullable;
package org.apache.cassandra.replication;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.journal.Journal;
import org.apache.cassandra.journal.KeySupport;
import org.apache.cassandra.journal.Params;
import org.apache.cassandra.journal.RecordConsumer;
import org.apache.cassandra.journal.RecordPointer;
import org.apache.cassandra.journal.SegmentCompactor;
import org.apache.cassandra.journal.ValueSerializer;
import org.apache.cassandra.journal.*;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.FBUtilities;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.zip.Checksum;
public class MutationJournal
{
private final Journal<MutationId, Mutation> journal;
public static final MutationJournal instance = new MutationJournal();
private final Journal<ShortMutationId, Mutation> journal;
private MutationJournal()
{
@ -69,25 +64,25 @@ public class MutationJournal
journal.shutdown();
}
public RecordPointer write(MutationId id, Mutation mutation)
public RecordPointer write(ShortMutationId id, Mutation mutation)
{
return journal.blockingWrite(id, mutation);
}
@Nullable
public Mutation read(MutationId id)
public Mutation read(ShortMutationId id)
{
return journal.readLast(id);
}
public boolean read(MutationId id, RecordConsumer<MutationId> consumer)
public boolean read(ShortMutationId id, RecordConsumer<ShortMutationId> consumer)
{
return journal.readLast(id, consumer);
}
public void readAll(Iterable<MutationId> ids, Collection<Mutation> into)
public void readAll(Iterable<ShortMutationId> ids, Collection<Mutation> into)
{
for (MutationId id : ids)
for (ShortMutationId id : ids)
{
Mutation mutation = read(id);
Preconditions.checkState(mutation != null);
@ -112,7 +107,18 @@ public class MutationJournal
@Override
public FlushMode flushMode()
{
return FlushMode.PERIODIC;
Config.CommitLogSync mode = DatabaseDescriptor.getCommitLogSync();
switch (DatabaseDescriptor.getCommitLogSync())
{
case batch:
return FlushMode.BATCH;
case periodic:
return FlushMode.PERIODIC;
case group:
return FlushMode.GROUP;
default:
throw new IllegalStateException("Unhandled flush mode: " + mode);
}
}
@Override
@ -146,88 +152,88 @@ public class MutationJournal
}
}
static class MutationIdSupport implements KeySupport<MutationId>
static class MutationIdSupport implements KeySupport<ShortMutationId>
{
static final int LOG_ID_OFFSET = 0;
static final int SEQUENCE_ID_OFFSET = LOG_ID_OFFSET + TypeSizes.LONG_SIZE;
static final int OFFSET_OFFSET = LOG_ID_OFFSET + TypeSizes.LONG_SIZE;
@Override
public int serializedSize(int userVersion)
{
return TypeSizes.LONG_SIZE // logId
+ TypeSizes.LONG_SIZE; // sequenceId
+ TypeSizes.INT_SIZE; // offset
}
@Override
public void serialize(MutationId id, DataOutputPlus out, int userVersion) throws IOException
public void serialize(ShortMutationId id, DataOutputPlus out, int userVersion) throws IOException
{
out.writeLong(id.logId);
out.writeLong(id.sequenceId);
out.writeLong(id.logId());
out.writeInt(id.offset());
}
@Override
public void serialize(MutationId id, ByteBuffer out, int userVersion) throws IOException
public void serialize(ShortMutationId id, ByteBuffer out, int userVersion) throws IOException
{
out.putLong(id.logId);
out.putLong(id.sequenceId);
out.putLong(id.logId());
out.putInt(id.offset());
}
@Override
public MutationId deserialize(DataInputPlus in, int userVersion) throws IOException
public ShortMutationId deserialize(DataInputPlus in, int userVersion) throws IOException
{
long logId = in.readLong();
long sequenceId = in.readLong();
return new MutationId(logId, sequenceId);
int offset = in.readInt();
return new ShortMutationId(logId, offset);
}
@Override
public MutationId deserialize(ByteBuffer buffer, int position, int userVersion)
public ShortMutationId deserialize(ByteBuffer buffer, int position, int userVersion)
{
long logId = buffer.getLong(position + LOG_ID_OFFSET);
long sequenceId = buffer.getLong(position + SEQUENCE_ID_OFFSET);
return new MutationId(logId, sequenceId);
int offset = buffer.getInt(position + OFFSET_OFFSET);
return new ShortMutationId(logId, offset);
}
@Override
public MutationId deserialize(ByteBuffer buffer, int userVersion)
public ShortMutationId deserialize(ByteBuffer buffer, int userVersion)
{
long logId = buffer.getLong();
long sequenceId = buffer.getLong();
return new MutationId(logId, sequenceId);
int offset = buffer.getInt();
return new ShortMutationId(logId, offset);
}
@Override
public void updateChecksum(Checksum crc, MutationId id, int userVersion)
public void updateChecksum(Checksum crc, ShortMutationId id, int userVersion)
{
FBUtilities.updateChecksumLong(crc, id.logId);
FBUtilities.updateChecksumLong(crc, id.sequenceId);
FBUtilities.updateChecksumLong(crc, id.logId());
FBUtilities.updateChecksumInt(crc, id.offset());
}
@Override
public int compareWithKeyAt(MutationId id, ByteBuffer buffer, int position, int userVersion)
public int compareWithKeyAt(ShortMutationId id, ByteBuffer buffer, int position, int userVersion)
{
int cmp = Long.compare(id.logId, buffer.getLong(position + LOG_ID_OFFSET));
return cmp != 0 ? cmp : Long.compare(id.sequenceId, buffer.getLong(position + SEQUENCE_ID_OFFSET));
int cmp = Long.compare(id.logId(), buffer.getLong(position + LOG_ID_OFFSET));
return cmp != 0 ? cmp : Integer.compare(id.offset(), buffer.getInt(position + OFFSET_OFFSET));
}
@Override
public int compare(MutationId id1, MutationId id2)
public int compare(ShortMutationId id1, ShortMutationId id2)
{
int cmp = Long.compare(id1.logId, id2.logId);
return cmp != 0 ? cmp : Long.compare(id1.sequenceId, id2.sequenceId);
int cmp = Long.compare(id1.logId(), id2.logId());
return cmp != 0 ? cmp : Integer.compare(id1.offset(), id2.offset());
}
}
static class MutationSerializer implements ValueSerializer<MutationId, Mutation>
static class MutationSerializer implements ValueSerializer<ShortMutationId, Mutation>
{
@Override
public void serialize(MutationId id, Mutation mutation, DataOutputPlus out, int userVersion) throws IOException
public void serialize(ShortMutationId id, Mutation mutation, DataOutputPlus out, int userVersion) throws IOException
{
Mutation.serializer.serialize(mutation, out, userVersion);
}
@Override
public Mutation deserialize(MutationId id, DataInputPlus in, int userVersion) throws IOException
public Mutation deserialize(ShortMutationId id, DataInputPlus in, int userVersion) throws IOException
{
return Mutation.serializer.deserialize(in, userVersion);
}

View File

@ -0,0 +1,351 @@
/*
* 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.replication;
import java.io.IOException;
import java.util.*;
import com.google.common.base.Preconditions;
import org.agrona.collections.Long2ObjectHashMap;
import org.apache.cassandra.db.Digest;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.schema.TableId;
public class MutationSummary
{
public static class CoordinatorSummary
{
private static final Comparator<CoordinatorSummary> idComparator =
(l, r) -> CoordinatorLogId.comparator.compare(l.logId(), r.logId());
public final Offsets.Immutable reconciled;
public final Offsets.Immutable unreconciled;
public CoordinatorSummary(Offsets.Immutable reconciled, Offsets.Immutable unreconciled)
{
Preconditions.checkArgument(reconciled.logId().equals(unreconciled.logId()));
this.reconciled = reconciled;
this.unreconciled = unreconciled;
}
@Override
public boolean equals(Object o)
{
if (o == null || getClass() != o.getClass()) return false;
CoordinatorSummary summary = (CoordinatorSummary) o;
return reconciled.equals(summary.reconciled) && unreconciled.equals(summary.unreconciled);
}
@Override
public int hashCode()
{
return Objects.hash(reconciled, unreconciled);
}
@Override
public String toString()
{
return "CoordinatorSummary{" +
"logId=" + logId() +
", reconciled=" + reconciled +
", unreconciled=" + unreconciled +
'}';
}
public CoordinatorLogId logId()
{
return reconciled.logId();
}
boolean contains(int offset)
{
return reconciled.contains(offset) || unreconciled.contains(offset);
}
/**
* Finds all elements that are contained by {@code left} and not contained by {@code right}
*/
static void difference(CoordinatorSummary left, CoordinatorSummary right, Collection<ShortMutationId> into)
{
Offsets.RangeIterator leftIds = Offsets.union(left.reconciled.rangeIterator(), left.unreconciled.rangeIterator());
Offsets.RangeIterator rightIds = Offsets.union(right.reconciled.rangeIterator(), right.unreconciled.rangeIterator());
Offsets.RangeIterator missing = Offsets.difference(leftIds, rightIds);
Offsets.forEachOffset(missing, (logId, offset) -> into.add(new ShortMutationId(logId, offset)));
}
void digest(Digest digest)
{
reconciled.digest(digest);
unreconciled.digest(digest);
}
public static class Builder
{
public final CoordinatorLogId logId;
public final Offsets.Immutable.Builder reconciled;
public final Offsets.Immutable.Builder unreconciled;
public Builder(CoordinatorLogId logId)
{
this.logId = logId;
reconciled = new Offsets.Immutable.Builder(logId);
unreconciled = new Offsets.Immutable.Builder(logId);
}
boolean isEmpty()
{
return reconciled.isEmpty() && unreconciled.isEmpty();
}
public CoordinatorSummary build()
{
return new CoordinatorSummary(reconciled.build(), unreconciled.build());
}
}
public static final IVersionedSerializer<CoordinatorSummary> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(CoordinatorSummary t, DataOutputPlus out, int version) throws IOException
{
Offsets.serializer.serialize(t.reconciled, out, version);
Offsets.serializer.serialize(t.unreconciled, out, version);
}
@Override
public CoordinatorSummary deserialize(DataInputPlus in, int version) throws IOException
{
return new CoordinatorSummary(Offsets.serializer.deserialize(in, version),
Offsets.serializer.deserialize(in, version));
}
@Override
public long serializedSize(CoordinatorSummary t, int version)
{
return Offsets.serializer.serializedSize(t.reconciled, version)
+ Offsets.serializer.serializedSize(t.unreconciled, version);
}
};
}
public static class Builder
{
public final TableId tableId;
private final Long2ObjectHashMap<CoordinatorSummary.Builder> builders = new Long2ObjectHashMap<>();
public Builder(TableId tableId)
{
this.tableId = tableId;
}
public CoordinatorSummary.Builder builderForLog(CoordinatorLogId logId)
{
CoordinatorSummary.Builder builder = builders.get(logId.asLong());
if (builder == null)
{
builder = new CoordinatorSummary.Builder(logId);
builders.put(logId.asLong(), builder);
}
return builder;
}
public MutationSummary build()
{
List<CoordinatorSummary> summaries = new ArrayList<>(builders.size());
for (CoordinatorSummary.Builder builder : builders.values())
if (!builder.isEmpty())
summaries.add(builder.build());
summaries.sort(CoordinatorSummary.idComparator);
return new MutationSummary(tableId, summaries);
}
}
private final TableId tableId;
private final List<CoordinatorSummary> summaries;
private transient final Long2ObjectHashMap<CoordinatorSummary> coordinatorSummaryMap = new Long2ObjectHashMap<>();
private MutationSummary(TableId tableId, List<CoordinatorSummary> summaries)
{
long lastId = 0;
for (int i=0, mi=summaries.size(); i<mi; i++)
{
CoordinatorSummary summary = summaries.get(i);
long thisId = summary.logId().asLong();
if (i > 0 && thisId <= lastId)
throw new IllegalArgumentException("duplicated or unsorted log id found");
coordinatorSummaryMap.put(thisId, summary);
lastId = thisId;
}
this.tableId = tableId;
this.summaries = summaries;
}
@Override
public boolean equals(Object o)
{
if (o == null || getClass() != o.getClass()) return false;
MutationSummary summary = (MutationSummary) o;
return tableId.equals(summary.tableId) && summaries.equals(summary.summaries);
}
@Override
public int hashCode()
{
return tableId.hashCode() + 31 * summaries.hashCode();
}
@Override
public String toString()
{
return "MutationSummary{tableId=" + tableId + ", summaries=" + summaries + '}';
}
public TableId tableId()
{
return tableId;
}
public byte[] digest()
{
Digest digest = Digest.forReadResponse();
digest.updateWithLong(tableId.asUUID().getMostSignificantBits());
digest.updateWithLong(tableId.asUUID().getLeastSignificantBits());
digest.updateWithInt(summaries.size());
for (CoordinatorSummary summary : summaries)
summary.digest(digest);
return digest.digest();
}
public boolean contains(ShortMutationId id)
{
CoordinatorSummary summary = coordinatorSummaryMap.get(id.logId());
return summary != null && summary.contains(id.offset());
}
public int unreconciledIds()
{
int count = 0;
for (CoordinatorSummary summary : summaries)
count += summary.unreconciled.offsetCount();
return count;
}
public int size()
{
return summaries.size();
}
boolean isEmpty()
{
return size() == 0;
}
public CoordinatorSummary get(int i)
{
return summaries.get(i);
}
public CoordinatorSummary get(CoordinatorLogId logId)
{
return coordinatorSummaryMap.get(logId.asLong());
}
/**
* Finds all elements that are contained by {@code left} and not contained by {@code right}
*/
public static void difference(MutationSummary left, MutationSummary right, Collection<ShortMutationId> into)
{
int i = 0, j = 0, lsize = left.size(), rsize = right.size();
while (i < lsize && j < rsize)
{
CoordinatorSummary l = left.get(i);
CoordinatorSummary r = right.get(j);
int cmp = CoordinatorSummary.idComparator.compare(l, r);
if (cmp == 0)
{
CoordinatorSummary.difference(l, r, into);
++i;
++j;
}
else if (cmp < 0)
{
l.reconciled.collectIds(into);
l.unreconciled.collectIds(into);
++i;
}
else
{
++j;
}
}
while (i < lsize)
{
CoordinatorSummary l = left.get(i);
l.reconciled.collectIds(into);
l.unreconciled.collectIds(into);
++i;
}
}
public static final IVersionedSerializer<MutationSummary> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(MutationSummary summary, DataOutputPlus out, int version) throws IOException
{
summary.tableId.serialize(out);
out.writeInt(summary.summaries.size());
for (int i=0,mi=summary.summaries.size(); i<mi; i++)
CoordinatorSummary.serializer.serialize(summary.summaries.get(i), out, version);
}
@Override
public MutationSummary deserialize(DataInputPlus in, int version) throws IOException
{
TableId tableId = TableId.deserialize(in);
int size = in.readInt();
List<CoordinatorSummary> summaries = new ArrayList<>(size);
for (int i = 0; i < size; i++)
summaries.add(CoordinatorSummary.serializer.deserialize(in, version));
return new MutationSummary(tableId, summaries);
}
@Override
public long serializedSize(MutationSummary summary, int version)
{
long size = summary.tableId.serializedSize();
size += TypeSizes.sizeof(summary.summaries.size());
for (int i=0,mi=summary.summaries.size(); i<mi; i++)
size += CoordinatorSummary.serializer.serializedSize(summary.summaries.get(i), version);
return size;
}
};
}

View File

@ -0,0 +1,239 @@
/*
* 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.replication;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
import org.agrona.collections.IntArrayList;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.reads.tracked.TrackedLocalReads;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// TODO (expected): persistence (handle restarts)
// TODO (expected): handle topology changes
public class MutationTrackingService
{
private static final Logger logger = LoggerFactory.getLogger(MutationTrackingService.class);
public static final MutationTrackingService instance = new MutationTrackingService();
private final TrackedLocalReads localReads = new TrackedLocalReads();
private final ConcurrentHashMap<String, KeyspaceShards> shards = new ConcurrentHashMap<>();
private volatile boolean started = false;
private MutationTrackingService() {}
// TODO (expected): implement a TCM ChangeListener
public synchronized void start(ClusterMetadata metadata)
{
if (started)
return;
logger.info("Starting replication tracking service");
for (KeyspaceMetadata keyspace : metadata.schema.getKeyspaces())
if (keyspace.useMutationTracking())
shards.put(keyspace.name, KeyspaceShards.make(keyspace, metadata, this::nextHostLogId));
started = true;
}
public synchronized boolean isStarted()
{
return started;
}
public void shutdownBlocking() throws InterruptedException
{
localReads.shutdownBlocking();
}
public TrackedLocalReads localReads()
{
return localReads;
}
public MutationId nextMutationId(String keyspace, Token token)
{
MutationId id = getOrCreate(keyspace).nextMutationId(token);
logger.trace("Created new mutation id {}", id);
return id;
}
public void witnessedRemoteMutation(String keyspace, Token token, MutationId mutationId, InetAddressAndPort onHost)
{
getOrCreate(keyspace).witnessedRemoteMutation(token, mutationId, onHost);
}
public void startWriting(Mutation mutation)
{
getOrCreate(mutation.getKeyspaceName()).startWriting(mutation);
}
public void finishWriting(Mutation mutation)
{
getOrCreate(mutation.getKeyspaceName()).finishWriting(mutation);
}
public MutationSummary createSummaryForKey(DecoratedKey key, TableId tableId, boolean includePending)
{
return getOrCreate(tableId).createSummaryForKey(key, tableId, includePending);
}
public MutationSummary createSummaryForRange(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending)
{
return getOrCreate(tableId).createSummaryForRange(range, tableId, includePending);
}
public MutationSummary createSummaryForRange(Range<Token> range, TableId tableId, boolean includePending)
{
return createSummaryForRange(Range.makeRowRange(range), tableId, includePending);
}
private KeyspaceShards getOrCreate(TableId tableId)
{
//noinspection DataFlowIssue
return getOrCreate(Schema.instance.getTableMetadata(tableId).keyspace);
}
private KeyspaceShards getOrCreate(String keyspace)
{
KeyspaceShards ks = shards.get(keyspace);
if (ks != null)
return ks;
ClusterMetadata csm = ClusterMetadata.current();
KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace);
return shards.computeIfAbsent(keyspace, ignore -> KeyspaceShards.make(ksm, csm, this::nextHostLogId));
}
// TODO (expected): durability
int nextHostLogId()
{
return nextHostLogId.incrementAndGet();
}
private final AtomicInteger nextHostLogId = new AtomicInteger();
private static class KeyspaceShards
{
private final String keyspace;
private final Map<Range<Token>, Shard> shards;
private transient final Map<Range<PartitionPosition>, Shard> ppShards;
static KeyspaceShards make(KeyspaceMetadata keyspace, ClusterMetadata cluster, IntSupplier logIdProvider)
{
Map<Range<Token>, Shard> shards = new HashMap<>();
cluster.placements.get(keyspace.params.replication).writes.forEach((tokenRange, forRange) -> {
IntArrayList participants = new IntArrayList(forRange.size(), IntArrayList.DEFAULT_NULL_VALUE);
for (InetAddressAndPort endpoint : forRange.endpoints())
participants.add(cluster.directory.peerId(endpoint).id());
Shard shard = new Shard(keyspace.name, tokenRange, cluster.myNodeId().id(), new Participants(participants), forRange.lastModified(), logIdProvider);
shards.put(tokenRange, shard);
});
return new KeyspaceShards(keyspace.name, shards);
}
KeyspaceShards(String keyspace, Map<Range<Token>, Shard> shards)
{
this.keyspace = keyspace;
this.shards = shards;
this.ppShards = new HashMap<>();
shards.forEach((range, shard) -> ppShards.put(Range.makeRowRange(range), shard));
}
MutationId nextMutationId(Token token)
{
return lookUp(token).nextId();
}
void witnessedRemoteMutation(Token token, MutationId mutationId, InetAddressAndPort onHost)
{
lookUp(token).witnessedRemoteMutation(mutationId, onHost);
}
void startWriting(Mutation mutation)
{
lookUp(mutation).startWriting(mutation);
}
void finishWriting(Mutation mutation)
{
lookUp(mutation).finishWriting(mutation);
}
MutationSummary createSummaryForKey(DecoratedKey key, TableId tableId, boolean includePending)
{
MutationSummary.Builder builder = new MutationSummary.Builder(tableId);
lookUp(key.getToken()).addSummaryForKey(key.getToken(), includePending, builder);
return builder.build();
}
MutationSummary createSummaryForRange(AbstractBounds<PartitionPosition> range, TableId tableId, boolean includePending)
{
MutationSummary.Builder builder = new MutationSummary.Builder(tableId);
forEachIntersectingShard(range, shard -> shard.addSummaryForRange(range, includePending, builder));
return builder.build();
}
private void forEachIntersectingShard(AbstractBounds<PartitionPosition> bounds, Consumer<Shard> consumer)
{
ppShards.forEach((range, shard) -> {
// TODO (expected): partial workaround - is there a better way to do this?
// SELECT * statements create Bounds[min,min], (PartitionKeyRestrictions.java:L174) not Range(min,min],
// which Ranges generally won't intersect with (Range.java:L148), so contains is used here to make it work
if (bounds.contains(range.right) || range.intersects(bounds))
consumer.accept(shard);
});
}
Shard lookUp(Mutation mutation)
{
return lookUp(mutation.key());
}
Shard lookUp(DecoratedKey key)
{
return lookUp(key.getToken());
}
Shard lookUp(Token token)
{
ClusterMetadata csm = ClusterMetadata.current();
KeyspaceMetadata ksm = csm.schema.getKeyspaceMetadata(keyspace);
Range<Token> range = ClusterMetadata.current().placements.get(ksm.params.replication).writes.forRange(token).range();
return shards.get(range);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
/*
* 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.replication;
import java.util.Arrays;
import java.util.Collection;
public class Participants
{
private final int[] hosts;
Participants(Collection<Integer> participants)
{
int i = 0;
int[] hosts = new int[participants.size()];
for (int host : participants) hosts[i++] = host;
Arrays.sort(hosts);
this.hosts = hosts;
}
int size()
{
return hosts.length;
}
int indexOf(int hostId)
{
int idx = Arrays.binarySearch(hosts, hostId);
if (idx < 0)
throw new IllegalArgumentException("Unknown host id " + hostId);
return idx;
}
int get(int idx)
{
if (idx < 0 || idx >= hosts.length)
throw new IllegalArgumentException("Out of bounds host idx " + idx);
return hosts[idx];
}
}

View File

@ -0,0 +1,210 @@
/*
* 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.replication;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.replication.MutationSummary.CoordinatorSummary;
public class ReconciliationPlan
{
private final ImmutableMap<InetAddressAndPort, Log2OffsetsMap.Immutable> txPlan;
public ReconciliationPlan(ImmutableMap<InetAddressAndPort, Log2OffsetsMap.Immutable> txPlan)
{
this.txPlan = txPlan;
}
public Set<InetAddressAndPort> nodes()
{
return txPlan.keySet();
}
public Log2OffsetsMap.Immutable peerReconciliation(InetAddressAndPort to)
{
return txPlan.get(to);
}
public Log2OffsetsMap.Immutable offsetsFor(InetAddressAndPort node)
{
return txPlan.get(node);
}
public boolean isEmpty()
{
return txPlan.isEmpty();
}
private static class PlanBuilder
{
final InetAddressAndPort node;
final MutationSummary summary;
final Map<InetAddressAndPort, Log2OffsetsMap.Immutable.Builder> peerReconciliations = new HashMap<>();
public PlanBuilder(InetAddressAndPort node, MutationSummary summary)
{
this.node = node;
this.summary = summary;
}
public void send(InetAddressAndPort to, Offsets sequenceIds)
{
peerReconciliations.computeIfAbsent(to, ep -> new Log2OffsetsMap.Immutable.Builder()).add(sequenceIds);
}
ReconciliationPlan build()
{
ImmutableMap.Builder<InetAddressAndPort, Log2OffsetsMap.Immutable> builder = ImmutableMap.builder();
peerReconciliations.forEach((to, ids) -> builder.put(to, ids.build()));
return new ReconciliationPlan(builder.build());
}
}
// TODO (desired): rework Offsets set logic usage to use Offsets#RangeIterator instead of rematerializing sets
private static class CoordinatorLogReconciliation
{
final CoordinatorLogId logId;
Offsets.Immutable reconciled;
Offsets.Immutable unreconciled;
Map<InetAddressAndPort, Offsets> unreconciledNodes = new HashMap<>();
CoordinatorLogReconciliation(CoordinatorLogId logId)
{
this.logId = logId;
}
void addPeerSummary(InetAddressAndPort peer, CoordinatorSummary summary)
{
Preconditions.checkArgument(summary.logId().equals(logId));
reconciled = Offsets.Immutable.union(reconciled, summary.reconciled);
unreconciled = Offsets.Immutable.union(unreconciled, summary.unreconciled);
unreconciledNodes.put(peer, summary.unreconciled);
}
void createPlan(Map<InetAddressAndPort, PlanBuilder> plan)
{
// remove reconciled ids
Offsets.Immutable allIds = Offsets.Immutable.difference(unreconciled, reconciled);
for (InetAddressAndPort receiver : plan.keySet())
{
Offsets.Immutable missing = Offsets.Immutable.difference(allIds, unreconciledNodes.get(receiver));
if (missing.isEmpty())
continue;
// TODO: look into more intelligent ways to distribute mutation requests
for (Map.Entry<InetAddressAndPort, Offsets> sender : unreconciledNodes.entrySet())
{
if (sender.getKey().equals(receiver))
continue;
Offsets senderIds = sender.getValue();
PlanBuilder senderPlan = plan.get(sender.getKey());
Offsets.Immutable requestedIds = Offsets.Immutable.intersection(missing, senderIds);
senderPlan.send(receiver, requestedIds);
missing = Offsets.Immutable.difference(missing, requestedIds);
if (missing.rangeCount() == 0)
break;
}
}
}
}
public static Map<InetAddressAndPort, ReconciliationPlan> calculateReconciliation(Map<InetAddressAndPort, MutationSummary> summaries)
{
Map<InetAddressAndPort, PlanBuilder> planBuilders = new HashMap<>();
Map<CoordinatorLogId, CoordinatorLogReconciliation> coordinatorReconciliations = new HashMap<>();
// organize data by peer and log id
summaries.forEach((node, summary) -> {
planBuilders.put(node, new PlanBuilder(node, summary));
for (int i=0; i<summary.size(); i++)
{
CoordinatorSummary coordinatorSummary = summary.get(i);
CoordinatorLogReconciliation reconciliation = coordinatorReconciliations.computeIfAbsent(coordinatorSummary.logId(), CoordinatorLogReconciliation::new);
reconciliation.addPeerSummary(node, coordinatorSummary);
}
});
coordinatorReconciliations.values().forEach(planBuilder -> planBuilder.createPlan(planBuilders));
Map<InetAddressAndPort, ReconciliationPlan> plans = new HashMap<>();
planBuilders.forEach((node, planBuilder) -> {
ReconciliationPlan plan = planBuilder.build();
if (!plan.isEmpty())
plans.put(node, plan);
});
return plans;
}
public static final IVersionedSerializer<ReconciliationPlan> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(ReconciliationPlan plan, DataOutputPlus out, int version) throws IOException
{
out.writeInt(plan.txPlan.size());
for (Map.Entry<InetAddressAndPort, Log2OffsetsMap.Immutable> entry : plan.txPlan.entrySet())
{
InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(entry.getKey(), out, version);
Log2OffsetsMap.Immutable.serializer.serialize(entry.getValue(), out, version);
}
}
@Override
public ReconciliationPlan deserialize(DataInputPlus in, int version) throws IOException
{
int size = in.readInt();
ImmutableMap.Builder<InetAddressAndPort, Log2OffsetsMap.Immutable> builder = ImmutableMap.builderWithExpectedSize(size);
for (int i = 0; i < size; i++)
{
InetAddressAndPort endpoint = InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version);
Log2OffsetsMap.Immutable offsets = Log2OffsetsMap.Immutable.serializer.deserialize(in, version);
builder.put(endpoint, offsets);
}
return new ReconciliationPlan(builder.build());
}
@Override
public long serializedSize(ReconciliationPlan plan, int version)
{
long size = TypeSizes.sizeof(plan.txPlan.size());
for (Map.Entry<InetAddressAndPort, Log2OffsetsMap.Immutable> entry : plan.txPlan.entrySet())
{
size += InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(entry.getKey(), version);
size += Log2OffsetsMap.Immutable.serializer.serializedSize(entry.getValue(), version);
}
return size;
}
};
}

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.replication;
import java.util.function.IntSupplier;
import com.google.common.base.Preconditions;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.replication.CoordinatorLog.CoordinatorLogPrimary;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.jctools.maps.NonBlockingHashMapLong;
public class Shard
{
private final String keyspace;
private final Range<Token> tokenRange;
private final int localHostId;
private final Participants participants;
private final Epoch sinceEpoch;
private final NonBlockingHashMapLong<CoordinatorLog> logs;
// TODO (expected): add support for log rotation
private final CoordinatorLogPrimary currentLocalLog;
Shard(String keyspace, Range<Token> tokenRange, int localHostId, Participants participants, Epoch sinceEpoch, IntSupplier logIdProvider)
{
this.keyspace = keyspace;
this.tokenRange = tokenRange;
this.localHostId = localHostId;
this.participants = participants;
this.sinceEpoch = sinceEpoch;
this.logs = new NonBlockingHashMapLong<>();
this.currentLocalLog = startNewLog(localHostId, logIdProvider.getAsInt(), participants);
logs.put(currentLocalLog.logId.asLong(), currentLocalLog);
}
MutationId nextId()
{
return currentLocalLog.nextId();
}
void witnessedRemoteMutation(MutationId mutationId, InetAddressAndPort onHost)
{
int onHostId = ClusterMetadata.current().directory.peerId(onHost).id();
get(mutationId).witnessedRemoteMutation(mutationId, onHostId);
}
void startWriting(Mutation mutation)
{
get(mutation.id()).startWriting(mutation);
}
void finishWriting(Mutation mutation)
{
get(mutation.id()).finishWriting(mutation);
}
void addSummaryForKey(Token token, boolean includePending, MutationSummary.Builder builder)
{
logs.forEach((id, log) -> {
MutationSummary.CoordinatorSummary.Builder summaryBuilder = builder.builderForLog(log.logId);
log.collectOffsetsFor(token, builder.tableId, includePending, summaryBuilder.unreconciled, summaryBuilder.reconciled);
});
}
void addSummaryForRange(AbstractBounds<PartitionPosition> range, boolean includePending, MutationSummary.Builder builder)
{
logs.forEach((id, log) -> {
MutationSummary.CoordinatorSummary.Builder summaryBuilder = builder.builderForLog(log.logId);
log.collectOffsetsFor(range, builder.tableId, includePending, summaryBuilder.unreconciled, summaryBuilder.reconciled);
});
}
/**
* Creates a new coordinator log for this host. Primarily on Shard init (node startup or topology change).
* Also on keyspace creation.
*/
private static CoordinatorLog.CoordinatorLogPrimary startNewLog(int localHostId, int hostLogId, Participants participants)
{
CoordinatorLogId logId = new CoordinatorLogId(localHostId, hostLogId);
return new CoordinatorLog.CoordinatorLogPrimary(localHostId, logId, participants);
}
private CoordinatorLog get(MutationId mutationId)
{
Preconditions.checkArgument(!mutationId.isNone());
return get(mutationId.logId());
}
private CoordinatorLog get(long logId)
{
CoordinatorLog log = logs.get(logId);
return log != null
? log : logs.computeIfAbsent(logId, ignore -> CoordinatorLog.create(localHostId, new CoordinatorLogId(logId), participants));
}
}

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.replication;
import java.io.IOException;
import java.util.Comparator;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
/**
* MutationId without the timestamp component. This is sufficient for uniquely identifying a mutation,
* and for lookup in the journal and most tracking data structures.
*/
public class ShortMutationId extends CoordinatorLogId
{
/**
* 4 byte offset. Offest is incremented, is alone is sufficient to identify
* the entry within a coordinator log.
* MutationId adds a timestamp for correlation purposes.
*/
protected final int offset;
public ShortMutationId(long logId, int offset)
{
super(logId);
this.offset = offset;
}
public ShortMutationId(CoordinatorLogId logId, int offset)
{
super(logId.hostId(), logId.hostLogId());
this.offset = offset;
}
public ShortMutationId(MutationId mutationId)
{
this(mutationId.logId(), mutationId.offset());
}
public long logId()
{
return super.asLong();
}
public int offset()
{
return offset;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (!(o instanceof ShortMutationId)) return false;
ShortMutationId that = (ShortMutationId) o;
return this.logId() == that.logId() && this.offset == that.offset;
}
@Override
public int hashCode()
{
return Integer.hashCode(offset) + 31 * super.hashCode();
}
@Override
public String toString()
{
return "ShortMutationId{" + hostId() + ", " + hostLogId() + ", " + offset() + '}';
}
public static final Comparator<ShortMutationId> comparator = (l, r) -> {
int cmp = CoordinatorLogId.comparator.compare(l, r);
return cmp != 0 ? cmp : Integer.compare(l.offset, r.offset);
};
public static final IVersionedSerializer<ShortMutationId> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(ShortMutationId id, DataOutputPlus out, int version) throws IOException
{
out.writeLong(id.logId());
out.writeInt(id.offset());
}
@Override
public ShortMutationId deserialize(DataInputPlus in, int version) throws IOException
{
long logId = in.readLong();
int offset = in.readInt();
return new ShortMutationId(logId, offset);
}
@Override
public long serializedSize(ShortMutationId id, int version)
{
return TypeSizes.sizeof(id.logId()) + TypeSizes.sizeof(id.offset());
}
};
}

View File

@ -0,0 +1,287 @@
/*
* 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.replication;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.DebuggableTask;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.locator.DynamicEndpointSnitch;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.locator.ReplicaPlans;
import org.apache.cassandra.net.ForwardingInfo;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.service.TrackedWriteResponseHandler;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.MonotonicClock;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.writeMetrics;
import static org.apache.cassandra.net.Verb.MUTATION_REQ;
public class TrackedWriteRequest
{
private static final Logger logger = LoggerFactory.getLogger(TrackedWriteRequest.class);
/**
* Coordinate write of a tracked mutation. Assumes the replica is a coordinator.
*
* @param mutation the mutation to be applied
* @param consistencyLevel the consistency level for the write operation
* @param requestTime object holding times when request got enqueued and started execution
*/
public static TrackedWriteResponseHandler perform(
Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
Tracing.trace("Determining replicas for mutation");
String keyspaceName = mutation.getKeyspaceName();
Keyspace keyspace = Keyspace.open(keyspaceName);
Token token = mutation.key().getToken();
MutationId id = MutationTrackingService.instance.nextMutationId(keyspaceName, token);
mutation = mutation.withMutationId(id);
ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(keyspace, consistencyLevel, token, ReplicaPlans.writeNormal);
if (plan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null)
writeMetrics.localRequests.mark();
else
writeMetrics.remoteRequests.mark();
AbstractReplicationStrategy rs = plan.replicationStrategy();
TrackedWriteResponseHandler handler =
TrackedWriteResponseHandler.wrap(rs.getWriteResponseHandler(plan, null, WriteType.SIMPLE, null, requestTime),
keyspaceName,
mutation.key().getToken(),
id);
applyLocallyAndSendToReplicas(mutation, plan, handler);
return handler;
}
public static void applyLocallyAndSendToReplicas(Mutation mutation, ReplicaPlan.ForWrite plan, TrackedWriteResponseHandler handler)
{
String localDataCenter = DatabaseDescriptor.getLocator().local().datacenter;
boolean applyLocally = false;
// this DC replicas
List<Replica> localDCReplicas = null;
// extra-DC, grouped by DC
Map<String, List<Replica>> remoteDCReplicas = null;
// only need to create a Message for non-local writes
Message<Mutation> message = null;
// For performance, Mutation caches serialized buffers that are computed lazily in serializedBuffer(). That
// computation is not synchronized however, and we will potentially call that method concurrently for each
// dispatched message (not that concurrent calls to serializedBuffer() are "unsafe" per se, just that they
// may result in multiple computations, making the caching optimization moot). So forcing the serialization
// here to make sure it's already cached/computed when it's concurrently used later.
// Side note: we have one cached buffers for each used EncodingVersion and this only pre-compute the one for
// the current version, but it's just an optimization, and we're ok not optimizing for mixed-version clusters.
Mutation.serializer.prepareSerializedBuffer(mutation, MessagingService.current_version);
for (Replica destination : plan.contacts())
{
if (!plan.isAlive(destination))
{
handler.expired(); // immediately mark the response as expired since the request will not be sent
continue;
}
if (destination.isSelf())
{
applyLocally = true;
continue;
}
if (message == null)
message = Message.outWithFlags(MUTATION_REQ, mutation, handler.getRequestTime(), singletonList(MessageFlag.CALL_BACK_ON_FAILURE));
String dc = DatabaseDescriptor.getLocator().location(destination.endpoint()).datacenter;
if (localDataCenter.equals(dc))
{
if (localDCReplicas == null)
localDCReplicas = new ArrayList<>(plan.contacts().size());
localDCReplicas.add(destination);
}
else
{
if (remoteDCReplicas == null)
remoteDCReplicas = new HashMap<>();
List<Replica> messages = remoteDCReplicas.get(dc);
if (messages == null)
messages = remoteDCReplicas.computeIfAbsent(dc, ignore -> new ArrayList<>(3)); // most DCs will have <= 3 replicas
messages.add(destination);
}
}
Preconditions.checkState(applyLocally); // the coordinator is always a replica
applyMutationLocally(mutation, handler);
if (localDCReplicas != null)
for (Replica destination : localDCReplicas)
MessagingService.instance().sendWriteWithCallback(message, destination, handler);
if (remoteDCReplicas != null)
{
// for each datacenter, send the message to one node to relay the write to other replicas
for (List<Replica> dcReplicas : remoteDCReplicas.values())
sendMessagesToRemoteDC(message, EndpointsForToken.copyOf(mutation.key().getToken(), dcReplicas), handler);
}
}
private static void applyMutationLocally(Mutation mutation, TrackedWriteResponseHandler handler)
{
Stage.MUTATION.maybeExecuteImmediately(new LocalMutationRunnable(mutation, handler));
}
private static class LocalMutationRunnable implements DebuggableTask.RunnableDebuggableTask
{
private final Mutation mutation;
private final TrackedWriteResponseHandler handler;
LocalMutationRunnable(Mutation mutation, TrackedWriteResponseHandler handler)
{
this.mutation = mutation;
this.handler = handler;
}
@Override
public final void run()
{
long now = MonotonicClock.Global.approxTime.now();
long deadline = handler.getRequestTime().computeDeadline(MUTATION_REQ.expiresAfterNanos());
if (now > deadline)
{
long timeTakenNanos = now - startTimeNanos();
MessagingService.instance().metrics.recordSelfDroppedMessage(Verb.MUTATION_REQ, timeTakenNanos, NANOSECONDS);
return;
}
try
{
mutation.apply();
handler.onResponse(null);
}
catch (Exception ex)
{
if (!(ex instanceof WriteTimeoutException))
logger.error("Failed to apply mutation locally : ", ex);
handler.onFailure(FBUtilities.getBroadcastAddressAndPort(), RequestFailureReason.forException(ex));
}
}
@Override
public long creationTimeNanos()
{
return handler.getRequestTime().enqueuedAtNanos();
}
@Override
public long startTimeNanos()
{
return handler.getRequestTime().startedAtNanos();
}
@Override
public String description()
{
// description is an Object and toString() called so we do not have to evaluate the Mutation.toString()
// unless expliclitly checked
return mutation.toString();
}
}
/*
* Send the message to the first replica of targets, and have it forward the message to others in its DC
*/
private static void sendMessagesToRemoteDC(Message<? extends IMutation> message,
EndpointsForToken targets,
TrackedWriteResponseHandler handler)
{
final Replica target;
if (targets.size() > 1)
{
target = pickReplica(targets);
EndpointsForToken forwardToReplicas = targets.filter(r -> r != target, targets.size());
for (Replica replica : forwardToReplicas)
{
MessagingService.instance().callbacks.addWithExpiration(handler, message, replica);
logger.trace("Adding FWD message to {}@{}", message.id(), replica);
}
// starting with 4.0, use the same message id for all replicas
long[] messageIds = new long[forwardToReplicas.size()];
Arrays.fill(messageIds, message.id());
message = message.withForwardTo(new ForwardingInfo(forwardToReplicas.endpointList(), messageIds));
}
else
{
target = targets.get(0);
}
Tracing.trace("Sending mutation to remote replica {}", target);
MessagingService.instance().sendWriteWithCallback(message, target, handler);
logger.trace("Sending message to {}@{}", message.id(), target);
}
private static Replica pickReplica(EndpointsForToken targets)
{
EndpointsForToken healthy = targets.filter(r -> DynamicEndpointSnitch.getSeverity(r.endpoint()) == 0);
EndpointsForToken select = healthy.isEmpty() ? targets : healthy;
return select.get(ThreadLocalRandom.current().nextInt(0, select.size()));
}
}

View File

@ -245,7 +245,7 @@ public final class DistributedMetadataLogKeyspace
public static KeyspaceMetadata initialMetadata(Set<String> knownDatacenters)
{
return KeyspaceMetadata.create(SchemaConstants.METADATA_KEYSPACE_NAME, new KeyspaceParams(true, ReplicationParams.simpleMeta(1, knownDatacenters), FastPathStrategy.simple()), Tables.of(Log));
return KeyspaceMetadata.create(SchemaConstants.METADATA_KEYSPACE_NAME, new KeyspaceParams(true, ReplicationParams.simpleMeta(1, knownDatacenters), FastPathStrategy.simple(), ReplicationType.untracked), Tables.of(Log));
}
public static KeyspaceMetadata initialMetadata(String datacenter)

View File

@ -120,27 +120,27 @@ public final class KeyspaceMetadata implements SchemaElement
public static KeyspaceMetadata create(String name, KeyspaceParams params, Tables tables)
{
return new KeyspaceMetadata(name, Kind.REGULAR, params, tables, Views.none(), Types.none(), UserFunctions.none());
return new KeyspaceMetadata(name, Kind.REGULAR, params, tables.withKeyspaceReplicationType(params.replicationType), Views.none(), Types.none(), UserFunctions.none());
}
public static KeyspaceMetadata create(String name, KeyspaceParams params, Tables tables, Views views, Types types, UserFunctions functions)
{
return new KeyspaceMetadata(name, Kind.REGULAR, params, tables, views, types, functions);
return new KeyspaceMetadata(name, Kind.REGULAR, params, tables.withKeyspaceReplicationType(params.replicationType), views, types, functions);
}
public static KeyspaceMetadata virtual(String name, Tables tables)
{
return new KeyspaceMetadata(name, Kind.VIRTUAL, KeyspaceParams.local(), tables, Views.none(), Types.none(), UserFunctions.none());
return new KeyspaceMetadata(name, Kind.VIRTUAL, KeyspaceParams.local(), tables.withKeyspaceReplicationType(ReplicationType.untracked), Views.none(), Types.none(), UserFunctions.none());
}
public KeyspaceMetadata withSwapped(KeyspaceParams params)
{
return new KeyspaceMetadata(name, kind, params, tables, views, types, userFunctions);
return new KeyspaceMetadata(name, kind, params, tables.withKeyspaceReplicationType(params.replicationType), views, types, userFunctions);
}
public KeyspaceMetadata withSwapped(Tables regular)
{
return new KeyspaceMetadata(name, kind, params, regular, views, types, userFunctions);
return new KeyspaceMetadata(name, kind, params, regular.withKeyspaceReplicationType(params.replicationType), views, types, userFunctions);
}
public KeyspaceMetadata withSwapped(Views views)
@ -290,6 +290,11 @@ public final class KeyspaceMetadata implements SchemaElement
return Optional.empty();
}
public boolean useMutationTracking()
{
return params.replicationType.isTracked();
}
@Override
public int hashCode()
{
@ -384,7 +389,9 @@ public final class KeyspaceMetadata implements SchemaElement
params.replication.appendCqlTo(builder);
builder.append(" AND durable_writes = ")
.append(params.durableWrites);
.append(params.durableWrites)
.append(" AND replication_type = ")
.appendWithSingleQuotes(params.replicationType.name());
if (params.fastPath != null)
{

View File

@ -55,11 +55,14 @@ public final class KeyspaceParams
@VisibleForTesting
public static boolean DEFAULT_LOCAL_DURABLE_WRITES = true;
public static final ReplicationType DEFAULT_REPLICATION_TYPE = ReplicationType.untracked;
public enum Option
{
DURABLE_WRITES,
REPLICATION,
FAST_PATH;
FAST_PATH,
REPLICATION_TYPE;
@Override
public String toString()
@ -73,74 +76,97 @@ public final class KeyspaceParams
public final FastPathStrategy fastPath;
public final String comment;
public final String securityLabel;
public final ReplicationType replicationType;
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath)
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, ReplicationType replicationType)
{
this(durableWrites, replication, fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
this(durableWrites, replication, fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, replicationType);
}
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, String comment, String securityLabel)
public KeyspaceParams(boolean durableWrites, ReplicationParams replication, FastPathStrategy fastPath, String comment, String securityLabel, ReplicationType replicationType)
{
this.durableWrites = durableWrites;
this.replication = replication;
this.fastPath = fastPath;
this.comment = comment == null ? EMPTY_COMMENT : comment;
this.securityLabel = securityLabel == null ? EMPTY_SECURITY_LABEL : securityLabel;
this.replicationType = replicationType;
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, FastPathStrategy fastPath)
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, FastPathStrategy fastPath, ReplicationType replicationType)
{
return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return new KeyspaceParams(durableWrites, ReplicationParams.fromMap(replication), fastPath, EMPTY_COMMENT, EMPTY_SECURITY_LABEL, replicationType);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, ReplicationType replicationType)
{
return create(durableWrites, replication, FastPathStrategy.simple(), replicationType);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, Map<String, String> fastPath)
{
return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath));
return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath), ReplicationType.untracked);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication, Map<String, String> fastPath, ReplicationType replicationType)
{
return create(durableWrites, replication, FastPathStrategy.fromMap(fastPath), replicationType);
}
public static KeyspaceParams create(boolean durableWrites, Map<String, String> replication)
{
return create(durableWrites, replication, FastPathStrategy.simple());
return create(durableWrites, replication, FastPathStrategy.simple(), ReplicationType.untracked);
}
public static KeyspaceParams local()
{
return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return new KeyspaceParams(DEFAULT_LOCAL_DURABLE_WRITES, ReplicationParams.local(), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams simple(int replicationFactor, ReplicationType replicationType)
{
return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), replicationType);
}
public static KeyspaceParams simple(int replicationFactor)
{
return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams simple(String replicationFactor)
{
return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return new KeyspaceParams(true, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams simpleTransient(int replicationFactor)
{
return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return new KeyspaceParams(false, ReplicationParams.simple(replicationFactor), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL, ReplicationType.untracked);
}
public static KeyspaceParams nts(ReplicationType replicationType, Object... args)
{
return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple(), replicationType);
}
public static KeyspaceParams nts(Object... args)
{
return new KeyspaceParams(true, ReplicationParams.nts(args), FastPathStrategy.simple(), EMPTY_COMMENT, EMPTY_SECURITY_LABEL);
return nts(ReplicationType.untracked, args);
}
public KeyspaceParams withSwapped(ReplicationParams params)
{
return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel);
return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel, replicationType);
}
public KeyspaceParams withComment(String comment)
{
return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
public KeyspaceParams withSecurityLabel(String securityLabel)
{
return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel);
return new KeyspaceParams(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
public void validate(String name, ClientState state, ClusterMetadata metadata)
@ -163,13 +189,14 @@ public final class KeyspaceParams
&& replication.equals(p.replication)
&& fastPath.equals(p.fastPath)
&& comment.equals(p.comment)
&& securityLabel.equals(p.securityLabel);
&& securityLabel.equals(p.securityLabel)
&& replicationType == p.replicationType;
}
@Override
public int hashCode()
{
return Objects.hashCode(durableWrites, replication, fastPath, comment, securityLabel);
return Objects.hashCode(durableWrites, replication, fastPath, comment, securityLabel, replicationType);
}
@Override
@ -181,6 +208,7 @@ public final class KeyspaceParams
.add(Option.FAST_PATH.toString(), fastPath.toString())
.add("COMMENT", comment)
.add("SECURITY_LABEL", securityLabel)
.add(Option.REPLICATION_TYPE.toString(), replicationType)
.toString();
}
@ -188,6 +216,7 @@ public final class KeyspaceParams
{
public void serialize(KeyspaceParams t, DataOutputPlus out, Version version) throws IOException
{
ReplicationType.serializer.serialize(t.replicationType, out, version);
ReplicationParams.serializer.serialize(t.replication, out, version);
out.writeBoolean(t.durableWrites);
if (version.isAtLeast(MIN_ACCORD_VERSION))
@ -201,6 +230,7 @@ public final class KeyspaceParams
public KeyspaceParams deserialize(DataInputPlus in, Version version) throws IOException
{
ReplicationType rtype = ReplicationType.serializer.deserialize(in, version);
ReplicationParams params = ReplicationParams.serializer.deserialize(in, version);
boolean durableWrites = in.readBoolean();
FastPathStrategy fastPath = version.isAtLeast(MIN_ACCORD_VERSION)
@ -213,12 +243,13 @@ public final class KeyspaceParams
comment = in.readUTF();
securityLabel = in.readUTF();
}
return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel);
return new KeyspaceParams(durableWrites, params, fastPath, comment, securityLabel, rtype);
}
public long serializedSize(KeyspaceParams t, Version version)
{
return ReplicationParams.serializer.serializedSize(t.replication, version) +
return ReplicationType.serializer.serializedSize(t.replicationType, version) +
ReplicationParams.serializer.serializedSize(t.replication, version) +
TypeSizes.sizeof(t.durableWrites) +
(version.isAtLeast(Version.V8) ? TypeSizes.sizeof(t.comment) : 0) +
(version.isAtLeast(Version.V8) ? TypeSizes.sizeof(t.securityLabel) : 0) +

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.schema;
import java.io.IOException;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.tcm.serialization.MetadataSerializer;
import org.apache.cassandra.tcm.serialization.Version;
public enum ReplicationType
{
untracked, tracked;
public static final MetadataSerializer<ReplicationType> serializer = new MetadataSerializer<>()
{
@Override
public void serialize(ReplicationType t, DataOutputPlus out, Version version) throws IOException
{
if (version.isBefore(Version.V9))
return;
switch (t)
{
case untracked:
out.writeByte(0);
break;
case tracked:
out.writeByte(1);
break;
default:
throw new IllegalArgumentException("Unsupported replication type: " + t);
}
}
@Override
public ReplicationType deserialize(DataInputPlus in, Version version) throws IOException
{
if (version.isBefore(Version.V9))
return untracked;
byte t = in.readByte();
switch (t)
{
case 0:
return untracked;
case 1:
return tracked;
default:
throw new IllegalArgumentException("Unsupported replication type: " + t);
}
}
@Override
public long serializedSize(ReplicationType t, Version version)
{
if (version.isBefore(Version.V9))
return 0;
return TypeSizes.BYTE_SIZE;
}
};
public boolean isTracked()
{
return this == tracked;
}
// FIXME: used in lieu of adding support for tracked reads in parameterized tests, fix usages of this method
public static ReplicationType[] fixmeValues()
{
return new ReplicationType[]{ untracked };
}
}

View File

@ -82,6 +82,7 @@ import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.RowIterators;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.ColumnMetadata.ClusteringOrder;
import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
import org.apache.cassandra.service.accord.fastpath.FastPathStrategy;
@ -145,6 +146,7 @@ public final class SchemaKeyspace
"CREATE TABLE %s ("
+ "keyspace_name text,"
+ "durable_writes boolean,"
+ "replication_type text,"
+ "replication frozen<map<text, text>>,"
+ "fast_path frozen<map<text, text>>,"
+ "PRIMARY KEY ((keyspace_name)))");
@ -488,7 +490,7 @@ public final class SchemaKeyspace
continue;
DecoratedKey key = partition.partitionKey();
Mutation.PartitionUpdateCollector puCollector = mutationMap.computeIfAbsent(key, k -> new Mutation.PartitionUpdateCollector(SchemaConstants.SCHEMA_KEYSPACE_NAME, key));
Mutation.PartitionUpdateCollector puCollector = mutationMap.computeIfAbsent(key, k -> new Mutation.PartitionUpdateCollector(MutationId.none(), SchemaConstants.SCHEMA_KEYSPACE_NAME, key));
puCollector.add(makeUpdateForSchema(partition, cmd.columnFilter()).withOnlyPresentColumns());
}
}
@ -537,12 +539,13 @@ public final class SchemaKeyspace
private static Mutation.SimpleBuilder makeCreateKeyspaceMutation(String name, KeyspaceParams params, long timestamp)
{
Mutation.SimpleBuilder builder = Mutation.simpleBuilder(Keyspaces.keyspace, decorate(Keyspaces, name))
Mutation.SimpleBuilder builder = Mutation.simpleBuilder(MutationId.none(), Keyspaces.keyspace, decorate(Keyspaces, name))
.timestamp(timestamp);
builder.update(Keyspaces)
.row()
.add(KeyspaceParams.Option.DURABLE_WRITES.toString(), params.durableWrites)
.add(KeyspaceParams.Option.REPLICATION_TYPE.toString(), params.replicationType.toString())
.add(KeyspaceParams.Option.REPLICATION.toString(),
(params.replication.isMeta() ? params.replication.asNonMeta() : params.replication).asMap())
.add(KeyspaceParams.Option.FAST_PATH.toString(), params.fastPath.asMap());
@ -566,7 +569,7 @@ public final class SchemaKeyspace
private static Mutation.SimpleBuilder makeDropKeyspaceMutation(KeyspaceMetadata keyspace, long timestamp)
{
Mutation.SimpleBuilder builder = Mutation.simpleBuilder(SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name))
Mutation.SimpleBuilder builder = Mutation.simpleBuilder(MutationId.none(), SchemaConstants.SCHEMA_KEYSPACE_NAME, decorate(Keyspaces, keyspace.name))
.timestamp(timestamp);
for (TableMetadata schemaTable : ALL_TABLE_METADATA)
@ -1030,12 +1033,24 @@ public final class SchemaKeyspace
UntypedResultSet.Row row = query(query, keyspaceName).one();
boolean durableWrites = row.getBoolean(KeyspaceParams.Option.DURABLE_WRITES.toString());
ReplicationType replicationType;
if (row.has(KeyspaceParams.Option.REPLICATION_TYPE.toString()))
{
String replicationTypeName = row.getString(KeyspaceParams.Option.REPLICATION_TYPE.toString());
replicationType = replicationTypeName != null ? ReplicationType.valueOf(replicationTypeName) : ReplicationType.untracked;
}
else
{
replicationType = ReplicationType.untracked;
}
Map<String, String> replication = row.getFrozenTextMap(KeyspaceParams.Option.REPLICATION.toString());
Map<String, String> fastPath = row.getFrozenTextMap(KeyspaceParams.Option.FAST_PATH.toString());
KeyspaceParams params = KeyspaceParams.create(durableWrites, replication, fastPath);
KeyspaceParams params = KeyspaceParams.create(durableWrites, replication, fastPath, replicationType);
if (keyspaceName.equals(SchemaConstants.METADATA_KEYSPACE_NAME))
params = new KeyspaceParams(params.durableWrites, params.replication.asMeta(), FastPathStrategy.simple());
params = new KeyspaceParams(params.durableWrites, params.replication.asMeta(), FastPathStrategy.simple(), ReplicationType.untracked);
return params;
}

View File

@ -185,6 +185,7 @@ public class TableMetadata implements SchemaElement
public final IPartitioner partitioner;
public final Kind kind;
public final TableParams params;
public final ReplicationType keyspaceReplicationType;
public final ImmutableSet<Flag> flags;
@Nullable
@ -233,6 +234,7 @@ public class TableMetadata implements SchemaElement
partitioner = builder.partitioner;
kind = builder.kind;
params = builder.params.build();
keyspaceReplicationType = builder.keyspaceReplicationType;
indexName = kind == Kind.INDEX ? name.substring(name.indexOf('.') + 1) : null;
@ -316,6 +318,7 @@ public class TableMetadata implements SchemaElement
.partitioner(partitioner)
.kind(kind)
.params(params)
.keyspaceReplicationType(keyspaceReplicationType)
.flags(flags)
.addColumns(columns())
.droppedColumns(droppedColumns)
@ -329,6 +332,11 @@ public class TableMetadata implements SchemaElement
return kind == Kind.INDEX;
}
public TableMetadata withKeyspaceReplicationType(ReplicationType type)
{
return unbuild().keyspaceReplicationType(type).build();
}
public TableMetadata withSwapped(TableParams params)
{
return unbuild().params(params).build();
@ -364,6 +372,13 @@ public class TableMetadata implements SchemaElement
return kind == Kind.VIRTUAL;
}
public ReplicationType replicationType()
{
if (kind != Kind.REGULAR)
return ReplicationType.untracked;
return keyspaceReplicationType;
}
public Optional<String> indexName()
{
return Optional.ofNullable(indexName);
@ -975,6 +990,7 @@ public class TableMetadata implements SchemaElement
private IPartitioner partitioner;
private Kind kind = Kind.REGULAR;
private TableParams.Builder params = TableParams.builder();
private ReplicationType keyspaceReplicationType = ReplicationType.untracked;
// See the comment on Flag.COMPOUND definition for why we (still) inconditionally add this flag.
private Set<Flag> flags = EnumSet.of(Flag.COMPOUND);
@ -1077,6 +1093,12 @@ public class TableMetadata implements SchemaElement
return new ColumnMetadata(prev.ksName, prev.cfName, prev.name, prev.type, uniqueId, prev.position(), prev.kind, prev.getMask(), prev.getColumnConstraints(), prev.comment, prev.securityLabel);
}
public Builder keyspaceReplicationType(ReplicationType type)
{
keyspaceReplicationType = type;
return this;
}
public Builder id(TableId val)
{
id = val;
@ -1102,6 +1124,8 @@ public class TableMetadata implements SchemaElement
public Builder kind(Kind val)
{
if (val != Kind.REGULAR)
keyspaceReplicationType = null;
kind = val;
return this;
}

View File

@ -192,6 +192,13 @@ public final class Tables implements Iterable<TableMetadata>
: this;
}
public Tables withKeyspaceReplicationType(ReplicationType type)
{
return any(this, t -> t.keyspaceReplicationType != type)
? builder().add(transform(this, t -> t.withKeyspaceReplicationType(type))).build()
: this;
}
MapDifference<String, TableMetadata> indexesDiff(Tables other)
{
Map<String, TableMetadata> thisIndexTables = new HashMap<>();

View File

@ -80,6 +80,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Locator;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
import org.apache.cassandra.metrics.DefaultNameFactory;
import org.apache.cassandra.net.StartupClusterConnectivityChecker;
@ -271,6 +272,7 @@ public class CassandraDaemon
Keyspace.setInitialized();
CommitLog.instance.start();
MutationJournal.instance.start();
SnapshotManager.instance.start(false);
SnapshotManager.instance.clearExpiredSnapshots();

View File

@ -134,6 +134,7 @@ import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.TrackedWriteRequest;
import org.apache.cassandra.schema.PartitionDenylist;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
@ -173,6 +174,7 @@ import org.apache.cassandra.service.reads.ReadCallback;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.range.RangeCommands;
import org.apache.cassandra.service.reads.repair.ReadRepair;
import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.membership.NodeState;
import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
@ -194,6 +196,7 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.primitives.Txn.Kind.Read;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
import static java.util.Collections.singleton;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
@ -961,6 +964,71 @@ public class StorageProxy implements StorageProxyMBean
});
}
/**
* Performs a coordinated write with mutation tracking.
* Assumes that local coordinator is a replica (forwarding implementation pending).
*
* @param mutation
* @param consistencyLevel
* @param requestTime
*/
public static void mutateWithTracking(Mutation mutation, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
try
{
TrackedWriteRequest.perform(mutation, consistencyLevel, requestTime).get();
}
catch (WriteTimeoutException|WriteFailureException ex)
{
if (consistencyLevel == ConsistencyLevel.ANY)
{
// TODO (expected): what exactly?
}
else
{
if (ex instanceof WriteFailureException)
{
writeMetrics.failures.mark();
writeMetricsForLevel(consistencyLevel).failures.mark();
WriteFailureException fe = (WriteFailureException)ex;
Tracing.trace("Write failure; received {} of {} required replies, failed {} requests",
fe.received, fe.blockFor, fe.failureReasonByEndpoint.size());
}
else
{
writeMetrics.timeouts.mark();
writeMetricsForLevel(consistencyLevel).timeouts.mark();
WriteTimeoutException te = (WriteTimeoutException)ex;
Tracing.trace("Write timeout; received {} of {} required replies", te.received, te.blockFor);
}
throw ex;
}
}
catch (UnavailableException e)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistencyLevel).unavailables.mark();
Tracing.trace("Unavailable");
throw e;
}
catch (OverloadedException e)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistencyLevel).unavailables.mark();
Tracing.trace("Overloaded");
throw e;
}
finally
{
// We track latency based on request processing time, since the amount of time that request spends in the queue
// is not a representative metric of replica performance.
long latency = nanoTime() - requestTime.startedAtNanos();
writeMetrics.addNano(latency);
writeMetricsForLevel(consistencyLevel).addNano(latency);
updateCoordinatorWriteLatencyTableMetric(singleton(mutation), latency);
}
}
/**
* Use this method to have these Mutations applied
* across all replicas. This method will take care
@ -1220,11 +1288,11 @@ public class StorageProxy implements StorageProxyMBean
}
@SuppressWarnings("unchecked")
public static void mutateWithTriggers(List<? extends IMutation> mutations,
ConsistencyLevel consistencyLevel,
boolean mutateAtomically,
Dispatcher.RequestTime requestTime,
PreserveTimestamp preserveTimestamps)
public static void mutateWithoutConditions(List<? extends IMutation> mutations,
ConsistencyLevel consistencyLevel,
boolean mutateAtomically,
Dispatcher.RequestTime requestTime,
PreserveTimestamp preserveTimestamps)
throws WriteTimeoutException, WriteFailureException, UnavailableException, OverloadedException, InvalidRequestException
{
if (DatabaseDescriptor.getPartitionDenylistEnabled() && DatabaseDescriptor.getDenylistWritesEnabled())
@ -1256,7 +1324,24 @@ public class StorageProxy implements StorageProxyMBean
long size = IMutation.dataSize(augmented != null ? augmented : mutations);
writeMetrics.mutationSize.update(size);
writeMetricsForLevel(consistencyLevel).mutationSize.update(size);
if (augmented != null || mutateAtomically || updatesView)
boolean isTracked = Schema.instance.getKeyspaceMetadata(mutations.get(0).getKeyspaceName()).params.replicationType.isTracked();
if (isTracked)
{
if (mutations.stream().anyMatch(m -> m instanceof CounterMutation))
throw new InvalidRequestException("Mutation tracking is currently unsupported with counters");
if (augmented != null)
throw new InvalidRequestException("Mutation tracking is currently unsupported with triggers");
if (mutateAtomically)
throw new InvalidRequestException("Mutation tracking is currently unsupported with logged batches");
if (mutations.size() > 1)
throw new InvalidRequestException("Mutation tracking is currently unsupported with unlogged batches");
if (updatesView)
throw new InvalidRequestException("Mutation tracking is currently unsupported with materialized views");
mutateWithTracking((Mutation) mutations.get(0), consistencyLevel, requestTime);
}
else if (augmented != null || mutateAtomically || updatesView)
mutateAtomically(augmented != null ? augmented : (List<Mutation>)mutations, consistencyLevel, updatesView, requestTime);
else
dispatchMutationsWithRetryOnDifferentSystem(mutations, consistencyLevel, requestTime, preserveTimestamps);
@ -2628,6 +2713,30 @@ public class StorageProxy implements StorageProxyMBean
};
}
private static PartitionIterator fetchRowsTracked(List<SinglePartitionReadCommand> commands,
ConsistencyLevel consistencyLevel,
Dispatcher.RequestTime requestTime)
{
int cmdCount = commands.size();
TrackedRead.Partition[] reads = new TrackedRead.Partition[cmdCount];
ClusterMetadata metadata = ClusterMetadata.current();
for (int i=0; i<cmdCount; i++)
reads[i] = TrackedRead.Partition.create(metadata, commands.get(i), consistencyLevel);
for (TrackedRead.Partition read : reads)
read.start(requestTime);
if (cmdCount == 1)
return reads[0].awaitResults();
List<PartitionIterator> iterators = new ArrayList<>(cmdCount);
for (TrackedRead.Partition read : reads)
iterators.add(read.awaitResults());
return PartitionIterators.concat(iterators);
}
/**
* This function executes local and remote reads, and blocks for the results:
*
@ -2642,10 +2751,10 @@ public class StorageProxy implements StorageProxyMBean
* This should not be called directly because it bypasses statistics and error handling. It is public
* so it can be used by Accord to fetch rows and the statistics will be tracked by Accord.
*/
public static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands,
ConsistencyLevel consistencyLevel,
ReadCoordinator coordinator,
Dispatcher.RequestTime requestTime)
public static PartitionIterator fetchRowsUntracked(List<SinglePartitionReadCommand> commands,
ConsistencyLevel consistencyLevel,
ReadCoordinator coordinator,
Dispatcher.RequestTime requestTime)
throws UnavailableException, ReadFailureException, ReadTimeoutException
{
int cmdCount = commands.size();
@ -2714,6 +2823,22 @@ public class StorageProxy implements StorageProxyMBean
return concatAndBlockOnRepair(results, repairs);
}
private static PartitionIterator fetchRows(List<SinglePartitionReadCommand> commands,
ConsistencyLevel consistencyLevel,
ReadCoordinator coordinator,
Dispatcher.RequestTime requestTime)
{
if (commands.get(0).metadata().replicationType().isTracked())
{
return fetchRowsTracked(commands, consistencyLevel, requestTime);
}
else
{
return fetchRowsUntracked(commands, consistencyLevel, coordinator, requestTime);
}
}
public static class LocalReadRunnable extends DroppableRunnable implements RunnableDebuggableTask
{
private final ReadCommand command;

View File

@ -165,6 +165,8 @@ import org.apache.cassandra.repair.RepairParallelism;
import org.apache.cassandra.repair.SharedContext;
import org.apache.cassandra.repair.autorepair.AutoRepair;
import org.apache.cassandra.repair.messages.RepairOption;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.CompactionParams.TombstoneOption;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Keyspaces;
@ -3963,6 +3965,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
SnapshotManager.instance.shutdownAndWait(1L, MINUTES);
HintsService.instance.shutdownBlocking();
MutationTrackingService.instance.shutdownBlocking();
// Interrupt ongoing compactions and shutdown CM to prevent further compactions.
CompactionManager.instance.forceShutdown();
@ -3972,6 +3975,7 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
CommitLog.instance.forceRecycleAllSegments();
CommitLog.instance.shutdownBlocking();
MutationJournal.instance.shutdownBlocking();
AutoRepair.instance.shutdownBlocking();

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.service;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.replication.MutationTrackingService;
public class TrackedWriteResponseHandler extends AbstractWriteResponseHandler<NoPayload>
{
private final AbstractWriteResponseHandler<NoPayload> wrapped;
private final String keyspace;
private final Token token;
private final MutationId mutationId;
private TrackedWriteResponseHandler(
AbstractWriteResponseHandler<NoPayload> wrapped, String keyspace, Token token, MutationId mutationId)
{
super(wrapped.replicaPlan, wrapped.callback, wrapped.writeType, null, wrapped.getRequestTime());
this.wrapped = wrapped;
this.keyspace = keyspace;
this.token = token;
this.mutationId = mutationId;
}
public static TrackedWriteResponseHandler wrap(
AbstractWriteResponseHandler<NoPayload> handler, String keyspace, Token token, MutationId mutationId)
{
return new TrackedWriteResponseHandler(handler, keyspace, token, mutationId);
}
@Override
public void onResponse(Message<NoPayload> msg)
{
/* local mutations are witnessed from Keyspace.applyInternalTracked */
if (msg != null)
MutationTrackingService.instance.witnessedRemoteMutation(keyspace, token, mutationId, msg.from());
wrapped.onResponse(msg);
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
{
wrapped.onFailure(from, failureReason);
}
@Override
public boolean trackLatencyForSnitch()
{
return wrapped.trackLatencyForSnitch();
}
@Override
protected int ackCount()
{
return wrapped.ackCount();
}
@Override
public boolean invokeOnFailure()
{
return wrapped.invokeOnFailure();
}
@Override
public void get() throws WriteTimeoutException, WriteFailureException
{
wrapped.get();
}
@Override
protected int blockFor()
{
return wrapped.blockFor();
}
@Override
protected int candidateReplicaCount()
{
return wrapped.candidateReplicaCount();
}
@Override
protected boolean waitingFor(InetAddressAndPort from)
{
return wrapped.waitingFor(from);
}
@Override
protected void signal()
{
wrapped.signal();
}
}

View File

@ -36,6 +36,7 @@ import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableMetadata;
import static org.apache.cassandra.db.SystemKeyspace.legacyPaxosTtlSec;
@ -316,7 +317,15 @@ public class Commit
public Mutation makeMutation()
{
return new Mutation(update, PotentialTxnConflicts.ALLOW);
// TODO (expected): what's the best thing to do here? Deriving the mutation id from the ballot seems like the best
// thing to do, like we do with the partition update timestamps, but there are caveats related to id collisions
// and the assumption that a mutation id is unique amonth other mutations, which is not the case w/ paxos ballots,
// which only need to be unique to a given partition key to be accepted. It may be best to keep them separate anyway,
// since the reconciliation process as currently planned will not allow writes with ids before some point in time,
// and there might be edge cases where that locks up paxos execution or has other side effects. The downside of
// not making paxos mutation ids deterministic is that the same commit may create multiple mutation ids if a paxos
// operation is not fully committed, then re-committed on repair or the next operation
return new Mutation(MutationId.fixme(), update, PotentialTxnConflicts.ALLOW);
}
@Override

View File

@ -199,6 +199,7 @@ public abstract class AbstractReadExecutor
ReadCoordinator coordinator,
Dispatcher.RequestTime requestTime) throws UnavailableException
{
Preconditions.checkArgument(!command.metadata().replicationType().isTracked());
Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id);
SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry;

View File

@ -61,6 +61,7 @@ import org.apache.cassandra.service.reads.DataResolver;
import org.apache.cassandra.service.reads.ReadCallback;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.repair.ReadRepair;
import org.apache.cassandra.service.reads.tracked.TrackedRead;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
@ -208,7 +209,8 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
// If enabled, request repaired data tracking info from full replicas, but
// only if there are multiple full replicas to compare results from.
boolean trackRepairedStatus = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled()
&& replicaPlan.contacts().filter(Replica::isFull).size() > 1;
&& replicaPlan.contacts().filter(Replica::isFull).size() > 1
&& !command.metadata().replicationType().isTracked();
ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan);
ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair =
@ -329,7 +331,39 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
command.metadata().enforceStrictLiveness());
}
PartitionIterator sendNextRequests()
private PartitionIterator sendNextRequestsTracked()
{
List<PartitionIterator> concurrentQueries = new ArrayList<>(concurrencyFactor);
try
{
for (int i = 0; i < concurrencyFactor && replicaPlans.hasNext(); )
{
ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next();
PartitionRangeReadCommand rangeCommand = command.forSubRange(replicaPlan.range(), i == 0);
TrackedRead.Range read = TrackedRead.Range.create(rangeCommand, replicaPlan);
read.start(requestTime);
concurrentQueries.add(read.iterator());
// due to RangeMerger, coordinator may fetch more ranges than required by concurrency factor.
rangesQueried += replicaPlan.vnodeCount();
i += replicaPlan.vnodeCount();
}
batchesRequested++;
}
catch (Throwable t)
{
for (PartitionIterator response : concurrentQueries)
response.close();
throw t;
}
Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size());
return PartitionIterators.concat(concurrentQueries);
}
PartitionIterator sendNextRequestsUntracked()
{
List<PartitionIterator> concurrentQueries = new ArrayList<>(concurrencyFactor);
List<ReadRepair<?, ?>> readRepairs = new ArrayList<>(concurrencyFactor);
@ -366,13 +400,29 @@ public class RangeCommandIterator extends AbstractIterator<RowIterator> implemen
response.close();
throw t;
}
Tracing.trace("Submitted {} concurrent range requests", concurrentQueries.size());
return StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs);
}
PartitionIterator sendNextRequests()
{
PartitionIterator result;
if (command.metadata().replicationType().isTracked())
{
result = sendNextRequestsTracked();
}
else
{
result = sendNextRequestsUntracked();
}
// We want to count the results for the sake of updating the concurrency factor (see updateConcurrencyFactor)
// but we don't want to enforce any particular limit at this point (this could break code than rely on
// postReconciliationProcessing), hence the DataLimits.NONE.
counter = DataLimits.NONE.newCounter(command.nowInSec(), true, command.selectsFullPartition(), enforceStrictLiveness);
return counter.applyTo(StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs));
return counter.applyTo(result);
}
// Wrap the iterator to retry if request routing is incorrect

View File

@ -33,7 +33,7 @@ class SingleRangeResponse extends AbstractIterator<RowIterator> implements Parti
{
private final DataResolver<EndpointsForRange, ReplicaPlan.ForRangeRead> resolver;
private final ReadCallback<EndpointsForRange, ReplicaPlan.ForRangeRead> handler;
private final ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair;
protected final ReadRepair<EndpointsForRange, ReplicaPlan.ForRangeRead> readRepair;
private PartitionIterator result;

View File

@ -31,6 +31,7 @@ import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.replication.MutationId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.tracing.Tracing;
@ -53,7 +54,7 @@ public class BlockingReadRepairs
return null;
DecoratedKey key = update.partitionKey();
Mutation mutation = new Mutation(update, potentialTxnConflicts);
Mutation mutation = new Mutation(MutationId.fixme(), update, potentialTxnConflicts);
int messagingVersion = MessagingService.instance().versions.get(destination);
try

View File

@ -0,0 +1,154 @@
/*
* 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.service.reads.tracked;
import java.util.List;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.db.transform.RTBoundValidator;
import org.apache.cassandra.index.Index;
import static org.apache.cassandra.db.partitions.UnfilteredPartitionIterators.MergeListener.NOOP;
public abstract class AbstractPartialTrackedRead implements PartialTrackedRead
{
private static final Logger logger = LoggerFactory.getLogger(AbstractPartialTrackedRead.class);
private enum State
{
INITIALIZED,
PREPARED,
READING,
FINISHED
}
final ReadExecutionController executionController;
final Index.Searcher searcher;
final ColumnFamilyStore cfs;
final long startTimeNanos;
volatile State state = State.INITIALIZED;
public AbstractPartialTrackedRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos)
{
this.executionController = executionController;
this.searcher = searcher;
this.cfs = cfs;
this.startTimeNanos = startTimeNanos;
}
@Override
public ReadExecutionController executionController()
{
return executionController;
}
@Override
public Index.Searcher searcher()
{
return searcher;
}
@Override
public ColumnFamilyStore cfs()
{
return cfs;
}
@Override
public long startTimeNanos()
{
return startTimeNanos;
}
abstract void freezeInitialData();
abstract UnfilteredPartitionIterator initialData();
abstract UnfilteredPartitionIterator augmentedData();
abstract void augmentResponse(PartitionUpdate update);
/**
* Implementors need to call this before returning this from createInProgressRead
*/
synchronized void prepare()
{
logger.trace("Preparing read {}", this);
Preconditions.checkState(state == State.INITIALIZED);
freezeInitialData();
state = State.PREPARED;
}
@Override
public void augment(Mutation mutation)
{
Preconditions.checkState(state == State.PREPARED);
PartitionUpdate update = mutation.getPartitionUpdate(command().metadata());
if (update != null)
augmentResponse(update);
}
private UnfilteredPartitionIterator complete(UnfilteredPartitionIterator iterator)
{
return command().completeTrackedRead(iterator, this);
}
abstract CompletedRead createResult(UnfilteredPartitionIterator iterator);
@Override
public synchronized CompletedRead complete()
{
Preconditions.checkState(state == State.PREPARED);
state = State.READING;
UnfilteredPartitionIterator initial = initialData();
UnfilteredPartitionIterator augmented = augmentedData();
UnfilteredPartitionIterator result = augmented != null ?
UnfilteredPartitionIterators.merge(List.of(initial, augmented), NOOP) :
initial;
result = complete(result);
// validate that the sequence of RT markers is correct: open is followed by close, deletion times for both
// ends equal, and there are no dangling RT bound in any partition.
result = RTBoundValidator.validate(result, RTBoundValidator.Stage.PROCESSED, true);
return createResult(complete(result));
}
@Override
public synchronized void close()
{
if (state == State.FINISHED)
return;
logger.trace("Closing read {}", this);
executionController.close();
state = State.FINISHED;
}
}

View File

@ -0,0 +1,347 @@
/*
* 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.service.reads.tracked;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.TreeMap;
import com.google.common.base.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DataRange;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.PartitionPosition;
import org.apache.cassandra.db.PartitionRangeReadCommand;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.filter.DataLimits;
import org.apache.cassandra.db.partitions.AbstractBTreePartition;
import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.partitions.SimpleBTreePartition;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.transform.EmptyPartitionsDiscarder;
import org.apache.cassandra.db.transform.Transformation;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.dht.ExcludingBounds;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.transactions.UpdateTransaction;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.locator.ReplicaPlans;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
public class PartialTrackedRangeRead extends AbstractPartialTrackedRead
{
private static final Logger logger = LoggerFactory.getLogger(PartialTrackedRangeRead.class);
private final PartitionRangeReadCommand command;
private final SortedMap<DecoratedKey, SimpleBTreePartition> data = new TreeMap<>();
private final UnfilteredPartitionIterator initialData;
private final boolean enforceStrictLiveness;
// short read support
private DecoratedKey lastPartitionKey; // key of the last observed partition
private boolean partitionsFetched; // whether we've seen any new partitions since iteration start or last moreContents() call
private boolean initialIteratorExhausted;
private boolean wasAugmented;
AbstractBounds<PartitionPosition> followUpBounds;
public PartialTrackedRangeRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, PartitionRangeReadCommand command, UnfilteredPartitionIterator initialData)
{
super(executionController, searcher, cfs, startTimeNanos);
this.command = command;
this.initialData = initialData;
this.enforceStrictLiveness = command.metadata().enforceStrictLiveness();
}
public static PartialTrackedRangeRead create(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, PartitionRangeReadCommand command, UnfilteredPartitionIterator initialData)
{
PartialTrackedRangeRead read = new PartialTrackedRangeRead(executionController, searcher, cfs, startTimeNanos, command, initialData);
try
{
read.prepare();
return read;
}
catch (Throwable e)
{
read.close();
throw e;
}
}
@Override
public ReadCommand command()
{
return command;
}
UnfilteredRowIterator queryPartition(AbstractBTreePartition partition)
{
return partition.unfilteredIterator(command.columnFilter(),
command.requestedSlices(),
command.clusteringIndexFilter(partition.partitionKey()).isReversed());
}
private static void consume(UnfilteredPartitionIterator iterator)
{
while (iterator.hasNext())
{
try (UnfilteredRowIterator partition = iterator.next())
{
while (partition.hasNext())
partition.next();
}
}
}
@Override
void freezeInitialData()
{
// memtable contents are frozen at read completion time, when the iterator is evaluated, not at the beginning
// of the read, when references to memtables and sstables are collected. Because of this, replica coordinated
// reads can cause read monotonicity to be broken by returning data that hasn't been replicated to at least
// CL other nodes via reconciliation. To prevent this, the contents of the initial iterator are materialized
// onto heap at partition granularity until the limits of the read are reached.
UnfilteredPartitionIterator materializer = new AbstractUnfilteredPartitionIterator()
{
@Override
public TableMetadata metadata()
{
return initialData.metadata();
}
@Override
public boolean hasNext()
{
return initialData.hasNext();
}
@Override
public UnfilteredRowIterator next()
{
try (UnfilteredRowIterator rowIterator = initialData.next())
{
SimpleBTreePartition partition = augmentResponseInternal(PartitionUpdate.fromIterator(rowIterator, command.columnFilter()));
lastPartitionKey = partition.partitionKey();
partitionsFetched = true;
return queryPartition(partition);
}
}
@Override
public void close()
{
super.close();
initialData.close();
}
};
// unmerged per-source counter
final DataLimits.Counter singleResultCounter = command.limits().newCounter(command.nowInSec(),
false,
command.selectsFullPartition(),
enforceStrictLiveness);
try (UnfilteredPartitionIterator iterator = singleResultCounter.applyTo(materializer))
{
consume(iterator);
}
initialIteratorExhausted = command.limits().isExhausted(singleResultCounter);
if (partitionsFetched)
{
AbstractBounds<PartitionPosition> bounds = command.dataRange().keyRange();
followUpBounds = bounds.inclusiveRight()
? new Range<>(lastPartitionKey, bounds.right)
: new ExcludingBounds<>(lastPartitionKey, bounds.right);
Preconditions.checkState(!followUpBounds.contains(lastPartitionKey));
}
wasAugmented = false;
}
@Override
UnfilteredPartitionIterator initialData()
{
Iterator<SimpleBTreePartition> iterator = data.values().iterator();
return new AbstractUnfilteredPartitionIterator()
{
@Override
public TableMetadata metadata()
{
return command.metadata();
}
@Override
public boolean hasNext()
{
return iterator.hasNext();
}
@Override
public UnfilteredRowIterator next()
{
return queryPartition(iterator.next());
}
};
}
@Override
UnfilteredPartitionIterator augmentedData()
{
return null;
}
private SimpleBTreePartition augmentResponseInternal(PartitionUpdate update)
{
SimpleBTreePartition partition = data.computeIfAbsent(update.partitionKey(), key -> new SimpleBTreePartition(key, update.metadata(), UpdateTransaction.NO_OP));
partition.update(update);
return partition;
}
@Override
void augmentResponse(PartitionUpdate update)
{
// if the input iterator reached the row limit, then we can't apply any augmenting mutations that are past
// the last materialized key. Since we wouldn't have materialized the local data for that key, applying an
// update would cause us to return incomplete data for it.
if (initialIteratorExhausted || !followUpBounds.contains(update.partitionKey()))
augmentResponseInternal(update);
wasAugmented = true;
}
private class ExtendingCompletedRead implements CompletedRead
{
final UnfilteredPartitionIterator iterator;
// merged end-result counter
final DataLimits.Counter mergedResultCounter = command.limits().newCounter(command.nowInSec(),
true,
command.selectsFullPartition(),
enforceStrictLiveness);
public ExtendingCompletedRead(UnfilteredPartitionIterator iterator)
{
this.iterator = iterator;
}
@Override
public PartitionIterator iterator()
{
PartitionIterator filtered = UnfilteredPartitionIterators.filter(iterator, command.nowInSec());
PartitionIterator counted = Transformation.apply(filtered, mergedResultCounter);
return Transformation.apply(counted, new EmptyPartitionsDiscarder());
}
@Override
public TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos)
{
// never try to request additional partitions from replicas if our reconciled partitions are already filled to the limit
if (mergedResultCounter.isDone())
return null;
// we do not apply short read protection when we have no limits at all
if (command.limits().isUnlimited())
return null;
/*
* If this is a single partition read command or an (indexed) partition range read command with
* a partition key specified, then we can't and shouldn't try fetch more partitions.
*/
if (command.isLimitedToOnePartition())
return null;
/*
* If the returned result doesn't have enough rows/partitions to satisfy even the original limit, don't ask for more.
*
* Can only take the short cut if there is no per partition limit set. Otherwise it's possible to hit false
* positives due to some rows being uncounted for in certain scenarios (see CASSANDRA-13911).
*/
if (initialIteratorExhausted && command.limits().perPartitionCount() == DataLimits.NO_LIMIT)
return null;
/*
* Either we had an empty iterator as the initial response, or our moreContents() call got us an empty iterator.
* There is no point to ask the replica for more rows - it has no more in the requested range.
*/
if (!partitionsFetched)
return null;
partitionsFetched = false;
/*
* We are going to fetch one partition at a time for thrift and potentially more for CQL.
* The row limit will either be set to the per partition limit - if the command has no total row limit set, or
* the total # of rows remaining - if it has some. If we don't grab enough rows in some of the partitions,
* then future ShortReadRowsProtection.moreContents() calls will fetch the missing ones.
*/
int toQuery = command.limits().count() != DataLimits.NO_LIMIT
? command.limits().count() - mergedResultCounter.rowsCounted()
: command.limits().perPartitionCount();
ColumnFamilyStore.metricsFor(command.metadata().id).shortReadProtectionRequests.mark();
Tracing.trace("Requesting {} extra rows from {} for short read protection", toQuery, FBUtilities.getBroadcastAddressAndPort());
logger.info("Requesting {} extra rows from {} for short read protection", toQuery, FBUtilities.getBroadcastAddressAndPort());
return makeFollowupRead(toQuery, consistencyLevel, expiresAtNanos);
}
private TrackedRead<?, ?> makeFollowupRead(int toQuery, ConsistencyLevel consistencyLevel, long expiresAtNanos)
{
DataLimits newLimits = command.limits().forShortReadRetry(toQuery);
DataRange newDataRange = command.dataRange().forSubRange(followUpBounds);
Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
PartitionRangeReadCommand followUpCmd = command.withUpdatedLimitsAndDataRange(newLimits, newDataRange);
ReplicaPlan.ForRangeRead replicaPlan = ReplicaPlans.forRangeRead(keyspace,
followUpCmd.indexQueryPlan(),
consistencyLevel,
followUpCmd.dataRange().keyRange(),
1);
TrackedRead.Range read = TrackedRead.Range.create(followUpCmd, replicaPlan);
logger.trace("Short read detected, starting followup read {}", read);
read.start(expiresAtNanos);
return read;
}
@Override
public void close()
{
iterator.close();
}
}
@Override
CompletedRead createResult(UnfilteredPartitionIterator iterator)
{
if (wasAugmented)
return new ExtendingCompletedRead(iterator);
return CompletedRead.simple(iterator, command().nowInSec());
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.service.reads.tracked;
import java.util.Collection;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.index.Index;
public interface PartialTrackedRead
{
interface CompletedRead extends AutoCloseable
{
PartitionIterator iterator();
TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos);
@Override
void close();
static CompletedRead simple(UnfilteredPartitionIterator partition, long nowInSec)
{
return new CompletedRead()
{
@Override
public PartitionIterator iterator()
{
return UnfilteredPartitionIterators.filter(partition, nowInSec);
}
@Override
public TrackedRead<?, ?> followupRead(ConsistencyLevel consistencyLevel, long expiresAtNanos)
{
return null;
}
@Override
public void close()
{
partition.close();
}
};
}
}
CompletedRead complete();
void augment(Mutation mutation);
default void augment(Collection<Mutation> mutations)
{
mutations.forEach(this::augment);
}
ReadExecutionController executionController();
Index.Searcher searcher();
ColumnFamilyStore cfs();
long startTimeNanos();
ReadCommand command();
void close();
}

View File

@ -0,0 +1,108 @@
/*
* 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.service.reads.tracked;
import com.google.common.base.Preconditions;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.partitions.SimpleBTreePartition;
import org.apache.cassandra.db.partitions.SingletonUnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.transactions.UpdateTransaction;
public class PartialTrackedSinglePartitionRead extends AbstractPartialTrackedRead
{
private final SinglePartitionReadCommand command;
private final UnfilteredPartitionIterator initialData;
private SimpleBTreePartition augmentedData;
public PartialTrackedSinglePartitionRead(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, SinglePartitionReadCommand command, UnfilteredPartitionIterator initialData)
{
super(executionController, searcher, cfs, startTimeNanos);
this.command = command;
this.initialData = initialData;
}
public static PartialTrackedSinglePartitionRead create(ReadExecutionController executionController, Index.Searcher searcher, ColumnFamilyStore cfs, long startTimeNanos, SinglePartitionReadCommand command, UnfilteredPartitionIterator initialData)
{
PartialTrackedSinglePartitionRead read = new PartialTrackedSinglePartitionRead(executionController, searcher, cfs, startTimeNanos, command, initialData);
try
{
read.prepare();
return read;
}
catch (Throwable e)
{
read.close();
throw e;
}
}
@Override
void freezeInitialData()
{
// the iterators from queryStorage grabs sstable references and a
// snapshot of the memtable partition so we don't need to do anything here
}
@Override
public ReadCommand command()
{
return command;
}
@Override
UnfilteredPartitionIterator initialData()
{
return initialData;
}
@Override
UnfilteredPartitionIterator augmentedData()
{
if (augmentedData == null)
return null;
Slices slices = command.clusteringIndexFilter().getSlices(command.metadata());
UnfilteredRowIterator augmented = augmentedData.unfilteredIterator(command.columnFilter(), slices, command.clusteringIndexFilter().isReversed());
return new SingletonUnfilteredPartitionIterator(augmented);
}
@Override
void augmentResponse(PartitionUpdate update)
{
Preconditions.checkArgument(update.partitionKey().equals(command.partitionKey()));
if (augmentedData == null)
augmentedData = new SimpleBTreePartition(command.partitionKey(), command.metadata(), UpdateTransaction.NO_OP);
augmentedData.update(update);
}
@Override
CompletedRead createResult(UnfilteredPartitionIterator iterator)
{
return CompletedRead.simple(iterator, command().nowInSec());
}
}

View File

@ -0,0 +1,95 @@
/*
* 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.service.reads.tracked;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.replication.MutationTrackingService;
/**
* Notifies the read coordinator that this node has received mutations
* from the sending node
*/
public class ReadReconcileNotify
{
private static final Logger logger = LoggerFactory.getLogger(ReadReconcileNotify.class);
public final TrackedRead.Id readId;
public final int syncId;
public ReadReconcileNotify(TrackedRead.Id readId, int syncId)
{
this.readId = readId;
this.syncId = syncId;
}
@Override
public String toString()
{
return "ReadReconcileNotify{" +
"reconciliationId=" + readId +
", syncId=" + syncId +
'}';
}
public static final IVerbHandler<ReadReconcileNotify> verbHandler = new IVerbHandler<ReadReconcileNotify>()
{
@Override
public void doVerb(Message<ReadReconcileNotify> message) throws IOException
{
ReadReconcileNotify notify = message.payload;
logger.trace("Received read reconcile notify from {}: {}", message.from(), notify);
MutationTrackingService.instance.localReads().acknowledgeSync(notify.readId, notify.syncId);
}
};
public static final IVersionedSerializer<ReadReconcileNotify> serializer = new IVersionedSerializer<ReadReconcileNotify>()
{
@Override
public void serialize(ReadReconcileNotify notify, DataOutputPlus out, int version) throws IOException
{
TrackedRead.Id.serializer.serialize(notify.readId, out, version);
out.writeInt(notify.syncId);
}
@Override
public ReadReconcileNotify deserialize(DataInputPlus in, int version) throws IOException
{
TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
int syncId = in.readInt();
return new ReadReconcileNotify(readId, syncId);
}
@Override
public long serializedSize(ReadReconcileNotify notify, int version)
{
return TrackedRead.Id.serializer.serializedSize(notify.readId, version)
+ TypeSizes.sizeof(notify.syncId);
}
};
}

View File

@ -0,0 +1,138 @@
/*
* 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.service.reads.tracked;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.utils.CollectionSerializer;
import static org.apache.cassandra.locator.InetAddressAndPort.Serializer.inetAddressAndPortSerializer;
public class ReadReconcileReceive
{
private static final Logger logger = LoggerFactory.getLogger(ReadReconcileReceive.class);
public final TrackedRead.Id readId;
public final int syncId;
public final InetAddressAndPort coordinator;
public final List<Mutation> mutations;
public ReadReconcileReceive(TrackedRead.Id readId, int syncId, InetAddressAndPort coordinator, List<Mutation> mutations)
{
this.readId = readId;
this.syncId = syncId;
this.coordinator = coordinator;
this.mutations = mutations;
}
private static String mutationString(List<Mutation> mutations)
{
StringBuilder builder = new StringBuilder('[');
boolean isFirst = true;
for (Mutation mutation : mutations)
{
if (!isFirst)
{
builder.append(", ");
}
isFirst = false;
builder.append(mutation.id());
}
builder.append(']');
return builder.toString();
}
@Override
public String toString()
{
return "ReadReconcileReceive{" +
"reconciliationId=" + readId +
", syncId=" + syncId +
", coordinator=" + coordinator +
", mutations=" + mutationString(mutations) +
'}';
}
public static final IVerbHandler<ReadReconcileReceive> verbHandler = new IVerbHandler<ReadReconcileReceive>()
{
@Override
public void doVerb(Message<ReadReconcileReceive> message) throws IOException
{
// TODO: check epoch and tokens?
ReadReconcileReceive receive = message.payload;
logger.trace("Received read reconciliation from {}: {}", message.from(), receive);
receive.mutations.forEach(Mutation::apply);
if (!MutationTrackingService.instance.localReads().receiveMutations(receive.readId, receive.syncId, receive.mutations))
{
// if this isn't a locally coordinated read, notify the coordinator
ReadReconcileNotify notify = new ReadReconcileNotify(receive.readId, receive.syncId);
MessagingService.instance().send(Message.out(Verb.READ_RECONCILE_NOTIFY, notify), receive.coordinator);
}
}
};
public static final IVersionedSerializer<ReadReconcileReceive> serializer = new IVersionedSerializer<ReadReconcileReceive>()
{
@Override
public void serialize(ReadReconcileReceive rcv, DataOutputPlus out, int version) throws IOException
{
TrackedRead.Id.serializer.serialize(rcv.readId, out, version);
out.writeInt(rcv.syncId);
inetAddressAndPortSerializer.serialize(rcv.coordinator, out, version);
CollectionSerializer.serializeCollection(Mutation.serializer, rcv.mutations, out, version);
}
@Override
public ReadReconcileReceive deserialize(DataInputPlus in, int version) throws IOException
{
TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
int syncId = in.readInt();
InetAddressAndPort coordinator = inetAddressAndPortSerializer.deserialize(in, version);
List<Mutation> mutations = CollectionSerializer.deserializeCollection(Mutation.serializer, ArrayList::new, in, version);
return new ReadReconcileReceive(readId, syncId, coordinator, mutations);
}
@Override
public long serializedSize(ReadReconcileReceive t, int version)
{
return TrackedRead.Id.serializer.serializedSize(t.readId, version)
+ TypeSizes.sizeof(t.syncId)
+ inetAddressAndPortSerializer.serializedSize(t.coordinator, version)
+ CollectionSerializer.serializedSizeCollection(Mutation.serializer, t.mutations, version);
}
};
}

View File

@ -0,0 +1,166 @@
/*
* 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.service.reads.tracked;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.Log2OffsetsMap;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.utils.CollectionSerializer;
/**
* Instructs a node to send mutations to other node
*/
public class ReadReconcileSend
{
private static final Logger logger = LoggerFactory.getLogger(ReadReconcileSend.class);
public static class PeerSync
{
final int syncId;
final InetAddressAndPort to;
final Log2OffsetsMap.Immutable plan;
public PeerSync(int syncId, InetAddressAndPort to, Log2OffsetsMap.Immutable plan)
{
this.syncId = syncId;
this.to = to;
this.plan = plan;
}
@Override
public String toString()
{
return "PeerSync{" +
"syncId=" + syncId +
", to=" + to +
", plan=" + plan +
'}';
}
public static final IVersionedSerializer<PeerSync> serializer = new IVersionedSerializer<PeerSync>()
{
@Override
public void serialize(PeerSync sync, DataOutputPlus out, int version) throws IOException
{
out.writeInt(sync.syncId);
InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serialize(sync.to, out, version);
Log2OffsetsMap.Immutable.serializer.serialize(sync.plan, out, version);
}
@Override
public PeerSync deserialize(DataInputPlus in, int version) throws IOException
{
return new PeerSync(in.readInt(),
InetAddressAndPort.Serializer.inetAddressAndPortSerializer.deserialize(in, version),
Log2OffsetsMap.Immutable.serializer.deserialize(in, version));
}
@Override
public long serializedSize(PeerSync sync, int version)
{
return TypeSizes.sizeof(sync.syncId)
+ InetAddressAndPort.Serializer.inetAddressAndPortSerializer.serializedSize(sync.to, version)
+ Log2OffsetsMap.Immutable.serializer.serializedSize(sync.plan, version);
}
};
}
public final TrackedRead.Id reconcileId;
public final ImmutableList<PeerSync> syncTasks;
public ReadReconcileSend(TrackedRead.Id reconcileId, List<PeerSync> syncTasks)
{
this.reconcileId = reconcileId;
this.syncTasks = ImmutableList.copyOf(syncTasks);
}
@Override
public String toString()
{
return "ReadReconcileSend{" +
"reconcileId=" + reconcileId +
", syncTasks=" + syncTasks +
'}';
}
public static final IVerbHandler<ReadReconcileSend> verbHandler = new IVerbHandler<>()
{
@Override
public void doVerb(Message<ReadReconcileSend> message)
{
logger.trace("Received ReadReconcileSend from {}: {}", message.from(), message.payload);
// TODO: check epoch and tokens?
ReadReconcileSend payload = message.payload;
for (PeerSync sync : message.payload.syncTasks)
{
// TODO (expected): do not deser just to serialize again, if same messaging versions (common case)
// TODO (expected): don't materialize mutation ids, look up from offset collections
int mutationCount = sync.plan.idCount();
List<Mutation> mutations = new ArrayList<>(mutationCount);
MutationJournal.instance.readAll(sync.plan, mutations);
Preconditions.checkArgument(mutationCount == mutations.size());
ReadReconcileReceive receive = new ReadReconcileReceive(payload.reconcileId, sync.syncId, message.from(), mutations);
logger.trace("Sending {} to replica {}", receive, sync.to);
MessagingService.instance().send(Message.out(Verb.READ_RECONCILE_RCV, receive), sync.to);
}
}
};
public static final IVersionedSerializer<ReadReconcileSend> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(ReadReconcileSend send, DataOutputPlus out, int version) throws IOException
{
TrackedRead.Id.serializer.serialize(send.reconcileId, out, version);
CollectionSerializer.serializeList(PeerSync.serializer, send.syncTasks, out, version);
}
@Override
public ReadReconcileSend deserialize(DataInputPlus in, int version) throws IOException
{
TrackedRead.Id readId = TrackedRead.Id.serializer.deserialize(in, version);
List<PeerSync> syncTasks = CollectionSerializer.deserializeCollection(PeerSync.serializer, ArrayList::new, in, version);
return new ReadReconcileSend(readId, syncTasks);
}
@Override
public long serializedSize(ReadReconcileSend send, int version)
{
return TrackedRead.Id.serializer.serializedSize(send.reconcileId, version) + CollectionSerializer.serializedSizeCollection(PeerSync.serializer, send.syncTasks, version);
}
};
}

View File

@ -0,0 +1,99 @@
/*
* 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.service.reads.tracked;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.TypeSizes;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionIterators;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.ByteBufferUtil;
import java.io.IOException;
import java.nio.ByteBuffer;
public class TrackedDataResponse
{
private final int serializationVersion;
private final ByteBuffer data;
public TrackedDataResponse(int serializationVersion, ByteBuffer data)
{
this.serializationVersion = serializationVersion;
this.data = data;
}
public static TrackedDataResponse create(PartitionIterator iter, ColumnFilter selection)
{
try (DataOutputBuffer buffer = new DataOutputBuffer())
{
PartitionIterators.Serializer.serialize(iter, selection, buffer, MessagingService.current_version);
return new TrackedDataResponse(MessagingService.current_version, buffer.buffer(false));
}
catch (IOException e)
{
// We're serializing in memory so this shouldn't happen
throw new RuntimeException(e);
}
}
public PartitionIterator makeIterator(ReadCommand command)
{
try (DataInputBuffer in = new DataInputBuffer(data, true))
{
return PartitionIterators.Serializer.deserialize(command.metadata(), command.columnFilter(), in, serializationVersion);
}
catch (IOException e)
{
// We're deserializing in memory so this shouldn't happen
throw new RuntimeException(e);
}
}
public static final IVersionedSerializer<TrackedDataResponse> serializer = new IVersionedSerializer<TrackedDataResponse>()
{
@Override
public void serialize(TrackedDataResponse response, DataOutputPlus out, int version) throws IOException
{
out.writeInt(response.serializationVersion);
ByteBufferUtil.writeWithVIntLength(response.data, out);
}
@Override
public TrackedDataResponse deserialize(DataInputPlus in, int version) throws IOException
{
int serializationVersion = in.readInt();
ByteBuffer data = ByteBufferUtil.readWithVIntLength(in);
return new TrackedDataResponse(serializationVersion, data);
}
@Override
public long serializedSize(TrackedDataResponse response, int version)
{
return TypeSizes.sizeof(response.serializationVersion)
+ ByteBufferUtil.serializedSizeWithVIntLength(response.data);
}
};
}

View File

@ -0,0 +1,617 @@
/*
* 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.service.reads.tracked;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionIterators;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.metrics.ReadRepairMetrics;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.replication.Log2OffsetsMap;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.ReconciliationPlan;
import org.apache.cassandra.replication.ShortMutationId;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.AbstractFuture;
import org.apache.cassandra.utils.concurrent.Accumulator;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
public class TrackedLocalReadCoordinator
{
private static final Logger logger = LoggerFactory.getLogger(TrackedLocalReadCoordinator.class);
private final TrackedRead.Id readId;
private final AsyncPromise<TrackedDataResponse> promise;
private volatile State state;
public TrackedLocalReadCoordinator(TrackedRead.Id readId)
{
this.readId = readId;
this.promise = new AsyncPromise<>();
this.state = INITIALIZED;
}
public AbstractFuture<TrackedDataResponse> addCallback(BiConsumer<TrackedDataResponse, Throwable> callback)
{
return promise.addCallback(callback);
}
enum Status { INITIALIZED, AWAITING_READ, READING, RECONCILING, ABORTED, COMPLETED }
private static abstract class State
{
abstract Status status();
State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
{
// TODO: validate permitted state instead of just ignoring events?
return this;
}
State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
{
throw new IllegalStateException("Received in-progress read with status " + status());
}
State receiveSummary(InetAddressAndPort from, MutationSummary summary)
{
logger.trace("Ignoring summary from {} with status {}", from, status());
return this;
}
State acknowledgeSync(int syncId)
{
logger.trace("Ignoring sync ack {} with status {}", syncId, status());
return this;
}
State receiveMutations(List<Mutation> mutations)
{
logger.trace("Ignoring {} mutations with status {}", mutations.size(), status());
return this;
}
State abort()
{
return ABORTED;
}
boolean isPurgeable(long nanoTime)
{
return false;
}
boolean isReading()
{
return is(Status.READING);
}
boolean isCompleted()
{
return is(Status.COMPLETED);
}
boolean is(Status status)
{
return status() == status;
}
@Override
public String toString()
{
return status().toString();
}
}
static final State INITIALIZED = new State()
{
@Override
Status status() { return Status.INITIALIZED; }
@Override
State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
{
return new Reading(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos);
}
@Override
State receiveSummary(InetAddressAndPort from, MutationSummary summary)
{
return new AwaitingRead(from, summary);
}
};
static final State ABORTED = new State()
{
@Override
Status status() { return Status.ABORTED; }
@Override
State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
{
return this; // the read can't complete without data, but it could have been aborted in the meantime
}
@Override
boolean isPurgeable(long nanoTime) { return true; }
};
static final State COMPLETED = new State()
{
@Override
Status status() { return Status.COMPLETED; }
@Override
boolean isPurgeable(long nanoTime) { return true; }
};
private static class ReceivedSummary
{
final InetAddressAndPort from;
final MutationSummary summary;
ReceivedSummary(InetAddressAndPort from, MutationSummary summary)
{
this.from = from;
this.summary = summary;
}
}
// if we start receiving summaries before we receive the read command, they're
// collected here
private static class AwaitingRead extends State
{
private long lastUpdateNanos;
private final List<ReceivedSummary> summaries;
AwaitingRead(InetAddressAndPort summaryFrom, MutationSummary summary)
{
summaries = new ArrayList<>();
summaries.add(new ReceivedSummary(summaryFrom, summary));
}
@Override
Status status()
{
return Status.AWAITING_READ;
}
@Override
State startLocalRead(TrackedRead.Id readId, AsyncPromise<TrackedDataResponse> promise, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
{
return new Reading(readId, promise, command, replicaPlan, summaryNodes, summaries, expiresAtNanos);
}
@Override
State receiveSummary(InetAddressAndPort from, MutationSummary summary)
{
summaries.add(new ReceivedSummary(from, summary));
lastUpdateNanos = Clock.Global.nanoTime();
return this;
}
@Override
boolean isPurgeable(long nanoTime)
{
return nanoTime - lastUpdateNanos > DatabaseDescriptor.getReadRpcTimeout(TimeUnit.NANOSECONDS);
}
}
private static class Reading extends State
{
private final TrackedRead.Id readId;
private final AsyncPromise<TrackedDataResponse> promise;
private final ReadCommand command;
private volatile PartialTrackedRead read;
private final long expiresAtNanos;
private final ReplicaPlan.AbstractForRead<?, ?> replicaPlan;
private final Accumulator<ReceivedSummary> summaries;
private final int[] summaryNodes; // for speculating when we haven't received enough summaries
Reading(
TrackedRead.Id readId,
AsyncPromise<TrackedDataResponse> promise,
ReadCommand command,
ReplicaPlan.AbstractForRead<?, ?> replicaPlan,
int[] summaryNodes,
long expiresAtNanos)
{
this.readId = readId;
this.promise = promise;
this.expiresAtNanos = expiresAtNanos;
this.command = command;
this.replicaPlan = replicaPlan;
this.summaries = new Accumulator<>(replicaPlan.readCandidates().size());
this.summaryNodes = summaryNodes;
}
Reading(
TrackedRead.Id readId,
AsyncPromise<TrackedDataResponse> promise,
ReadCommand command,
ReplicaPlan.AbstractForRead<?, ?> replicaPlan,
int[] summaryNodes,
List<ReceivedSummary> summaries,
long expiresAtNanos)
{
this(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos);
for (ReceivedSummary summary : summaries) this.summaries.add(summary);
}
@Override
Status status()
{
return Status.READING;
}
@Override
boolean isPurgeable(long nanoTime)
{
return nanoTime - expiresAtNanos > 0;
}
private State maybeComplete()
{
if (read == null || summaries.size() < replicaPlan.readQuorum())
return this;
Map<InetAddressAndPort, MutationSummary> summaryMap = new HashMap<>();
summaries.snapshot().forEach(rc -> summaryMap.put(rc.from, rc.summary));
Map<InetAddressAndPort, ReconciliationPlan> reconciliations = ReconciliationPlan.calculateReconciliation(summaryMap);
if (reconciliations.isEmpty())
{
logger.trace("Read complete for {}", readId);
complete(promise, read, command.columnFilter(), replicaPlan.consistencyLevel(), expiresAtNanos);
return COMPLETED;
}
else
{
logger.trace("Beginning reconciliation for {}", readId);
Reconciling reconciling = new Reconciling(readId, promise, command, read, replicaPlan.consistencyLevel(), expiresAtNanos, reconciliations);
reconciling.start(); // TODO: don't do this until after the coordinator state is set to reconciling if converting to lock free
return reconciling;
}
}
@Override
State receiveInProgressRead(PartialTrackedRead read, MutationSummary summary)
{
if (this.read != null)
return this;
logger.trace("In progress read received for {}", readId);
this.read = read;
summaries.add(new ReceivedSummary(FBUtilities.getBroadcastAddressAndPort(), summary));
return maybeComplete();
}
@Override
State receiveSummary(InetAddressAndPort from, MutationSummary summary)
{
logger.trace("Summary received from {} for {}", from, readId);
summaries.add(new ReceivedSummary(from, summary));
return maybeComplete();
}
@Override
State abort()
{
logger.trace("Aborting read {}", readId);
if (read != null)
read.close();
return ABORTED;
}
}
private static class PendingSync
{
final int syncId;
final InetAddressAndPort from;
final InetAddressAndPort to;
final Log2OffsetsMap.Immutable plan;
PendingSync(int syncId, InetAddressAndPort from, InetAddressAndPort to, Log2OffsetsMap.Immutable plan)
{
this.syncId = syncId;
this.from = from;
this.to = to;
this.plan = plan;
}
ReadReconcileSend.PeerSync toPeerSync()
{
return new ReadReconcileSend.PeerSync(syncId, to, plan);
}
}
private static class Reconciling extends State
{
private final TrackedRead.Id readId;
private final AsyncPromise<TrackedDataResponse> promise;
private final ReadCommand command;
private final PartialTrackedRead read;
private final ConsistencyLevel consistencyLevel;
private final long expiresAtNanos;
final Map<InetAddressAndPort, ReconciliationPlan> plans;
final Log2OffsetsMap.Mutable outstandingMutations = new Log2OffsetsMap.Mutable();
final Map<Integer, PendingSync> pendingSync = new ConcurrentHashMap<>();
final int blockFor;
Reconciling(
TrackedRead.Id readId,
AsyncPromise<TrackedDataResponse> promise,
ReadCommand command,
PartialTrackedRead read,
ConsistencyLevel consistencyLevel,
long expiresAtNanos,
Map<InetAddressAndPort, ReconciliationPlan> plans)
{
this.readId = readId;
this.promise = promise;
this.command = command;
this.read = Preconditions.checkNotNull(read);
this.consistencyLevel = consistencyLevel;
this.expiresAtNanos = expiresAtNanos;
this.plans = plans;
int syncs = 0;
int nextSyncId = 0;
for (Map.Entry<InetAddressAndPort, ReconciliationPlan> entry : plans.entrySet())
{
InetAddressAndPort from = entry.getKey();
ReconciliationPlan plan = entry.getValue();
for (InetAddressAndPort to : plan.nodes())
{
int syncId = nextSyncId++;
PendingSync sync = new PendingSync(syncId, from, to, plan.peerReconciliation(to));
pendingSync.put(syncId, sync);
syncs++;
if (to.equals(FBUtilities.getBroadcastAddressAndPort()))
{
outstandingMutations.addAll(plan.offsetsFor(to));
}
}
}
if (logger.isTraceEnabled())
logger.trace("Reconciling {} syncs, {} mutations for {}", syncs, outstandingMutations.idCount(), readId);
this.blockFor = syncs;
}
@Override
Status status()
{
return Status.RECONCILING;
}
@Override
boolean isPurgeable(long nanoTime)
{
return nanoTime - expiresAtNanos > 0;
}
void start()
{
ReadRepairMetrics.trackedReconcile.mark();
ColumnFamilyStore.metricsFor(command.metadata().id).readRepairRequests.mark();
Map<InetAddressAndPort, List<ReadReconcileSend.PeerSync>> peerSync = new HashMap<>();
pendingSync.values().forEach(pending ->
peerSync.computeIfAbsent(pending.from, node -> new ArrayList<>()).add(pending.toPeerSync()));
for (Map.Entry<InetAddressAndPort, List<ReadReconcileSend.PeerSync>> entry : peerSync.entrySet())
{
Message<ReadReconcileSend> message = Message.out(Verb.READ_RECONCILE_SEND, new ReadReconcileSend(readId, entry.getValue()));
logger.trace("Sending read reconciliation for {} {} to {}", readId, message.payload, entry.getKey());
MessagingService.instance().send(message, entry.getKey());
}
}
private State maybeComplete()
{
if (!pendingSync.isEmpty() || !outstandingMutations.isEmpty())
return this;
logger.trace("Reconciliation completed for read {}", readId);
complete(promise, read, command.columnFilter(), consistencyLevel, expiresAtNanos);
return COMPLETED;
}
@Override
State acknowledgeSync(int syncId)
{
logger.trace("Reconciliation sync {} received for {}", syncId, readId);
pendingSync.remove(syncId);
return maybeComplete();
}
@Override
State receiveMutations(List<Mutation> mutations)
{
Log2OffsetsMap.Mutable received = new Log2OffsetsMap.Mutable();
mutations.forEach(mutation -> {
logger.trace("Received mutation {} for read {}", mutation.id(), readId);
received.add(mutation.id());
});
outstandingMutations.removeAll(received);
if (logger.isTraceEnabled())
logger.trace("Received {} mutations, {} mutations outstanding for {}", mutations.size(), outstandingMutations.idCount(), readId);
read.augment(mutations);
return maybeComplete();
}
@Override
State abort()
{
read.close();
return ABORTED;
}
}
@Override
public String toString()
{
return "TrackedLocalReadCoordinator{" + readId + ':' + state.status() + '}';
}
@VisibleForTesting
public static void processDelta(PartialTrackedRead read, MutationSummary initialSummary, MutationSummary secondarySummary)
{
// Compute any mutations that we could've missed during initial read execution.
ArrayList<ShortMutationId> delta = new ArrayList<>();
MutationSummary.difference(secondarySummary, initialSummary, delta);
delta.forEach(mutationId -> {
Mutation mutation = MutationJournal.instance.read(mutationId);
Preconditions.checkNotNull(mutation);
read.augment(mutation);
});
}
public void startLocalRead(TrackedRead.Id readId, ReadCommand command, ReplicaPlan.AbstractForRead<?, ?> replicaPlan, int[] summaryNodes, long expiresAtNanos)
{
synchronized (this)
{
if (!(state = state.startLocalRead(readId, promise, command, replicaPlan, summaryNodes, expiresAtNanos)).isReading())
return;
}
PartialTrackedRead read;
MutationSummary secondarySummary;
MutationSummary initialSummary = command.createMutationSummary(false);
ReadExecutionController controller = command.executionController(false);
try
{
read = command.beginTrackedRead(controller);
// Create another summary once initial data has been read fully. We do this to catch
// any mutations that may have arrived during initial read execution.
secondarySummary = command.createMutationSummary(true);
processDelta(read, initialSummary, secondarySummary);
}
catch (Exception e)
{
controller.close();
abort();
throw e;
}
synchronized (this)
{
state = state.receiveInProgressRead(read, secondarySummary);
}
}
private static void complete(AsyncPromise<TrackedDataResponse> promise, PartialTrackedRead read, ColumnFilter selection, ConsistencyLevel consistencyLevel, long expiresAtNanos)
{
Stage.READ.submit(() -> completeInternal(promise, read, selection, consistencyLevel, expiresAtNanos));
}
private static void completeInternal(AsyncPromise<TrackedDataResponse> promise, PartialTrackedRead read, ColumnFilter selection, ConsistencyLevel consistencyLevel, long expiresAtNanos)
{
try (PartialTrackedRead.CompletedRead completedRead = read.complete())
{
TrackedDataResponse response = TrackedDataResponse.create(completedRead.iterator(), selection);
TrackedRead<?, ?> followUp = completedRead.followupRead(consistencyLevel, expiresAtNanos);
if (followUp != null)
{
ReadCommand command = read.command();
followUp.future().addCallback((iterator, error) -> {
if (error != null)
{
promise.tryFailure(error);
return;
}
PartitionIterator previous = response.makeIterator(command);
TrackedDataResponse newResponse = TrackedDataResponse.create(PartitionIterators.concat(List.of(previous, iterator)), selection);
promise.trySuccess(newResponse);
});
}
else
{
promise.trySuccess(response);
}
}
catch (Exception e)
{
promise.tryFailure(e);
throw e;
}
finally
{
read.close();
}
}
synchronized void receiveSummary(InetAddressAndPort from, MutationSummary summary)
{
if (logger.isTraceEnabled())
logger.trace("Received summary {} from {}, for {}", summary, from, state);
state = state.receiveSummary(from, summary);
}
synchronized boolean acknowledgeSync(int syncId)
{
return (state = state.acknowledgeSync(syncId)).isCompleted();
}
synchronized boolean receiveMutations(List<Mutation> mutations)
{
return (state = state.receiveMutations(mutations)).isCompleted();
}
synchronized void abort()
{
state = state.abort();
}
boolean isTimedOutOrComplete(long nanoTime)
{
return state.isPurgeable(nanoTime);
}
}

View File

@ -0,0 +1,182 @@
/*
* 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.service.reads.tracked;
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
import org.apache.cassandra.concurrent.Shutdownable;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.*;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.locator.ReplicaPlans;
import org.apache.cassandra.service.reads.SpeculativeRetryPolicy;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
import static org.apache.cassandra.concurrent.ExecutorFactory.SimulatorSemantics.NORMAL;
/**
* Since the read reconciliations don't use 2 way callbacks, a map of active reconciliations
* are maintained and expired here.
*
* Borrowed heavily from RequestCallbacks
*/
public class TrackedLocalReads implements Shutdownable
{
private static final Logger logger = LoggerFactory.getLogger(TrackedLocalReads.class);
private final ConcurrentMap<TrackedRead.Id, TrackedLocalReadCoordinator> reads = new ConcurrentHashMap<>();
private final ScheduledExecutorPlus executor = executorFactory().scheduled("Reconciliation-Map-Reaper", NORMAL);
public TrackedLocalReads()
{
long expirationInterval = defaultExpirationInterval();
executor.scheduleWithFixedDelay(this::expire, expirationInterval, expirationInterval, NANOSECONDS);
}
private void expire()
{
long start = Clock.Global.nanoTime();
int n = 0;
for (Map.Entry<TrackedRead.Id, TrackedLocalReadCoordinator> entry : reads.entrySet())
{
TrackedLocalReadCoordinator read = entry.getValue();
if (read.isTimedOutOrComplete(start))
{
read.abort();
if (reads.remove(entry.getKey(), entry.getValue()))
n++;
}
}
if (n > 0)
logger.trace("Expired {} entries", n);
}
private TrackedLocalReadCoordinator getOrCreate(TrackedRead.Id id)
{
return reads.computeIfAbsent(id, TrackedLocalReadCoordinator::new);
}
public void receiveSummary(InetAddressAndPort from, TrackedSummaryResponse summary)
{
getOrCreate(summary.readId()).receiveSummary(from, summary.summary());
}
public TrackedLocalReadCoordinator beginRead(TrackedRead.Id readId, ClusterMetadata metadata, ReadCommand command, ConsistencyLevel consistencyLevel, int[] summaryNodes, long expiresAtNanos)
{
Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id);
SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry;
ReplicaPlan.AbstractForRead<?, ?> replicaPlan;
if (command instanceof SinglePartitionReadCommand)
{
replicaPlan = ReplicaPlans.forRead(metadata,
keyspace,
((SinglePartitionReadCommand) command).partitionKey().getToken(),
command.indexQueryPlan(),
consistencyLevel,
retry);
}
else
{
// TODO: confirm range we're reading doesn't span multiple replica sets
replicaPlan = ReplicaPlans.forRangeRead(keyspace,
command.indexQueryPlan(),
consistencyLevel,
command.dataRange().keyRange(),
1);
}
// TODO: confirm all summaryNodes are present in the replica plan
TrackedLocalReadCoordinator coordinator = getOrCreate(readId);
coordinator.startLocalRead(readId, command, replicaPlan, summaryNodes, expiresAtNanos);
return coordinator;
}
public void acknowledgeSync(TrackedRead.Id readId, int syncId)
{
TrackedLocalReadCoordinator read = reads.get(readId);
if (read == null)
return;
if (read.acknowledgeSync(syncId))
reads.remove(readId);
}
public boolean receiveMutations(TrackedRead.Id readId, int syncId, List<Mutation> mutations)
{
TrackedLocalReadCoordinator read = reads.get(readId);
if (read == null)
return false;
read.receiveMutations(mutations);
if (read.acknowledgeSync(syncId))
reads.remove(readId);
return true;
}
public static long defaultExpirationInterval()
{
return DatabaseDescriptor.getMinRpcTimeout(NANOSECONDS) / 2;
}
@Override
public void shutdown()
{
executor.shutdown();
}
@Override
public boolean isTerminated()
{
return executor.isTerminated();
}
@Override
public Object shutdownNow()
{
return executor.shutdownNow();
}
public void shutdownBlocking() throws InterruptedException
{
if (executor == null || executor.isTerminated())
return;
executor.shutdown();
executor.awaitTermination(1, MINUTES);
}
@Override
public boolean awaitTermination(long timeout, TimeUnit units) throws InterruptedException
{
return executor.awaitTermination(timeout, units);
}
}

View File

@ -0,0 +1,520 @@
/*
* 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.service.reads.tracked;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.partitions.AbstractPartitionIterator;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.exceptions.ReadFailureException;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.locator.*;
import org.apache.cassandra.net.*;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.reads.SpeculativeRetryPolicy;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics;
public abstract class TrackedRead<E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> implements RequestCallback<TrackedDataResponse>
{
private static final Logger logger = LoggerFactory.getLogger(TrackedRead.class);
public static class Id
{
private static final int nodeId = ClusterMetadata.current().myNodeId().id();
private static final AtomicLong lastHlc = new AtomicLong();
private final int node;
private final long hlc;
public Id(int node, long hlc)
{
this.node = node;
this.hlc = hlc;
}
@Override
public boolean equals(Object o)
{
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
return node == id.node && hlc == id.hlc;
}
@Override
public int hashCode()
{
return Integer.hashCode(node) * 31 + Long.hashCode(hlc);
}
@Override
public String toString()
{
return "Id{" + node + ':' + hlc + '}';
}
public static final IVersionedSerializer<Id> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(Id id, DataOutputPlus out, int version) throws IOException
{
out.writeInt(id.node);
out.writeLong(id.hlc);
}
@Override
public Id deserialize(DataInputPlus in, int version) throws IOException
{
int node = in.readInt();
long hlc = in.readLong();
return new Id(node, hlc);
}
@Override
public long serializedSize(Id id, int version)
{
return TypeSizes.sizeof(id.node) + TypeSizes.sizeof(id.hlc);
}
};
public static Id nextId()
{
while (true)
{
long lastMicros = lastHlc.get();
long nextMicros = Math.max(lastMicros + 1, TimeUnit.MILLISECONDS.toMicros(Clock.Global.currentTimeMillis()));
if (lastHlc.compareAndSet(lastMicros, nextMicros))
return new Id(nodeId, nextMicros);
}
}
}
private final AsyncPromise<PartitionIterator> future = new AsyncPromise<>();
private final Id readId = Id.nextId();
private final ReadCommand command;
private final ReplicaPlan.AbstractForRead<E, P> replicaPlan;
private final ConsistencyLevel consistencyLevel;
private static class RequestFailure extends Throwable
{
private final InetAddressAndPort from;
private final RequestFailureReason reason;
public RequestFailure(InetAddressAndPort from, RequestFailureReason reason)
{
this.from = from;
this.reason = reason;
}
public Map<InetAddressAndPort, RequestFailureReason> reasonByEndpoint()
{
return Map.of(from, reason);
}
}
public TrackedRead(ReadCommand command, ReplicaPlan.AbstractForRead<E, P> replicaPlan, ConsistencyLevel consistencyLevel)
{
this.command = command;
this.replicaPlan = replicaPlan;
this.consistencyLevel = consistencyLevel;
}
@Override
public String toString()
{
return "TrackedRead." + getClass().getSimpleName() + '{' + readId + '}';
}
protected abstract Verb verb();
public static class Partition extends TrackedRead<EndpointsForToken, ReplicaPlan.ForTokenRead>
{
private Partition(SinglePartitionReadCommand command, ReplicaPlan.AbstractForRead<EndpointsForToken, ReplicaPlan.ForTokenRead> replicaPlan, ConsistencyLevel consistencyLevel)
{
super(command, replicaPlan, consistencyLevel);
}
public static Partition create(ClusterMetadata metadata, SinglePartitionReadCommand command, ConsistencyLevel consistencyLevel)
{
Preconditions.checkArgument(command.metadata().replicationType().isTracked());
Keyspace keyspace = Keyspace.open(command.metadata().keyspace);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(command.metadata().id);
SpeculativeRetryPolicy retry = cfs.metadata().params.speculativeRetry;
ReplicaPlan.ForTokenRead replicaPlan = ReplicaPlans.forRead(metadata,
keyspace,
cfs.getTableId(),
command.partitionKey().getToken(),
command.indexQueryPlan(),
consistencyLevel,
retry,
ReadCoordinator.DEFAULT);
return new Partition(command, replicaPlan, consistencyLevel);
}
@Override
protected Verb verb()
{
return Verb.TRACKED_PARTITION_READ_REQ;
}
}
public static class Range extends TrackedRead<EndpointsForRange, ReplicaPlan.ForRangeRead>
{
private Range(PartitionRangeReadCommand command, ReplicaPlan.AbstractForRead<EndpointsForRange, ReplicaPlan.ForRangeRead> replicaPlan, ConsistencyLevel consistencyLevel)
{
super(command, replicaPlan, consistencyLevel);
}
public static TrackedRead.Range create(PartitionRangeReadCommand command, ReplicaPlan.ForRangeRead replicaPlan)
{
Preconditions.checkArgument(command.metadata().replicationType().isTracked());
return new Range(command, replicaPlan, replicaPlan.consistencyLevel());
}
@Override
protected Verb verb()
{
return Verb.TRACKED_RANGE_READ_REQ;
}
}
private static <E extends Endpoints<E>> int[] endpointsToHostIds(E endpoints)
{
int[] hostids = new int[endpoints.size()];
int idx = 0;
ClusterMetadata metadata = ClusterMetadata.current();
for (Replica replica : endpoints)
hostids[idx++] = metadata.directory.peerId(replica.endpoint()).id();
return hostids;
}
public void start(long expiresAt)
{
// TODO: skip local coordination if this node knows its recovering from an outage
// TODO: read speculation
Replica localReplica = replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort());
if (localReplica != null)
readMetrics.localRequests.mark();
else
readMetrics.remoteRequests.mark();
// create an id
// select data node
// select summary nodes
E selected = replicaPlan.contacts();
Replica dataNode = localReplica != null && localReplica.isFull()
? localReplica
: Iterables.getOnlyElement(selected.filter(Replica::isFull, 1));
E summaryNodes = selected.filter(r -> r != dataNode);
int[] summaryHostIds = endpointsToHostIds(summaryNodes);
if (dataNode == localReplica)
{
Stage.READ.submit(() -> {
TrackedLocalReadCoordinator coordinator = MutationTrackingService.instance.localReads().beginRead(readId, ClusterMetadata.current(), command, consistencyLevel, summaryHostIds, expiresAt);
coordinator.addCallback((response, error) -> {
if (error != null)
{
// TODO: notify coordinator that read has failed
logger.error("Error while processing read", error);
return;
}
logger.trace("Finished locally coordinating {}", this);
onResponse(response);
});
});
}
else
{
DataRequest dataRequest = new DataRequest(readId, command, consistencyLevel, summaryHostIds);
Message<DataRequest> dataMessage = Message.outWithFlag(verb(), dataRequest, MessageFlag.CALL_BACK_ON_FAILURE);
MessagingService.instance().sendWithCallback(dataMessage, dataNode.endpoint(), this);
}
if (summaryNodes.isEmpty())
return;
SummaryRequest summaryRequest = new SummaryRequest(readId, command);
Message<SummaryRequest> summaryMessage = Message.outWithParam(Verb.TRACKED_SUMMARY_REQ,
summaryRequest,
ParamType.RESPOND_TO,
dataNode.endpoint());
for (Replica replica : summaryNodes)
{
if (localReplica == replica)
{
Stage.READ.submit(() -> summaryRequest.executeLocally(summaryMessage, ClusterMetadata.current()));
}
else
{
MessagingService.instance().send(summaryMessage, replica.endpoint());
}
}
}
public void start(Dispatcher.RequestTime requestTime)
{
start(requestTime.computeDeadline(verb().expiresAfterNanos()));
}
private void onResponse(TrackedDataResponse response)
{
future.trySuccess(response.makeIterator(command));
}
@Override
public void onResponse(Message<TrackedDataResponse> msg)
{
onResponse(msg.payload);
}
@Override
public void onFailure(InetAddressAndPort from, org.apache.cassandra.exceptions.RequestFailure failure)
{
future.tryFailure(new RequestFailure(from, failure.reason));
}
@Override
public boolean invokeOnFailure()
{
return true;
}
public Future<PartitionIterator> future()
{
return future;
}
public PartitionIterator awaitResults()
{
try
{
return future.get(command.getTimeout(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
}
catch (InterruptedException e)
{
throw new UncheckedInterruptedException(e);
}
catch (ExecutionException e)
{
Throwable ex = e.getCause();
Map<InetAddressAndPort, RequestFailureReason> reasons = Collections.emptyMap();
if (ex instanceof RequestFailure)
{
RequestFailure failure = (RequestFailure) ex;
if (failure.reason == RequestFailureReason.TIMEOUT)
{
throw new ReadTimeoutException(replicaPlan.consistencyLevel(), 0, replicaPlan.readQuorum(), false);
}
reasons = failure.reasonByEndpoint();
}
throw new ReadFailureException(replicaPlan.consistencyLevel(), 0, replicaPlan.readQuorum(), false, reasons);
}
catch (TimeoutException e)
{
throw new ReadTimeoutException(replicaPlan.consistencyLevel(), 0, replicaPlan.readQuorum(), false);
}
}
public PartitionIterator iterator()
{
return new AbstractPartitionIterator()
{
PartitionIterator result = null;
@Override
protected RowIterator computeNext()
{
if (result == null)
result = awaitResults();
if (!result.hasNext())
return endOfData();
return result.next();
}
};
}
public abstract static class Request
{
protected final Id readId;
protected final ReadCommand command;
protected Request(Id readId, ReadCommand command)
{
this.readId = readId;
this.command = command;
}
public abstract void executeLocally(Message<? extends Request> message, ClusterMetadata metadata);
}
public static class DataRequest extends Request
{
private final ConsistencyLevel consistencyLevel;
private final int[] summaryNodes;
public DataRequest(Id readId, ReadCommand command, ConsistencyLevel consistencyLevel, int[] summaryNodes)
{
super(readId, command);
this.consistencyLevel = consistencyLevel;
this.summaryNodes = summaryNodes;
}
@Override
public void executeLocally(Message<? extends Request> message, ClusterMetadata metadata)
{
TrackedLocalReadCoordinator coordinator = MutationTrackingService.instance.localReads().beginRead(readId, metadata, command, consistencyLevel, summaryNodes, message.expiresAtNanos());
coordinator.addCallback((response, error) -> {
if (error != null)
{
// TODO: notify coordinator that read has failed
logger.error("Error while processing read", error);
return;
}
MessagingService.instance().send(message.responseWith(response), message.from());
});
}
public static final IVersionedSerializer<DataRequest> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(DataRequest request, DataOutputPlus out, int version) throws IOException
{
Id.serializer.serialize(request.readId, out, version);
ReadCommand.serializer.serialize(request.command, out, version);
out.writeInt(request.consistencyLevel.code);
out.writeInt(request.summaryNodes.length);
for (int hostid : request.summaryNodes)
out.writeInt(hostid);
}
@Override
public DataRequest deserialize(DataInputPlus in, int version) throws IOException
{
Id readId = Id.serializer.deserialize(in, version);
ReadCommand command = ReadCommand.serializer.deserialize(in, version);
ConsistencyLevel consistencyLevel = ConsistencyLevel.fromCode(in.readInt());
int[] summaryNodes = new int[in.readInt()];
for (int i = 0; i < summaryNodes.length; i++)
summaryNodes[i] = in.readInt();
return new DataRequest(readId, command, consistencyLevel, summaryNodes);
}
@Override
public long serializedSize(DataRequest request, int version)
{
return Id.serializer.serializedSize(request.readId, version) +
ReadCommand.serializer.serializedSize(request.command, version) +
TypeSizes.sizeof(request.consistencyLevel.code) +
TypeSizes.sizeof(request.summaryNodes.length) +
((long) TypeSizes.INT_SIZE * request.summaryNodes.length);
}
};
}
public static class SummaryRequest extends Request
{
public SummaryRequest(Id readId, ReadCommand command)
{
super(readId, command);
}
@Override
public void executeLocally(Message<? extends Request> message, ClusterMetadata metadata)
{
MutationSummary summary = command.createMutationSummary(false);
TrackedSummaryResponse response = new TrackedSummaryResponse(readId, summary);
MessagingService.instance().send(message.responseWith(response), message.respondTo());
}
public static final IVersionedSerializer<SummaryRequest> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(SummaryRequest request, DataOutputPlus out, int version) throws IOException
{
Id.serializer.serialize(request.readId, out, version);
ReadCommand.serializer.serialize(request.command, out, version);
}
@Override
public SummaryRequest deserialize(DataInputPlus in, int version) throws IOException
{
Id readId = Id.serializer.deserialize(in, version);
ReadCommand command = ReadCommand.serializer.deserialize(in, version);
return new SummaryRequest(readId, command);
}
@Override
public long serializedSize(SummaryRequest request, int version)
{
return Id.serializer.serializedSize(request.readId, version) +
ReadCommand.serializer.serializedSize(request.command, version);
}
};
}
public static final IVerbHandler<Request> verbHandler = new AbstractReadCommandVerbHandler<>()
{
@Override
protected void performRead(Message<Request> message, ClusterMetadata metadata)
{
message.payload.executeLocally(message, metadata);
}
@Override
protected ReadCommand getCommand(Request payload)
{
return payload.command;
}
};
}

View File

@ -0,0 +1,77 @@
/*
* 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.service.reads.tracked;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.replication.MutationSummary;
import org.apache.cassandra.replication.MutationTrackingService;
import java.io.IOException;
public class TrackedSummaryResponse
{
private final TrackedRead.Id readId;
private final MutationSummary summary;
public TrackedSummaryResponse(TrackedRead.Id readId, MutationSummary summary)
{
this.readId = readId;
this.summary = summary;
}
public TrackedRead.Id readId()
{
return readId;
}
public MutationSummary summary()
{
return summary;
}
public static final IVerbHandler<TrackedSummaryResponse> verbHandler =
message -> MutationTrackingService.instance.localReads().receiveSummary(message.from(), message.payload);
public static final IVersionedSerializer<TrackedSummaryResponse> serializer = new IVersionedSerializer<>()
{
@Override
public void serialize(TrackedSummaryResponse summary, DataOutputPlus out, int version) throws IOException
{
TrackedRead.Id.serializer.serialize(summary.readId, out, version);
MutationSummary.serializer.serialize(summary.summary, out, version);
}
@Override
public TrackedSummaryResponse deserialize(DataInputPlus in, int version) throws IOException
{
TrackedRead.Id id = TrackedRead.Id.serializer.deserialize(in, version);
MutationSummary summary = MutationSummary.serializer.deserialize(in, version);
return new TrackedSummaryResponse(id, summary);
}
@Override
public long serializedSize(TrackedSummaryResponse summary, int version)
{
return TrackedRead.Id.serializer.serializedSize(summary.readId, version) +
MutationSummary.serializer.serializedSize(summary.summary, version);
}
};
}

View File

@ -1,84 +0,0 @@
/*
* 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.service.tracking;
public class MutationId
{
/**
* 4 byte TCM host id + 4 byte host log id packed into a long.
* Host log ID is unique within the host, allocated
* anew on host restart - one per token range replicated by the host,
* persisted on allocation, unique within the host.
*/
public final long logId;
/**
* 4 byte position + 4 byte timestamp packed into a long.
* Position is incremented, the timestamp is monotonically non-decreasing.
* The position is enough to identify the entry within a coordinator log,
* the timestamp is added for correlation purposes.
*/
public final long sequenceId;
MutationId(long logId, long sequenceId)
{
this.logId = logId;
this.sequenceId = sequenceId;
}
public int hostId()
{
return (int) (0xffffffffL & (logId >> 32));
}
public int hostLogId()
{
return (int) (0xffffffffL & logId);
}
public int position()
{
return (int) (0xffffffffL & (sequenceId >> 32));
}
public int timestamp()
{
return (int) (0xffffffffL & sequenceId);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (!(o instanceof MutationId)) return false;
MutationId that = (MutationId) o;
return this.logId == that.logId && this.sequenceId == that.sequenceId;
}
@Override
public int hashCode()
{
return Long.hashCode(logId) + 31 * Long.hashCode(sequenceId);
}
@Override
public String toString()
{
return "MutationId{" + logId + ", " + sequenceId + '}';
}
}

View File

@ -53,6 +53,7 @@ import org.apache.cassandra.gms.NewGossiper;
import org.apache.cassandra.gms.VersionedValue;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.DistributedSchema;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Keyspaces;
@ -158,7 +159,8 @@ import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
LocalLog.LogSpec logSpec = LocalLog.logSpec()
.withStorage(LogStorage.SystemKeyspace)
.afterReplay(Startup::scrubDataDirectories,
(metadata) -> StorageService.instance.registerMBeans())
(metadata) -> StorageService.instance.registerMBeans(),
MutationTrackingService.instance::start)
.withDefaultListeners();
ClusterMetadataService.setInstance(new ClusterMetadataService(new UniformRangePlacement(),
wrapProcessor,
@ -285,7 +287,8 @@ import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
LocalLog.LogSpec logSpec = LocalLog.logSpec()
.withInitialState(emptyFromSystemTables)
.afterReplay(Startup::scrubDataDirectories,
(metadata) -> StorageService.instance.registerMBeans())
(metadata) -> StorageService.instance.registerMBeans(),
MutationTrackingService.instance::start)
.withStorage(LogStorage.SystemKeyspace)
.withDefaultListeners();
@ -392,7 +395,8 @@ import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
ClusterMetadataService.unsetInstance();
LocalLog.LogSpec logSpec = LocalLog.logSpec()
.afterReplay(Startup::scrubDataDirectories,
(_metadata) -> StorageService.instance.registerMBeans())
(_metadata) -> StorageService.instance.registerMBeans(),
MutationTrackingService.instance::start)
.withPreviousState(prev)
.withInitialState(metadata)
.withStorage(LogStorage.SystemKeyspace)

View File

@ -36,7 +36,7 @@ import static org.apache.cassandra.db.TypeSizes.sizeofUnsignedVInt;
public class NodeVersion implements Comparable<NodeVersion>
{
public static final Serializer serializer = new Serializer();
public static final Version CURRENT_METADATA_VERSION = Version.V8;
public static final Version CURRENT_METADATA_VERSION = Version.V9;
public static final NodeVersion CURRENT = new NodeVersion(new CassandraVersion(FBUtilities.getReleaseVersionString()), CURRENT_METADATA_VERSION);
private static final CassandraVersion SINCE_VERSION = CassandraVersion.CASSANDRA_5_1;

View File

@ -95,7 +95,7 @@ public class CancelCMSReconfiguration implements Transformation
// Also update schema with the corrected params
KeyspaceMetadata keyspace = prev.schema.getKeyspaceMetadata(SchemaConstants.METADATA_KEYSPACE_NAME);
KeyspaceMetadata newKeyspace = keyspace.withSwapped(new KeyspaceParams(keyspace.params.durableWrites, fromPlacement, FastPathStrategy.simple()));
KeyspaceMetadata newKeyspace = keyspace.withSwapped(new KeyspaceParams(keyspace.params.durableWrites, fromPlacement, FastPathStrategy.simple(), keyspace.params.replicationType));
transformer = transformer.with(new DistributedSchema(prev.schema.getKeyspaces().withAddedOrUpdated(newKeyspace)));
}

View File

@ -79,6 +79,11 @@ public enum Version
*/
V8(8),
/**
* - MutationTracking
*/
V9(9),
UNKNOWN(Integer.MAX_VALUE);
/**

View File

@ -264,7 +264,7 @@ public abstract class PrepareCMSReconfiguration implements Transformation
// In a complex reconfiguration, in addition to initiating the sequence of membership changes,
// we're modifying the replication params of the metadata keyspace so we supply a function to do that
KeyspaceMetadata keyspace = prev.schema.getKeyspaceMetadata(SchemaConstants.METADATA_KEYSPACE_NAME);
KeyspaceMetadata newKeyspace = keyspace.withSwapped(new KeyspaceParams(keyspace.params.durableWrites, replicationParams, FastPathStrategy.simple()));
KeyspaceMetadata newKeyspace = keyspace.withSwapped(new KeyspaceParams(keyspace.params.durableWrites, replicationParams, FastPathStrategy.simple(), keyspace.params.replicationType));
return executeInternal(prev,
transformer -> transformer.with(prev.placements.replaceParams(prev.nextEpoch(), ReplicationParams.meta(prev), replicationParams))

View File

@ -126,6 +126,8 @@ import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.NoPayload;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.repair.autorepair.AutoRepair;
import org.apache.cassandra.replication.MutationJournal;
import org.apache.cassandra.replication.MutationTrackingService;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.service.ActiveRepairService;
@ -787,6 +789,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
CassandraDaemon.getInstanceForTesting().migrateSystemDataIfNeeded();
CommitLog.instance.start();
MutationJournal.instance.start();
SnapshotManager.instance.start(false);
SnapshotManager.instance.clearExpiredSnapshots();
@ -1055,6 +1058,8 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
// CommitLog must shut down after Stage, or threads from the latter may attempt to use the former.
// (ex. A Mutation stage thread may attempt to add a mutation to the CommitLog.)
error = parallelRun(error, executor, CommitLog.instance::shutdownBlocking);
error = parallelRun(error, executor, MutationJournal.instance::shutdownBlocking);
error = parallelRun(error, executor, MutationTrackingService.instance::shutdownBlocking);
error = parallelRun(error, executor,
() -> shutdownAndWait(Collections.singletonList(JMXBroadcastExecutor.executor))
);

View File

@ -18,56 +18,151 @@
package org.apache.cassandra.distributed.test;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.SimpleStatement;
import com.google.common.collect.Iterators;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInstanceConfig;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.serializers.SimpleDateSerializer;
import org.apache.cassandra.serializers.TimeSerializer;
import org.apache.cassandra.serializers.TimestampSerializer;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
@RunWith(Parameterized.class)
public class GroupByTest extends TestBaseImpl
{
@Parameterized.Parameter(0)
public ReplicationType replicationType;
@Parameterized.Parameters(name = "replication={0}")
public static Collection<Object[]> data()
{
List<Object[]> result = new ArrayList<>();
for (ReplicationType replication : ReplicationType.values())
result.add(new Object[]{replication});
return result;
}
private static Cluster cluster;
private static int nextKeyspaceId = 0;
@BeforeClass
public static void setupClass() throws Exception
{
Consumer<IInstanceConfig> config = cfg -> cfg.with(Feature.GOSSIP, NETWORK, NATIVE_PROTOCOL).set("user_defined_functions_enabled", "true");
cluster = Cluster.build().withNodes(3).withConfig(config).start();
}
@AfterClass
public static void tearDownClass() throws Exception
{
if (cluster != null)
cluster.close();
}
@Before
public void setup()
{
Assert.assertNotNull(cluster);
cluster.filters().reset();
KEYSPACE = "ks_" + nextKeyspaceId++;
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': " + cluster.size() + "} AND REPLICATION_TYPE = '" + replicationType.name() + "';");
}
private static void markNodeDown(IInvokableInstance instance, InetAddressAndPort ep)
{
instance.runOnInstance(() -> {
});
}
private static void awaitNodeDown(IInvokableInstance instance, InetAddressAndPort ep)
{
instance.runOnInstance(() -> {
long started = System.currentTimeMillis();
FailureDetector.instance.forceConviction(ep);
while (true)
{
long elapsed = System.currentTimeMillis() - started;
if (elapsed > TimeUnit.SECONDS.toMillis(30))
throw new AssertionError("Timed out waiting for node down - " + ep);
if (!FailureDetector.instance.isAlive(ep))
return;
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
throw new UncheckedInterruptedException(e);
}
}
});
}
private static void awaitNodeDown(IInvokableInstance instance, InetSocketAddress ep)
{
awaitNodeDown(instance, InetAddressAndPort.getByAddress(ep));
}
@Test
public void groupByWithDeletesAndSrpOnPartitions() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("user_defined_functions_enabled", "true")).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
initFunctions(cluster);
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, '1') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (2, '2') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck='1'"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=2 AND ck='2'"));
for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
initFunctions(cluster);
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, '1') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (2, '2') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck='1'"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=2 AND ck='2'"));
for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
for (int i = 1; i <= 4; i++)
{
String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
for (int i = 1; i <= 4; i++)
{
Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ConsistencyLevel.ALL, i);
assertRows(Iterators.toArray(rows, Object[].class));
}
Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ALL, i);
assertRows(Iterators.toArray(rows, Object[].class));
}
}
}
@ -75,26 +170,23 @@ public class GroupByTest extends TestBaseImpl
@Test
public void groupByWithDeletesAndSrpOnRows() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("user_defined_functions_enabled", "true")).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
initFunctions(cluster);
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '1') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '2') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='1'"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='2'"));
for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck text, PRIMARY KEY (pk, ck))"));
initFunctions(cluster);
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '1') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '2') USING TIMESTAMP 0"));
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='0'"));
cluster.get(2).executeInternal(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, '0') USING TIMESTAMP 0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='1'"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck='2'"));
for (String limitClause : new String[]{ "", "LIMIT 1", "LIMIT 10", "PER PARTITION LIMIT 1", "PER PARTITION LIMIT 10" })
String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
for (int i = 1; i <= 4; i++)
{
String query = withKeyspace("SELECT concat(ck) FROM %s.tbl GROUP BY pk " + limitClause);
for (int i = 1; i <= 4; i++)
{
Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ConsistencyLevel.ALL, i);
assertRows(Iterators.toArray(rows, Object[].class));
}
Iterator<Object[]> rows = cluster.coordinator(2).executeWithPaging(query, ALL, i);
assertRows(Iterators.toArray(rows, Object[].class));
}
}
}
@ -102,116 +194,107 @@ public class GroupByTest extends TestBaseImpl
@Test
public void testGroupByWithAggregatesAndPaging() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(2).withConfig((cfg) -> cfg.set("user_defined_functions_enabled", "true")).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, v1 text, v2 text, v3 text, primary key (pk, ck))"));
initFunctions(cluster);
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,1,'1','1','1')"), ALL);
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,2,'2','2','2')"), ALL);
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,3,'3','3','3')"), ALL);
for (int i = 1; i <= 4; i++)
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, v1 text, v2 text, v3 text, primary key (pk, ck))"));
initFunctions(cluster);
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk"),
ALL, i),
row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,1,'1','1','1')"), ConsistencyLevel.ALL);
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,2,'2','2','2')"), ConsistencyLevel.ALL);
cluster.coordinator(1).execute(withKeyspace("insert into %s.tbl (pk, ck, v1, v2, v3) values (1,3,'3','3','3')"), ConsistencyLevel.ALL);
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk limit 1"),
ALL, i),
row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
for (int i = 1; i <= 4; i++)
{
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk"),
ConsistencyLevel.ALL, i),
row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select concat(v1), concat(v2), concat(v3) from %s.tbl where pk = 1 group by pk limit 1"),
ConsistencyLevel.ALL, i),
row("_ 1 2 3", "_ 1 2 3", "_ 1 2 3"));
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select * from %s.tbl where pk = 1 group by pk"),
ConsistencyLevel.ALL, i),
row(1, 1, "1", "1", "1"));
}
assertRows(cluster.coordinator(1).executeWithPaging(withKeyspace("select * from %s.tbl where pk = 1 group by pk"),
ALL, i),
row(1, 1, "1", "1", "1"));
}
}
@Test
public void testGroupWithDeletesAndPaging() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(2).withConfig(cfg -> cfg.with(Feature.GOSSIP, NETWORK, NATIVE_PROTOCOL)).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, PRIMARY KEY (pk, ck))"));
ICoordinator coordinator = cluster.coordinator(1);
coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, 0)"), ALL);
coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, 1)"), ALL);
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck=0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck=1"));
String query = withKeyspace("SELECT * FROM %s.tbl GROUP BY pk");
Iterator<Object[]> rows = coordinator.executeWithPaging(query, ALL, 1);
assertRows(Iterators.toArray(rows, Object[].class));
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1").build();
Session session = c.connect())
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int, ck int, PRIMARY KEY (pk, ck))"));
ICoordinator coordinator = cluster.coordinator(1);
coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (0, 0)"), ConsistencyLevel.ALL);
coordinator.execute(withKeyspace("INSERT INTO %s.tbl (pk, ck) VALUES (1, 1)"), ConsistencyLevel.ALL);
cluster.get(1).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=0 AND ck=0"));
cluster.get(2).executeInternal(withKeyspace("DELETE FROM %s.tbl WHERE pk=1 AND ck=1"));
String query = withKeyspace("SELECT * FROM %s.tbl GROUP BY pk");
Iterator<Object[]> rows = coordinator.executeWithPaging(query, ConsistencyLevel.ALL, 1);
assertRows(Iterators.toArray(rows, Object[].class));
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder().addContactPoint("127.0.0.1").build();
Session session = c.connect())
{
SimpleStatement stmt = new SimpleStatement(withKeyspace("select * from %s.tbl where pk = 1 group by pk"));
stmt.setFetchSize(1);
Iterator<Row> rs = session.execute(stmt).iterator();
Assert.assertFalse(rs.hasNext());
}
SimpleStatement stmt = new SimpleStatement(withKeyspace("select * from %s.tbl where pk = 1 group by pk"));
stmt.setFetchSize(1);
Iterator<Row> rs = session.execute(stmt).iterator();
Assert.assertFalse(rs.hasNext());
}
}
@Test
public void testGroupByTimeRangesWithTimestampType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTimestamp (pk int, time timestamp, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:10:00 UTC', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:12:00 UTC', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:14:00 UTC', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:15:00 UTC', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:21:00 UTC', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:22:00 UTC', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:00 UTC', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:20 UTC', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:26:20 UTC', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:30:00 UTC', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTimestamp (pk int, time timestamp, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:10:00 UTC', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:12:00 UTC', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:14:00 UTC', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:15:00 UTC', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:21:00 UTC', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:22:00 UTC', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:00 UTC', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (1, '2016-09-27 16:26:20 UTC', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:26:20 UTC', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTimestamp (pk, time, v) VALUES (2, '2016-09-27 16:30:00 UTC', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
for (String startingTime : new String[] {"", ", '2016-09-27 UTC'"} )
{
for (String startingTime : new String[] {"", ", '2016-09-27 UTC'"} )
{
String stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
String stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L),
row(2, toTimestamp("2016-09-27 16:30:00 UTC"), 11, 11, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp GROUP BY pk, floor(time, 5m" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L),
row(2, toTimestamp("2016-09-27 16:25:00 UTC"), 10, 10, 1L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L),
row(1, toTimestamp("2016-09-27 16:15:00 UTC"), 4, 4, 1L),
row(1, toTimestamp("2016-09-27 16:10:00 UTC"), 1, 3, 3L));
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
stmt = "SELECT pk, floor(time, 5m" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithTimestamp WHERE pk = 1 GROUP BY pk, floor(time, 5m" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toTimestamp("2016-09-27 16:25:00 UTC"), 7, 8, 2L),
row(1, toTimestamp("2016-09-27 16:20:00 UTC"), 5, 6, 2L));
}
}
}
@ -219,60 +302,56 @@ public class GroupByTest extends TestBaseImpl
@Test
public void testGroupByTimeRangesWithDateType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithDate (pk int, time date, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-27', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-28', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-29', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-30', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-01', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-04', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-20', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-11-27', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-01', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-02', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithDate (pk int, time date, v int, primary key (pk, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-27', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-28', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-29', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-09-30', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-01', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-04', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-10-20', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (1, '2016-11-27', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-01', 10)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithDate (pk, time, v) VALUES (2, '2016-11-02', 11)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
for (String startingTime : new String[] {"", ", '2016-06-01'"} )
{
for (String startingTime : new String[] {"", ", '2016-06-01'"} )
{
String stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
String stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ")";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate GROUP BY pk, floor(time, 1mo" + startingTime + ") PER PARTITION LIMIT 1";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-01"), 1, 4, 4L),
row(2, toLocalDate("2016-11-01"), 10, 11, 2L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L),
row(1, toLocalDate("2016-09-01"), 1, 4, 4L));
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
}
stmt = "SELECT pk, floor(time, 1mo" + startingTime + "), min(v), max(v), count(v) FROM %s.testWithDate WHERE pk = 1 GROUP BY pk, floor(time, 1mo" + startingTime + ") ORDER BY time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-11-01"), 8, 8, 1L),
row(1, toLocalDate("2016-10-01"), 5, 7, 3L));
}
}
}
@ -280,55 +359,51 @@ public class GroupByTest extends TestBaseImpl
@Test
public void testGroupByTimeRangesWithTimeType() throws Throwable
{
try (Cluster cluster = init(builder().withNodes(3).start()))
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTime (pk int, date date, time time, v int, primary key (pk, date, time))"));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:10:00', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:12:00', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:14:00', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:15:00', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:21:00', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:22:00', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:00', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:20', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:20', 9)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:30', 10)"), ConsistencyLevel.QUORUM);
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.testWithTime (pk int, date date, time time, v int, primary key (pk, date, time))"));
String stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m)";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L));
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:10:00', 1)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:12:00', 2)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:14:00', 3)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:15:00', 4)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:21:00', 5)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:22:00', 6)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:00', 7)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-27', '16:26:20', 8)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:20', 9)"), ConsistencyLevel.QUORUM);
cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.testWithTime (pk, date, time, v) VALUES (1, '2016-09-28', '16:26:30', 10)"), ConsistencyLevel.QUORUM);
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m) LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L));
for (int pageSize : new int[] {2, 3, 4, 5, 7, 10})
{
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L));
String stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m)";
Iterator<Object[]> pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime GROUP BY pk, date, floor(time, 5m) LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:20:00"), 5, 6, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:15:00"), 4, 4, 1L),
row(1, toLocalDate("2016-09-27"), toTime("16:10:00"), 1, 3, 3L));
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L));
}
stmt = "SELECT pk, date, floor(time, 5m), min(v), max(v), count(v) FROM %s.testWithTime WHERE pk = 1 GROUP BY pk, date, floor(time, 5m) ORDER BY date DESC, time DESC LIMIT 2";
pagingRows = cluster.coordinator(1).executeWithPaging(withKeyspace(stmt), QUORUM, pageSize);
assertRows(pagingRows,
row(1, toLocalDate("2016-09-28"), toTime("16:25:00"), 9, 10, 2L),
row(1, toLocalDate("2016-09-27"), toTime("16:25:00"), 7, 8, 2L));
}
}

View File

@ -31,6 +31,7 @@ import org.junit.runners.Parameterized;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.shared.AssertUtils;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
@ -70,15 +71,19 @@ public abstract class ReadRepairEmptyRangeTombstonesTestBase extends TestBaseImp
@Parameterized.Parameter(3)
public boolean reverse;
@Parameterized.Parameters(name = "{index}: strategy={0} coordinator={1} paging={2} reverse={3}")
@Parameterized.Parameter(4)
public ReplicationType replicationType;
@Parameterized.Parameters(name = "{index}: strategy={0} coordinator={1} paging={2} reverse={3} replication={4}")
public static Collection<Object[]> data()
{
List<Object[]> result = new ArrayList<>();
for (int coordinator = 1; coordinator <= NUM_NODES; coordinator++)
for (boolean paging : BOOLEANS)
for (boolean reverse : BOOLEANS)
result.add(new Object[]{ ReadRepairStrategy.BLOCKING, coordinator, paging, reverse });
result.add(new Object[]{ ReadRepairStrategy.NONE, 1, false, false });
for (ReplicationType replication : ReplicationType.fixmeValues())
result.add(new Object[]{ ReadRepairStrategy.BLOCKING, coordinator, paging, reverse, replication });
result.add(new Object[]{ ReadRepairStrategy.NONE, 1, false, false, ReplicationType.untracked });
return result;
}
@ -250,14 +255,14 @@ public abstract class ReadRepairEmptyRangeTombstonesTestBase extends TestBaseImp
private Tester tester()
{
return new Tester(cluster, strategy, coordinator, flush(), paging, reverse);
return new Tester(cluster, strategy, coordinator, flush(), paging, reverse, replicationType);
}
private static class Tester extends ReadRepairTester<Tester>
{
private Tester(Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging, boolean reverse)
private Tester(Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging, boolean reverse, ReplicationType replicationType)
{
super(cluster, strategy, coordinator, flush, paging, reverse);
super(cluster, strategy, coordinator, flush, paging, reverse, replicationType);
}
@Override

View File

@ -18,8 +18,11 @@
package org.apache.cassandra.distributed.test;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
/**
@ -27,6 +30,13 @@ import static org.apache.cassandra.distributed.shared.AssertUtils.row;
*/
public class ReadRepairInQueriesTest extends ReadRepairQueryTester
{
@Before
public void setup()
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "Fix read repair for logged IN queries");
}
/**
* Test queries with an IN restriction on a table without clustering columns.
*/

View File

@ -29,6 +29,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
import static org.apache.cassandra.distributed.shared.AssertUtils.assertEquals;
@ -97,15 +98,19 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
@Parameterized.Parameter(3)
public boolean paging;
@Parameterized.Parameters(name = "{index}: strategy={0} coordinator={1} flush={2} paging={3}")
@Parameterized.Parameter(4)
public ReplicationType replicationType;
@Parameterized.Parameters(name = "{index}: strategy={0} coordinator={1} flush={2} paging={3} replication={4}")
public static Collection<Object[]> data()
{
List<Object[]> result = new ArrayList<>();
for (int coordinator = 1; coordinator <= NUM_NODES; coordinator++)
for (boolean flush : BOOLEANS)
for (boolean paging : BOOLEANS)
result.add(new Object[]{ ReadRepairStrategy.BLOCKING, coordinator, flush, paging });
result.add(new Object[]{ ReadRepairStrategy.NONE, 1, false, false });
for (ReplicationType replication : ReplicationType.values())
result.add(new Object[]{ ReadRepairStrategy.BLOCKING, coordinator, flush, paging, replication });
result.add(new Object[]{ ReadRepairStrategy.NONE, 1, false, false, ReplicationType.untracked });
return result;
}
@ -118,7 +123,6 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
.withConfig(config -> config.set("read_request_timeout", "1m")
.set("write_request_timeout", "1m"))
.start());
cluster.schemaChange(withKeyspace("CREATE TYPE %s.udt (x int, y int)"));
}
@AfterClass
@ -130,7 +134,7 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
protected Tester tester(String restriction)
{
return new Tester(restriction, cluster, strategy, coordinator, flush, paging);
return new Tester(restriction, cluster, strategy, coordinator, flush, paging, replicationType);
}
protected static class Tester extends ReadRepairTester<Tester>
@ -138,9 +142,9 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
private final String restriction; // the tested CQL query WHERE restriction
private final String allColumnsQuery; // a SELECT * query for the table using the tested restriction
Tester(String restriction, Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging)
Tester(String restriction, Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging, ReplicationType replicationType)
{
super(cluster, strategy, coordinator, flush, paging, false);
super(cluster, strategy, coordinator, flush, paging, false, replicationType);
this.restriction = restriction;
allColumnsQuery = String.format("SELECT * FROM %s %s", qualifiedTableName, restriction);
@ -173,6 +177,32 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
{
// query only the selected columns with CL=ALL to trigger partial read repair on that column
String columnsQuery = String.format("SELECT %s FROM %s %s", columns, qualifiedTableName, restriction);
if (replicationType.isTracked())
{
// for tracked replication, entire mutations will be replicated, so unlike untracked read repair we'd
// expect the node that missed writes to be completely up to date with the node that was last written to.
// So here we adjust expected rows for tracked replication
switch (lastMutatedNode)
{
case 1:
node2Rows = node1Rows;
break;
case 2:
node1Rows = node2Rows;
break;
default:
throw new AssertionError("Unhandled lastMutatedNode value: " + lastMutatedNode);
}
// we also expect all pending mutations to be reconciled in the initial read, and none to be reconciled on the verification step
columnsQueryRepairedRows = Math.max(columnsQueryRepairedRows, rowsQueryRepairedRows);
rowsQueryRepairedRows = 0;
// and all missing mutations should be reconciled as part of a single reconciliation, not one per-partition
columnsQueryRepairedRows = Math.min(columnsQueryRepairedRows, 1);
}
assertRowsDistributed(columnsQuery, columnsQueryRepairedRows, columnsQueryResults);
// query entire rows to repair the rest of the columns, that might trigger new repairs for those columns
@ -223,6 +253,12 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
Tester deleteRows(String rowDeletion, long repairedRows, Object[][] node1Rows, Object[][] node2Rows)
{
mutate(1, rowDeletion);
// for range reads we still expect all missing mutations to be reconciled as part
// of a single reconciliation operation
if (replicationType.isTracked())
repairedRows = Math.min(repairedRows, 1);
return verifyQuery(allColumnsQuery, repairedRows, node1Rows, node2Rows);
}
@ -259,9 +295,32 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
* Verifies the final status of the nodes with an unrestricted query, to ensure that the main tested query
* hasn't triggered any unexpected repairs. Then, it verifies that the node that hasn't been used as coordinator
* hasn't triggered any unexpected repairs. Finally, it drops the table.
*
* The expectUnrepaired flag is meant for range query tests where logged replication table special casing
* doesn't apply since we do expect the final query to find and repair missing mutations
*/
void tearDown(long repairedRows, Object[][] node1Rows, Object[][] node2Rows)
void tearDown(long repairedRows, Object[][] node1Rows, Object[][] node2Rows, boolean expectUnrepaired)
{
if (replicationType.isTracked() && !expectUnrepaired)
{
// for tracked replication, entire mutations will be replicated, so unlike untracked read repair we'd
// expect the node that missed writes to be completely up to date with the node that was last written to.
// So here we adjust expected rows for tracked replication
switch (lastMutatedNode)
{
case 1:
node2Rows = node1Rows;
break;
case 2:
node1Rows = node2Rows;
break;
default:
throw new AssertionError("Unhandled lastMutatedNode value: " + lastMutatedNode);
}
// we also expect all pending mutations to be reconciled in the initial read, and none to be reconciled on the verification step
repairedRows = 0;
}
verifyQuery("SELECT * FROM " + qualifiedTableName, repairedRows, node1Rows, node2Rows);
for (int n = 1; n <= cluster.size(); n++)
{
@ -275,5 +334,10 @@ public abstract class ReadRepairQueryTester extends TestBaseImpl
}
schemaChange("DROP TABLE " + qualifiedTableName);
}
void tearDown(long repairedRows, Object[][] node1Rows, Object[][] node2Rows)
{
tearDown(repairedRows, node1Rows, node2Rows, false);
}
}
}

View File

@ -20,10 +20,13 @@ package org.apache.cassandra.distributed.test;
import org.junit.Test;
import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
/**
* {@link ReadRepairQueryTester} for range queries.
*
*/
public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
{
@ -51,7 +54,8 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
rows(row(2, null, 200), row(3, 30, 300)))
.tearDown(1,
rows(row(1, 10, 100), row(2, null, 200)),
rows(row(2, null, 200)));
rows(row(2, null, 200)),
true);
}
/**
@ -84,7 +88,8 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
rows(row(2, 20, 200, 2000), row(3, 30, 300, 3000)))
.tearDown(1,
rows(row(1, 10, 100, 1000), row(3, 30, 300, 3000)),
rows(row(3, 30, 300, 3000)));
rows(row(3, 30, 300, 3000)),
true);
}
/**
@ -151,6 +156,7 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
@Test
public void testRangeQueryWithFilterOnSelectedColumnOnSkinnyTable()
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "uses ALLOW FILTERING (CASSANDRA-20555)");
tester("WHERE a=2 ALLOW FILTERING")
.createTable("CREATE TABLE %s (k int PRIMARY KEY, a int, b int)")
.mutate("INSERT INTO %s (k, a, b) VALUES (1, 2, 3)",
@ -177,6 +183,7 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
@Test
public void testRangeQueryWithFilterOnSelectedColumnOnWideTable()
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "uses ALLOW FILTERING (CASSANDRA-20555)");
tester("WHERE a=1 ALLOW FILTERING")
.createTable("CREATE TABLE %s (k int, c int, a int, b int, PRIMARY KEY(k, c))")
.mutate("INSERT INTO %s (k, c, a, b) VALUES (1, 1, 1, 1)",
@ -208,6 +215,8 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
@Test
public void testRangeQueryWithFilterOnUnselectedColumnOnSkinnyTable()
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "uses ALLOW FILTERING (CASSANDRA-20555)");
tester("WHERE b=3 ALLOW FILTERING")
.createTable("CREATE TABLE %s (k int PRIMARY KEY, a int, b int)")
.mutate("INSERT INTO %s (k, a, b) VALUES (1, 2, 3)",
@ -234,6 +243,8 @@ public class ReadRepairRangeQueriesTest extends ReadRepairQueryTester
@Test
public void testRangeQueryWithFilterOnUnselectedColumnOnWideTable()
{
if (coordinator == 2)
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "Depends on ALLOW FILTERING");
tester("WHERE b=2 ALLOW FILTERING")
.createTable("CREATE TABLE %s (k int, c int, a int, b int, PRIMARY KEY(k, c))")
.mutate("INSERT INTO %s (k, c, a, b) VALUES (1, 1, 1, 1)",

View File

@ -20,6 +20,8 @@ package org.apache.cassandra.distributed.test;
import org.junit.Test;
import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
/**
@ -62,6 +64,7 @@ public class ReadRepairSliceQueriesTest extends ReadRepairQueryTester
@Test
public void testSliceQueryWithFilter()
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType, "Depends on ALLOW FILTERING");
tester("WHERE k=0 AND a>10 AND a<40 ALLOW FILTERING")
.createTable("CREATE TABLE %s (k int, c int, a int, b int, PRIMARY KEY(k, c))")
.mutate("INSERT INTO %s (k, c, a, b) VALUES (0, 1, 10, 100)",

View File

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -57,14 +58,17 @@ import org.apache.cassandra.distributed.api.IMessageFilters.Filter;
import org.apache.cassandra.distributed.api.TokenSupplier;
import org.apache.cassandra.distributed.shared.NetworkTopology;
import org.apache.cassandra.distributed.test.accord.AccordTestBase;
import org.apache.cassandra.distributed.test.tracking.MutationTrackingUtils;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.reads.repair.BlockingReadRepair;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
import org.apache.cassandra.utils.concurrent.Condition;
import org.apache.cassandra.utils.concurrent.Future;
import static java.lang.String.format;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT;
import static org.apache.cassandra.db.Keyspace.open;
@ -73,6 +77,9 @@ import static org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM;
import static org.apache.cassandra.distributed.shared.AssertUtils.assertEquals;
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
import static org.apache.cassandra.net.Verb.READ_RECONCILE_NOTIFY;
import static org.apache.cassandra.net.Verb.READ_RECONCILE_RCV;
import static org.apache.cassandra.net.Verb.READ_RECONCILE_SEND;
import static org.apache.cassandra.net.Verb.READ_REPAIR_REQ;
import static org.apache.cassandra.net.Verb.READ_REPAIR_RSP;
import static org.apache.cassandra.net.Verb.READ_REQ;
@ -81,20 +88,32 @@ import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeConditio
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ReadRepairTest extends TestBaseImpl
public abstract class ReadRepairTestBase extends TestBaseImpl
{
private static int tableNum = 0;
private String tableName;
protected abstract ReplicationType replicationType();
private void incrementTableName()
{
tableName = "tbl" + tableNum++;
}
private static final AtomicInteger keyspaceIdx = new AtomicInteger();
private String keyspaceName;
private String tableName;
private String qualifiedTableName;
@Before
public void setup()
{
incrementTableName();
keyspaceName = "ks_" + keyspaceIdx.getAndIncrement();
tableName = "tbl";
qualifiedTableName = keyspaceName + '.' + tableName;
}
private void createKeyspace(Cluster cluster)
{
cluster.schemaChange(format("CREATE KEYSPACE %s WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor':%s} AND REPLICATION_TYPE='%s';",
keyspaceName, cluster.size(), replicationType()));
}
private String withTable(String fmt)
{
return format(fmt, qualifiedTableName);
}
/**
@ -104,34 +123,32 @@ public class ReadRepairTest extends TestBaseImpl
public void testBlockingReadRepair() throws Throwable
{
testReadRepair(ReadRepairStrategy.BLOCKING, false);
incrementTableName();
testReadRepair(ReadRepairStrategy.BLOCKING, true);
}
/**
*
* Tests basic behaviour of read repair with {@code NONE} read repair strategy.
*/
@Test
public void testNoneReadRepair() throws Throwable
{
testReadRepair(ReadRepairStrategy.NONE);
}
private void testReadRepair(ReadRepairStrategy strategy) throws Throwable
@Test
public void testBlockingReadRepairAccord() throws Throwable
{
testReadRepair(ReadRepairStrategy.BLOCKING, true);
}
protected void testReadRepair(ReadRepairStrategy strategy) throws Throwable
{
testReadRepair(strategy, false);
}
private void testReadRepair(ReadRepairStrategy strategy, boolean brrThroughAccord) throws Throwable {
try (Cluster cluster = init(Cluster.create(3, c -> c.with(Feature.GOSSIP, Feature.NETWORK)))) {
private void testReadRepair(ReadRepairStrategy strategy, boolean brrThroughAccord) throws Throwable
{
try (Cluster cluster = init(Cluster.create(3, c -> c.with(Feature.GOSSIP, Feature.NETWORK))))
{
createKeyspace(cluster);
TransactionalMode transactionalMode = brrThroughAccord ? TransactionalMode.test_unsafe_writes : TransactionalMode.off;
cluster.schemaChange(withKeyspace("CREATE TABLE %s." + tableName + " (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode='" + transactionalMode.toString().toLowerCase() + '\'' +
String.format(" AND read_repair='%s'", strategy)));
cluster.schemaChange(withTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode='" + transactionalMode.toString().toLowerCase() + '\'' +
format(" AND read_repair='%s'", strategy)));
AccordTestBase.ensureTableIsAccordManaged(cluster, KEYSPACE, "t");
Object[] row = row(1, 1, 1);
String insertQuery = withKeyspace("INSERT INTO %s." + tableName + " (k, c, v) VALUES (?, ?, ?)");
String selectQuery = withKeyspace("SELECT * FROM %s." + tableName + " WHERE k=1");
String insertQuery = withTable("INSERT INTO %s (k, c, v) VALUES (?, ?, ?)");
String selectQuery = withTable("SELECT * FROM %s WHERE k=1");
// insert data in two nodes, simulating a quorum write that has missed one node
cluster.get(1).executeInternal(insertQuery, row);
@ -159,16 +176,17 @@ public class ReadRepairTest extends TestBaseImpl
{
try (Cluster cluster = init(Cluster.create(3, c -> c.with(Feature.GOSSIP, Feature.NETWORK)))) {
final long reducedReadTimeout = 3000L;
createKeyspace(cluster);
cluster.forEach(i -> i.runOnInstance(() -> DatabaseDescriptor.setReadRpcTimeout(reducedReadTimeout)));
cluster.schemaChange("CREATE TABLE " + KEYSPACE + "." + tableName + " (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'");
cluster.get(1).executeInternal("INSERT INTO " + KEYSPACE + "." + tableName + " (pk, ck, v) VALUES (1, 1, 1)");
cluster.get(2).executeInternal("INSERT INTO " + KEYSPACE + "." + tableName + " (pk, ck, v) VALUES (1, 1, 1)");
assertRows(cluster.get(3).executeInternal("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1"));
cluster.verbs(READ_REPAIR_RSP).to(1).drop();
cluster.schemaChange(withTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'"));
cluster.get(1).executeInternal(withTable("INSERT INTO %s (pk, ck, v) VALUES (1, 1, 1)"));
cluster.get(2).executeInternal(withTable("INSERT INTO %s (pk, ck, v) VALUES (1, 1, 1)"));
assertRows(cluster.get(3).executeInternal(withTable("SELECT * FROM %s WHERE pk = 1")));
cluster.verbs(replicationType().isTracked() ? READ_RECONCILE_NOTIFY : READ_REPAIR_RSP).to(1).drop();
final long start = currentTimeMillis();
try
{
cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1", ConsistencyLevel.ALL);
cluster.coordinator(1).execute(withTable("SELECT * FROM %s WHERE pk = 1"), ConsistencyLevel.ALL);
fail("Read timeout expected but it did not occur");
}
catch (Exception ex)
@ -181,7 +199,7 @@ public class ReadRepairTest extends TestBaseImpl
assertTrue(actualTimeTaken > reducedReadTimeout);
// But it should not exceed too much
assertTrue(actualTimeTaken < reducedReadTimeout + magicDelayAmount);
assertRows(cluster.get(3).executeInternal("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1"),
assertRows(cluster.get(3).executeInternal(withTable("SELECT * FROM %s WHERE pk = 1")),
row(1, 1, 1)); // the partition happened when the repaired node sending back ack. The mutation should be in fact applied.
}
}
@ -192,33 +210,37 @@ public class ReadRepairTest extends TestBaseImpl
{
try (Cluster cluster = init(builder().withNodes(3).start()))
{
cluster.schemaChange("CREATE TABLE " + KEYSPACE + "." + tableName + " (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'");
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'"));
for (int i = 1 ; i <= 2 ; ++i)
cluster.get(i).executeInternal("INSERT INTO " + KEYSPACE + "." + tableName + " (pk, ck, v) VALUES (1, 1, 1)");
cluster.get(i).executeInternal(withTable("INSERT INTO %s (pk, ck, v) VALUES (1, 1, 1)"));
assertRows(cluster.get(3).executeInternal("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1"));
assertRows(cluster.get(3).executeInternal(withTable("SELECT * FROM %s WHERE pk = 1")));
cluster.filters().verbs(READ_REPAIR_REQ.id).to(3).drop();
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1",
ConsistencyLevel.QUORUM),
cluster.filters().verbs(READ_RECONCILE_SEND.id).to(3).drop();
cluster.filters().verbs(READ_RECONCILE_RCV.id).to(3).drop();
assertRows(cluster.coordinator(1).execute(withTable("SELECT * FROM %s WHERE pk = 1"), ConsistencyLevel.QUORUM),
row(1, 1, 1));
// Data was not repaired
assertRows(cluster.get(3).executeInternal("SELECT * FROM " + KEYSPACE + "." + tableName + " WHERE pk = 1"));
assertRows(cluster.get(3).executeInternal(withTable("SELECT * FROM %s WHERE pk = 1")));
}
}
@Test @Ignore
public void movingTokenReadRepairTest() throws Throwable
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType(), "Token moves not supported");
// TODO: rewrite using FuzzTestBase to control progress through decommission
// TODO: fails with vnode enabled
try (Cluster cluster = init(Cluster.build(4).withoutVNodes().start(), 3))
{
List<Token> tokens = cluster.tokens();
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'");
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (pk int, ck int, v int, PRIMARY KEY (pk, ck)) WITH read_repair='blocking'"));
int i = 0;
while (true)
@ -231,7 +253,7 @@ public class ReadRepairTest extends TestBaseImpl
}
// write only to #4
cluster.get(4).executeInternal("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, 1, 1)", i);
cluster.get(4).executeInternal(withTable("INSERT INTO %s (pk, ck, v) VALUES (?, 1, 1)"), i);
// mark #2 as leaving in #4
// cluster.get(4).acceptsOnInstance((InetSocketAddress endpoint) -> {
//// StorageService.instance.getTokenMetadata().addLeavingEndpoint(InetAddressAndPort.getByAddressOverrideDefaults(endpoint.getAddress(), endpoint.getPort()));
@ -244,13 +266,22 @@ public class ReadRepairTest extends TestBaseImpl
// (as a speculative repair in this case, as we prefer to send repair mutations to the initial
// set of read replicas, which are 2 and 3 here).
cluster.filters().verbs(READ_REQ.id).from(4).to(3).drop();
cluster.filters().verbs(READ_REPAIR_REQ.id).from(4).to(3).drop();
assertRows(cluster.coordinator(4).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = ?",
if (replicationType().isTracked())
{
cluster.filters().verbs(READ_RECONCILE_SEND.id).from(4).to(3).drop();
cluster.filters().verbs(READ_RECONCILE_RCV.id).from(4).to(3).drop();
}
else
{
cluster.filters().verbs(READ_REPAIR_REQ.id).from(4).to(3).drop();
}
assertRows(cluster.coordinator(4).execute(withTable("SELECT * FROM %s WHERE pk = ?"),
ConsistencyLevel.QUORUM, i),
row(i, 1, 1));
// verify that #1 receives the write
assertRows(cluster.get(1).executeInternal("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = ?", i),
assertRows(cluster.get(1).executeInternal(withTable("SELECT * FROM %s WHERE pk = ?"), i),
row(i, 1, 1));
}
}
@ -262,22 +293,25 @@ public class ReadRepairTest extends TestBaseImpl
@Test
public void alterRFAndRunReadRepair() throws Throwable
{
MutationTrackingUtils.fixmeSkipIfTracked(replicationType(), "RF changes not supported");
try (Cluster cluster = builder().withNodes(2).start())
{
cluster.schemaChange(withKeyspace("CREATE KEYSPACE %s WITH replication = " +
"{'class': 'SimpleStrategy', 'replication_factor': 1}"));
cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int PRIMARY KEY, a int, b int)" +
cluster.schemaChange(format("CREATE KEYSPACE %s WITH replication = " +
"{'class': 'SimpleStrategy', 'replication_factor': 1} " +
"AND replication_type='%s'",
keyspaceName, replicationType().toString()));
cluster.schemaChange(withTable("CREATE TABLE %s (k int PRIMARY KEY, a int, b int)" +
" WITH read_repair='blocking'"));
// insert a row that will only get to one node due to the RF=1
Object[] row = row(1, 1, 1);
cluster.get(1).executeInternal(withKeyspace("INSERT INTO %s.t (k, a, b) VALUES (?, ?, ?)"), row);
cluster.get(1).executeInternal(withTable("INSERT INTO %s (k, a, b) VALUES (?, ?, ?)"), row);
// flush to ensure reads come from sstables
cluster.get(1).flush(KEYSPACE);
cluster.get(1).flush(keyspaceName);
// at RF=1 it shouldn't matter which node we query, as the data should always come from the only replica
String query = withKeyspace("SELECT * FROM %s.t WHERE k = 1");
String query = withTable("SELECT * FROM %s WHERE k = 1");
for (int i = 1; i <= cluster.size(); i++)
assertRows(cluster.coordinator(i).execute(query, ALL), row);
@ -287,8 +321,8 @@ public class ReadRepairTest extends TestBaseImpl
// alter RF
ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT.setBoolean(true);
cluster.schemaChange(withKeyspace("ALTER KEYSPACE %s WITH replication = " +
"{'class': 'SimpleStrategy', 'replication_factor': 2}"));
cluster.schemaChange(format("ALTER KEYSPACE %s WITH replication = " +
"{'class': 'SimpleStrategy', 'replication_factor': 2}", keyspaceName));
// altering the RF shouldn't have triggered any read repair
assertRows(cluster.get(1).executeInternal(query), row);
@ -324,12 +358,13 @@ public class ReadRepairTest extends TestBaseImpl
{
try (Cluster cluster = init(Cluster.create(2)))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int, c int, v int, PRIMARY KEY(k, c))"));
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (k int, c int, v int, PRIMARY KEY(k, c))"));
ICoordinator coordinator = cluster.coordinator(1);
// insert some rows in all nodes
String insertQuery = withKeyspace("INSERT INTO %s.t (k, c, v) VALUES (?, ?, ?)");
String insertQuery = withTable("INSERT INTO %s (k, c, v) VALUES (?, ?, ?)");
for (int k = 0; k < 10; k++)
{
for (int c = 0; c < 10; c++)
@ -337,14 +372,14 @@ public class ReadRepairTest extends TestBaseImpl
}
// delete a subset of the inserted partitions, plus some others that don't exist
String deletePartitionQuery = withKeyspace("DELETE FROM %s.t WHERE k = ?");
String deletePartitionQuery = withTable("DELETE FROM %s WHERE k = ?");
for (int k = 5; k < 15; k++)
{
coordinator.execute(deletePartitionQuery, ALL, k);
}
// delete some of the rows of some of the partitions, including deleted and not deleted partitions
String deleteRowQuery = withKeyspace("DELETE FROM %s.t WHERE k = ? AND c = ?");
String deleteRowQuery = withTable("DELETE FROM %s WHERE k = ? AND c = ?");
for (int k = 2; k < 7; k++)
{
for (int c = 0; c < 5; c++)
@ -362,22 +397,22 @@ public class ReadRepairTest extends TestBaseImpl
if (flush)
{
for (int n = 1; n <= cluster.size(); n++)
cluster.get(n).flush(KEYSPACE);
cluster.get(n).flush(keyspaceName);
}
// run a bunch of queries verifying that they don't trigger read repair
coordinator.execute(withKeyspace("SELECT * FROM %s.t LIMIT 100"), QUORUM);
coordinator.execute(withTable("SELECT * FROM %s LIMIT 100"), QUORUM);
for (int k = 0; k < 15; k++)
{
coordinator.execute(withKeyspace("SELECT * FROM %s.t WHERE k=?"), QUORUM, k);
coordinator.execute(withTable("SELECT * FROM %s WHERE k=?"), QUORUM, k);
for (int c = 0; c < 10; c++)
{
coordinator.execute(withKeyspace("SELECT * FROM %s.t WHERE k=? AND c=?"), QUORUM, k, c);
coordinator.execute(withKeyspace("SELECT * FROM %s.t WHERE k=? AND c>?"), QUORUM, k, c);
coordinator.execute(withKeyspace("SELECT * FROM %s.t WHERE k=? AND c<?"), QUORUM, k, c);
coordinator.execute(withTable("SELECT * FROM %s WHERE k=? AND c=?"), QUORUM, k, c);
coordinator.execute(withTable("SELECT * FROM %s WHERE k=? AND c>?"), QUORUM, k, c);
coordinator.execute(withTable("SELECT * FROM %s WHERE k=? AND c<?"), QUORUM, k, c);
}
}
long requests = ReadRepairTester.readRepairRequestsCount(cluster.get(1), "t");
long requests = ReadRepairTester.readRepairRequestsCount(cluster.get(1), keyspaceName, tableName);
assertEquals("No read repair requests were expected, found " + requests, 0, requests);
}
}
@ -385,37 +420,41 @@ public class ReadRepairTest extends TestBaseImpl
@Test
public void readRepairRTRangeMovementTest() throws IOException
{
if (true)
return;
MutationTrackingUtils.fixmeSkipIfTracked(replicationType(), "Token moves not supported");
ExecutorPlus es = ExecutorFactory.Global.executorFactory().sequential("query-executor");
String key = "test1";
try (Cluster cluster = init(Cluster.build()
.withConfig(config -> config.with(Feature.GOSSIP, Feature.NETWORK)
.set("read_request_timeout", String.format("%dms", Integer.MAX_VALUE))
.set("native_transport_timeout", String.format("%dms", Integer.MAX_VALUE))
.set("read_request_timeout", format("%dms", Integer.MAX_VALUE))
.set("native_transport_timeout", format("%dms", Integer.MAX_VALUE))
)
.withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4))
.withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0"))
.withNodes(3)
.start()))
{
cluster.schemaChange("CREATE TABLE distributed_test_keyspace.tbl (\n" +
" key text,\n" +
" column1 int,\n" +
" PRIMARY KEY (key, column1)\n" +
") WITH CLUSTERING ORDER BY (column1 ASC)");
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (\n" +
" key text,\n" +
" column1 int,\n" +
" PRIMARY KEY (key, column1)\n" +
") WITH CLUSTERING ORDER BY (column1 ASC)"));
cluster.forEach(i -> i.runOnInstance(() -> open(KEYSPACE).getColumnFamilyStore("tbl").disableAutoCompaction()));
{
String ks = keyspaceName;
String tbl = tableName;
cluster.forEach(i -> i.runOnInstance(() -> open(ks).getColumnFamilyStore(tbl).disableAutoCompaction()));
}
for (int i = 1; i <= 2; i++)
{
cluster.get(i).executeInternal("DELETE FROM distributed_test_keyspace.tbl USING TIMESTAMP 50 WHERE key=?;", key);
cluster.get(i).executeInternal("DELETE FROM distributed_test_keyspace.tbl USING TIMESTAMP 80 WHERE key=? and column1 >= ? and column1 < ?;", key, 10, 100);
cluster.get(i).executeInternal("DELETE FROM distributed_test_keyspace.tbl USING TIMESTAMP 70 WHERE key=? and column1 = ?;", key, 30);
cluster.get(i).flush(KEYSPACE);
cluster.get(i).executeInternal(withTable("DELETE FROM %s USING TIMESTAMP 50 WHERE key=?;"), key);
cluster.get(i).executeInternal(withTable("DELETE FROM %s USING TIMESTAMP 80 WHERE key=? and column1 >= ? and column1 < ?;"), key, 10, 100);
cluster.get(i).executeInternal(withTable("DELETE FROM %s USING TIMESTAMP 70 WHERE key=? and column1 = ?;"), key, 30);
cluster.get(i).flush(keyspaceName);
}
cluster.get(3).executeInternal("DELETE FROM distributed_test_keyspace.tbl USING TIMESTAMP 100 WHERE key=?;", key);
cluster.get(3).flush(KEYSPACE);
cluster.get(3).executeInternal(withTable("DELETE FROM %s USING TIMESTAMP 100 WHERE key=?;"), key);
cluster.get(3).flush(keyspaceName);
// pause the read until we have bootstrapped a new node below
Condition continueRead = newOneTimeCondition();
@ -433,7 +472,7 @@ public class ReadRepairTest extends TestBaseImpl
return false;
}).drop();
String query = "SELECT * FROM distributed_test_keyspace.tbl WHERE key=? and column1 >= ? and column1 <= ?";
String query = withTable("SELECT * FROM %s WHERE key=? and column1 >= ? and column1 <= ?");
Future<Object[][]> read = es.submit(() -> cluster.coordinator(3).execute(query, ALL, key, 20, 40));
read.addCallback(new FutureCallback<Object[][]>()
{
@ -486,34 +525,35 @@ public class ReadRepairTest extends TestBaseImpl
{
try (Cluster cluster = init(Cluster.create(2)))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.t (k int, c int, PRIMARY KEY(k, c)) " +
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (k int, c int, PRIMARY KEY(k, c)) " +
"WITH gc_grace_seconds=0 AND compaction = " +
"{'class': 'SizeTieredCompactionStrategy', 'enabled': 'false'}"));
ICoordinator coordinator = cluster.coordinator(1);
// insert some data
coordinator.execute(withKeyspace("INSERT INTO %s.t(k, c) VALUES (0, 0)"), ALL);
coordinator.execute(withKeyspace("INSERT INTO %s.t(k, c) VALUES (1, 1)"), ALL);
coordinator.execute(withTable("INSERT INTO %s (k, c) VALUES (0, 0)"), ALL);
coordinator.execute(withTable("INSERT INTO %s (k, c) VALUES (1, 1)"), ALL);
// create partition tombstones in all nodes for both existent and not existent partitions
coordinator.execute(withKeyspace("DELETE FROM %s.t WHERE k=0"), ALL); // exists
coordinator.execute(withKeyspace("DELETE FROM %s.t WHERE k=2"), ALL); // doesn't exist
coordinator.execute(withTable("DELETE FROM %s WHERE k=0"), ALL); // exists
coordinator.execute(withTable("DELETE FROM %s WHERE k=2"), ALL); // doesn't exist
// create row tombstones in all nodes for both existent and not existent rows
coordinator.execute(withKeyspace("DELETE FROM %s.t WHERE k=1 AND c=1"), ALL); // exists
coordinator.execute(withKeyspace("DELETE FROM %s.t WHERE k=3 AND c=1"), ALL); // doesn't exist
coordinator.execute(withTable("DELETE FROM %s WHERE k=1 AND c=1"), ALL); // exists
coordinator.execute(withTable("DELETE FROM %s WHERE k=3 AND c=1"), ALL); // doesn't exist
// flush single sstable with tombstones
cluster.get(1).flush(KEYSPACE);
cluster.get(2).flush(KEYSPACE);
cluster.get(1).flush(keyspaceName);
cluster.get(2).flush(keyspaceName);
// purge tombstones from node2 with compaction (gc_grace_seconds=0)
cluster.get(2).forceCompact(KEYSPACE, "t");
cluster.get(2).forceCompact(keyspaceName, tableName);
// run an unrestricted range query verifying that it doesn't trigger read repair
coordinator.execute(withKeyspace("SELECT * FROM %s.t"), ALL);
long requests = ReadRepairTester.readRepairRequestsCount(cluster.get(1), "t");
coordinator.execute(withTable("SELECT * FROM %s"), ALL);
long requests = ReadRepairTester.readRepairRequestsCount(cluster.get(1), keyspaceName, tableName);
assertEquals("No read repair requests were expected, found " + requests, 0, requests);
}
}
@ -526,12 +566,13 @@ public class ReadRepairTest extends TestBaseImpl
.withInstanceInitializer(RRHelper::install)
.start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE distributed_test_keyspace.tbl0 (pk bigint,ck bigint,value bigint, PRIMARY KEY (pk, ck)) WITH CLUSTERING ORDER BY (ck ASC) AND read_repair='blocking';"));
createKeyspace(cluster);
cluster.schemaChange(withTable("CREATE TABLE %s (pk bigint,ck bigint,value bigint, PRIMARY KEY (pk, ck)) WITH CLUSTERING ORDER BY (ck ASC) AND read_repair='blocking';"));
long pk = 0L;
cluster.coordinator(1).execute("INSERT INTO distributed_test_keyspace.tbl0 (pk, ck, value) VALUES (?,?,?) USING TIMESTAMP 1", ConsistencyLevel.ALL, pk, 1L, 1L);
cluster.coordinator(1).execute("DELETE FROM distributed_test_keyspace.tbl0 USING TIMESTAMP 2 WHERE pk=? AND ck>?;", ConsistencyLevel.ALL, pk, 2L);
cluster.get(3).executeInternal("DELETE FROM distributed_test_keyspace.tbl0 USING TIMESTAMP 2 WHERE pk=?;", pk);
assertRows(cluster.coordinator(1).execute("SELECT * FROM distributed_test_keyspace.tbl0 WHERE pk=? AND ck>=? AND ck<?;",
cluster.coordinator(1).execute(withTable("INSERT INTO %s (pk, ck, value) VALUES (?,?,?) USING TIMESTAMP 1"), ConsistencyLevel.ALL, pk, 1L, 1L);
cluster.coordinator(1).execute(withTable("DELETE FROM %s USING TIMESTAMP 2 WHERE pk=? AND ck>?;"), ConsistencyLevel.ALL, pk, 2L);
cluster.get(3).executeInternal(withTable("DELETE FROM %s USING TIMESTAMP 2 WHERE pk=?;"), pk);
assertRows(cluster.coordinator(1).execute(withTable("SELECT * FROM %s WHERE pk=? AND ck>=? AND ck<?;"),
ConsistencyLevel.ALL, pk, 1L, 3L));
}
}

View File

@ -31,6 +31,7 @@ import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.shared.AssertUtils;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
@ -44,8 +45,9 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
{
private static final AtomicInteger seqNumber = new AtomicInteger();
private final String tableName = "t_" + seqNumber.getAndIncrement();
final String qualifiedTableName = KEYSPACE + '.' + tableName;
private final String keyspaceName = "ks_" + seqNumber.getAndIncrement();
private static final String tableName = "tbl";
final String qualifiedTableName = keyspaceName + '.' + tableName;
protected final Cluster cluster;
protected final ReadRepairStrategy strategy;
@ -53,8 +55,12 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
protected final boolean paging;
protected final boolean reverse;
protected final int coordinator;
protected final ReplicationType replicationType;
ReadRepairTester(Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging, boolean reverse)
// logged replication test support
protected int lastMutatedNode = -1;
ReadRepairTester(Cluster cluster, ReadRepairStrategy strategy, int coordinator, boolean flush, boolean paging, boolean reverse, ReplicationType replicationType)
{
this.cluster = cluster;
this.strategy = strategy;
@ -62,6 +68,7 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
this.paging = paging;
this.reverse = reverse;
this.coordinator = coordinator;
this.replicationType = replicationType;
}
abstract T self();
@ -76,6 +83,9 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
T createTable(String createTable)
{
cluster.schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION={'class': 'SimpleStrategy', 'replication_factor': " + cluster.size() + "} AND replication_type='%s'",
keyspaceName, replicationType.name()));
cluster.schemaChange(String.format("CREATE TYPE IF NOT EXISTS %s.udt (x int, y int)", keyspaceName));
String query;
switch (StringUtils.countMatches(createTable, "%s"))
{
@ -100,6 +110,7 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
*/
T mutate(int node, String... queries)
{
lastMutatedNode = node;
// run the write queries only on one node
for (String query : queries)
cluster.get(node).executeInternal(String.format(query, qualifiedTableName));
@ -150,13 +161,13 @@ public abstract class ReadRepairTester<T extends ReadRepairTester<T>>
long readRepairRequestsCount(int node)
{
return readRepairRequestsCount(cluster.get(node), tableName);
return readRepairRequestsCount(cluster.get(node), keyspaceName, tableName);
}
static long readRepairRequestsCount(IInvokableInstance node, String table)
static long readRepairRequestsCount(IInvokableInstance node, String keyspace, String table)
{
return node.callOnInstance(() -> {
ColumnFamilyStore cfs = Keyspace.open(KEYSPACE).getColumnFamilyStore(table);
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
return cfs.metric.readRepairRequests.getCount();
});
}

View File

@ -0,0 +1,30 @@
/*
* 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.distributed.test;
import org.apache.cassandra.schema.ReplicationType;
public class ReadRepairTrackedTest extends ReadRepairTestBase
{
@Override
protected ReplicationType replicationType()
{
return ReplicationType.tracked;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.distributed.test;
import org.junit.Test;
import org.apache.cassandra.schema.ReplicationType;
import org.apache.cassandra.service.reads.repair.ReadRepairStrategy;
public class ReadRepairUntrackedTest extends ReadRepairTestBase
{
@Override
protected ReplicationType replicationType()
{
return ReplicationType.untracked;
}
/**
*
* Tests basic behaviour of read repair with {@code NONE} read repair strategy.
*/
@Test
public void testNoneReadRepair() throws Throwable
{
testReadRepair(ReadRepairStrategy.NONE);
}
}

View File

@ -31,6 +31,7 @@ import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.exceptions.OverloadedException;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.reads.ReplicaFilteringProtection;
import static org.apache.cassandra.config.ReplicaFilteringProtectionOptions.DEFAULT_FAIL_THRESHOLD;
import static org.apache.cassandra.config.ReplicaFilteringProtectionOptions.DEFAULT_WARN_THRESHOLD;
@ -40,7 +41,7 @@ import static org.apache.cassandra.distributed.shared.AssertUtils.row;
import static org.junit.Assert.assertEquals;
/**
* Exercises the functionality of {@link org.apache.cassandra.service.reads.ReplicaFilteringProtection}, the
* Exercises the functionality of {@link ReplicaFilteringProtection}, the
* mechanism that ensures distributed index and filtering queries at read consistency levels > ONE/LOCAL_ONE
* avoid stale replica results.
*/

View File

@ -146,7 +146,7 @@ public class ResourceLeakTest extends TestBaseImpl
void dumpResources(String description) throws IOException, InterruptedException
{
dumpHeap(description, false);
dumpHeap(description, true);
if (dumpFileHandles)
{
dumpOpenFiles(description);

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