Accord migration and interop correctness

Patch by Ariel Weisberg; Reviewed by Blake Eggleston for CASSANDRA-19744
This commit is contained in:
Ariel Weisberg 2024-03-18 15:38:31 -04:00 committed by David Capwell
parent c377a066af
commit 8ab5003118
110 changed files with 4461 additions and 1380 deletions

2
.gitmodules vendored
View File

@ -1,4 +1,4 @@
[submodule "modules/accord"]
path = modules/accord
url = https://github.com/apache/cassandra-accord.git
url = ../cassandra-accord.git
branch = trunk

@ -1 +1 @@
Subproject commit 4aa0a8aeb6b12036660695e3fb89c69b5d40f345
Subproject commit 129a4862df43fdc5893687922a77bb0288f8cb83

View File

@ -27,25 +27,25 @@ import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.RateLimiter;
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutorPlus;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.cql3.UntypedResultSet.Row;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.Keyspace;
@ -55,6 +55,7 @@ import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.db.marshal.BytesType;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.gms.FailureDetector;
@ -70,26 +71,41 @@ import org.apache.cassandra.locator.Replicas;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessageFlag;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.WriteResponseHandler;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.IAccordService;
import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.SplitMutations;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.ExecutorUtils;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.MBeanWrapper;
import org.apache.cassandra.utils.Throwables;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.Future;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
import static org.apache.cassandra.config.CassandraRelevantProperties.BATCHLOG_REPLAY_TIMEOUT_IN_MS;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternalWithPaging;
import static org.apache.cassandra.hints.HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID;
import static org.apache.cassandra.net.Verb.MUTATION_REQ;
import static org.apache.cassandra.service.accord.txn.TxnResult.Kind.retry_new_protocol;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.mutateWithAccordAsync;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
public class BatchlogManager implements BatchlogManagerMBean
{
public static final String MBEAN_NAME = "org.apache.cassandra.db:type=BatchlogManager";
private static final long REPLAY_INTERVAL = 10 * 1000; // milliseconds
static final int DEFAULT_PAGE_SIZE = 128;
private static final Logger logger = LoggerFactory.getLogger(BatchlogManager.class);
@ -104,6 +120,8 @@ public class BatchlogManager implements BatchlogManagerMBean
private final RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);
private final AtomicBoolean isBatchlogReplayPaused = new AtomicBoolean(false);
public BatchlogManager()
{
batchlogTasks = executorFactory().scheduled(false, "BatchlogTasks");
@ -115,7 +133,7 @@ public class BatchlogManager implements BatchlogManagerMBean
batchlogTasks.scheduleWithFixedDelay(this::replayFailedBatches,
StorageService.RING_DELAY_MILLIS,
REPLAY_INTERVAL,
CassandraRelevantProperties.BATCHLOG_REPLAY_INTERVAL_MS.getLong(),
MILLISECONDS);
}
@ -184,7 +202,9 @@ public class BatchlogManager implements BatchlogManagerMBean
public void forceBatchlogReplay() throws Exception
{
logger.debug("Forcing batchlog replay");
startBatchlogReplay().get();
logger.debug("Finished forcing batchlog replay");
}
public Future<?> startBatchlogReplay()
@ -193,14 +213,25 @@ public class BatchlogManager implements BatchlogManagerMBean
return batchlogTasks.submit(this::replayFailedBatches);
}
void performInitialReplay() throws InterruptedException, ExecutionException
public void pauseReplay()
{
// Invokes initial replay. Used for testing only.
batchlogTasks.submit(this::replayFailedBatches).get();
logger.debug("Paused batchlog replay");
isBatchlogReplayPaused.set(true);
}
public void resumeReplay()
{
logger.debug("Resumed batchlog replay");
isBatchlogReplayPaused.set(false);
}
private void replayFailedBatches()
{
if (isBatchlogReplayPaused.get())
{
logger.debug("Batch log replay is paused, skipping replay");
return;
}
logger.trace("Started replayFailedBatches");
// rate limit is in bytes per second. Uses Double.MAX_VALUE if disabled (set to 0 in cassandra.yaml).
@ -223,6 +254,7 @@ public class BatchlogManager implements BatchlogManagerMBean
SchemaConstants.SYSTEM_KEYSPACE_NAME,
SystemKeyspace.BATCHES);
UntypedResultSet batches = executeInternalWithPaging(query, pageSize, lastReplayedUuid, limitUuid);
processBatchlogEntries(batches, pageSize, rateLimiter);
lastReplayedUuid = limitUuid;
logger.trace("Finished replayFailedBatches");
@ -276,16 +308,7 @@ public class BatchlogManager implements BatchlogManagerMBean
int version = row.getInt("version");
try
{
ReplayingBatch batch = new ReplayingBatch(id, version, row.getList("mutations", BytesType.instance));
if (batch.replay(rateLimiter, hintedNodes) > 0)
{
unfinishedBatches.add(batch);
}
else
{
remove(id); // no write mutations were sent (either expired or all CFs involved truncated).
++totalBatchesReplayed;
}
dispatchBatch(rateLimiter, row, id, version, hintedNodes, unfinishedBatches);
}
catch (IOException e)
{
@ -307,6 +330,8 @@ public class BatchlogManager implements BatchlogManagerMBean
// finalize the incomplete last page of batches
if (positionInPage > 0)
finishAndClearBatches(unfinishedBatches, hintedNodes, replayedBatches);
else
logger.trace("Had no batches to replay");
if (caughtException != null)
logger.warn(String.format("Encountered %d unexpected exceptions while sending out batches", skipped), caughtException);
@ -318,6 +343,35 @@ public class BatchlogManager implements BatchlogManagerMBean
replayedBatches.forEach(BatchlogManager::remove);
}
private void dispatchBatch(RateLimiter rateLimiter, Row row, TimeUUID id, int version, Set<UUID> hintedNodes, ArrayList<ReplayingBatch> unfinishedBatches) throws IOException
{
while (true)
{
ClusterMetadata cm = ClusterMetadata.current();
try
{
ReplayingBatch batch = new ReplayingBatch(id, version, row.getList("mutations", BytesType.instance), cm);
if (batch.replay(rateLimiter, hintedNodes))
{
unfinishedBatches.add(batch);
}
else
{
remove(id); // no write mutations were sent (either expired or all CFs involved truncated).
++totalBatchesReplayed;
}
}
catch (RetryOnDifferentSystemException e)
{
// Self apply can throw retry on different system
// Barring bugs we should already have the latest cluster metadata needed to correctly
// split the batch and retry since that is what was used to generate the exception
continue;
}
break;
}
}
private void finishAndClearBatches(ArrayList<ReplayingBatch> batches, Set<UUID> hintedNodes, Set<TimeUUID> replayedBatches)
{
// schedule hints for timed out deliveries
@ -340,61 +394,112 @@ public class BatchlogManager implements BatchlogManagerMBean
{
private final TimeUUID id;
private final long writtenAt;
private final List<Mutation> mutations;
private final int unsplitGcGs;
private final List<Mutation> normalMutations;
private final List<Mutation> accordMutations;
private final int replayedBytes;
private final ClusterMetadata cm;
private List<ReplayWriteResponseHandler<Mutation>> replayHandlers;
private List<ReplayWriteResponseHandler<Mutation>> replayHandlers = ImmutableList.of();
private AsyncTxnResult accordResult;
@Nullable
private Dispatcher.RequestTime accordTxnStart;
ReplayingBatch(TimeUUID id, int version, List<ByteBuffer> serializedMutations) throws IOException
ReplayingBatch(TimeUUID id, int version, List<ByteBuffer> serializedMutations, ClusterMetadata cm) throws IOException
{
this.id = id;
this.writtenAt = id.unix(MILLISECONDS);
this.mutations = new ArrayList<>(serializedMutations.size());
this.replayedBytes = addMutations(version, serializedMutations);
List<Mutation> unsplitMutations = new ArrayList<>(serializedMutations.size());
this.replayedBytes = addMutations(unsplitMutations, writtenAt, version, serializedMutations);
unsplitGcGs = gcgs(unsplitMutations);
SplitMutations<Mutation> splitMutations = ConsensusMigrationMutationHelper.splitMutationsIntoAccordAndNormal(cm, unsplitMutations);
logger.trace("Replaying batch with Accord {} and normal {}", splitMutations.accordMutations(), splitMutations.normalMutations());
normalMutations = splitMutations.normalMutations();
accordMutations = splitMutations.accordMutations();
if (accordMutations != null)
accordTxnStart = new Dispatcher.RequestTime(Clock.Global.nanoTime());
this.cm = cm;
}
public int replay(RateLimiter rateLimiter, Set<UUID> hintedNodes) throws IOException
public boolean replay(RateLimiter rateLimiter, Set<UUID> hintedNodes) throws IOException
{
logger.trace("Replaying batch {}", id);
if (mutations.isEmpty())
return 0;
if ((normalMutations == null || normalMutations.isEmpty()) && (accordMutations == null || accordMutations.isEmpty()))
return false;
int gcgs = gcgs(mutations);
if (MILLISECONDS.toSeconds(writtenAt) + gcgs <= FBUtilities.nowInSeconds())
return 0;
if (MILLISECONDS.toSeconds(writtenAt) + unsplitGcGs <= FBUtilities.nowInSeconds())
return false;
replayHandlers = sendReplays(mutations, writtenAt, hintedNodes);
if (accordMutations != null)
{
accordTxnStart = accordTxnStart.withStartedAt(Clock.Global.nanoTime());
accordResult = accordMutations != null ? mutateWithAccordAsync(cm, accordMutations, null, accordTxnStart) : null;
}
if (normalMutations != null)
replayHandlers = sendReplays(normalMutations, writtenAt, hintedNodes);
rateLimiter.acquire(replayedBytes); // acquire afterwards, to not mess up ttl calculation.
return replayHandlers.size();
return replayHandlers.size() > 0 || accordMutations != null;
}
public void finish(Set<UUID> hintedNodes)
{
for (int i = 0; i < replayHandlers.size(); i++)
Throwable failure = null;
// Check if the Accord mutations succeeded asynchronously
try
{
ReplayWriteResponseHandler<Mutation> handler = replayHandlers.get(i);
try
if (accordResult != null)
{
handler.get();
IAccordService accord = AccordService.instance();
TxnResult.Kind kind = accord.getTxnResult(accordResult, true, ConsistencyLevel.QUORUM, accordTxnStart).kind();
if (kind == retry_new_protocol)
throw new RetryOnDifferentSystemException();
}
catch (WriteTimeoutException|WriteFailureException e)
}
catch (WriteTimeoutException|WriteFailureException|RetryOnDifferentSystemException e)
{
logger.trace("Failed replaying a batched mutation on Accord, will write a hint");
logger.trace("Failure was : {}", e.getMessage());
writeHintsForUndeliveredAccordTxns(hintedNodes);
}
catch (Exception e)
{
failure = Throwables.merge(failure, e);
}
try
{
for (int i = 0; i < replayHandlers.size(); i++)
{
if (logger.isTraceEnabled())
ReplayWriteResponseHandler<Mutation> handler = replayHandlers.get(i);
try
{
handler.get();
}
catch (WriteTimeoutException|WriteFailureException|RetryOnDifferentSystemException e)
{
logger.trace("Failed replaying a batched mutation to a node, will write a hint");
logger.trace("Failure was : {}", e.getMessage());
// writing hints for the rest to hints, starting from i
writeHintsForUndeliveredEndpoints(i, hintedNodes);
break;
}
// writing hints for the rest to hints, starting from i
writeHintsForUndeliveredEndpoints(i, hintedNodes);
return;
}
}
catch (Exception e)
{
logger.debug("Unexpected batchlog replay exception", e);
failure = Throwables.merge(failure, e);
}
if (failure != null)
throw Throwables.unchecked(failure);
}
private int addMutations(int version, List<ByteBuffer> serializedMutations) throws IOException
private static int addMutations(List<Mutation> unsplitMutations, long writtenAt, int version, List<ByteBuffer> serializedMutations) throws IOException
{
int ret = 0;
for (ByteBuffer serializedMutation : serializedMutations)
@ -402,7 +507,7 @@ public class BatchlogManager implements BatchlogManagerMBean
ret += serializedMutation.remaining();
try (DataInputBuffer in = new DataInputBuffer(serializedMutation, true))
{
addMutation(Mutation.serializer.deserialize(in, version));
addMutation(unsplitMutations, writtenAt, Mutation.serializer.deserialize(in, version));
}
}
@ -412,19 +517,41 @@ public class BatchlogManager implements BatchlogManagerMBean
// Remove CFs that have been truncated since. writtenAt and SystemTable#getTruncatedAt() both return millis.
// We don't abort the replay entirely b/c this can be considered a success (truncated is same as delivered then
// truncated.
private void addMutation(Mutation mutation)
private static void addMutation(List<Mutation> unsplitMutations, long writtenAt, Mutation mutation)
{
for (TableId tableId : mutation.getTableIds())
if (writtenAt <= SystemKeyspace.getTruncatedAt(tableId))
mutation = mutation.without(tableId);
if (!mutation.isEmpty())
mutations.add(mutation);
if (mutation != null)
unsplitMutations.add(mutation);
}
// Write the hint assuming that when it is replayed it will probably be replayed
// as an Accord transaction so no reason to record per endpoint hints for all the endpoints
// Hints will still have to split and re-route on replay
private void writeHintsForUndeliveredAccordTxns(Set<UUID> hintedNodes)
{
if (accordMutations == null)
return;
int gcgs = gcgs(accordMutations);
// expired
if (MILLISECONDS.toSeconds(writtenAt) + gcgs <= FBUtilities.nowInSeconds())
return;
for (Mutation m : accordMutations)
HintsService.instance.write(ImmutableList.of(RETRY_ON_DIFFERENT_SYSTEM_UUID), Hint.create(m, writtenAt));
hintedNodes.add(RETRY_ON_DIFFERENT_SYSTEM_UUID);
}
private void writeHintsForUndeliveredEndpoints(int startFrom, Set<UUID> hintedNodes)
{
int gcgs = gcgs(mutations);
if (normalMutations == null)
return;
int gcgs = gcgs(normalMutations);
// expired
if (MILLISECONDS.toSeconds(writtenAt) + gcgs <= FBUtilities.nowInSeconds())
@ -434,7 +561,7 @@ public class BatchlogManager implements BatchlogManagerMBean
for (int i = startFrom; i < replayHandlers.size(); i++)
{
ReplayWriteResponseHandler<Mutation> handler = replayHandlers.get(i);
Mutation undeliveredMutation = mutations.get(i);
Mutation undeliveredMutation = normalMutations.get(i);
if (handler != null)
{

View File

@ -61,6 +61,7 @@ public enum CassandraRelevantProperties
AUTOCOMPACTION_ON_STARTUP_ENABLED("cassandra.autocompaction_on_startup_enabled", "true"),
AUTO_BOOTSTRAP("cassandra.auto_bootstrap"),
AUTO_REPAIR_FREQUENCY_SECONDS("cassandra.auto_repair_frequency_seconds", convertToString(TimeUnit.MINUTES.toSeconds(5))),
BATCHLOG_REPLAY_INTERVAL_MS("cassandra.batchlog.replay_interval_ms", "10000"),
BATCHLOG_REPLAY_TIMEOUT_IN_MS("cassandra.batchlog.replay_timeout_in_ms"),
BATCH_COMMIT_LOG_SYNC_INTERVAL("cassandra.batch_commitlog_sync_interval_millis", "1000"),
/**
@ -271,6 +272,8 @@ public enum CassandraRelevantProperties
*/
GOSSIP_SETTLE_POLL_SUCCESSES_REQUIRED("cassandra.gossip_settle_poll_success_required", "3"),
HINT_DISPATCH_INTERVAL_MS("cassandra.hint_dispatch_interval_ms", "10000"),
IGNORED_SCHEMA_CHECK_ENDPOINTS("cassandra.skip_schema_check_for_endpoints"),
IGNORED_SCHEMA_CHECK_VERSIONS("cassandra.skip_schema_check_for_versions"),
IGNORE_CORRUPTED_SCHEMA_TABLES("cassandra.ignore_corrupted_schema_tables"),

View File

@ -80,6 +80,7 @@ import org.apache.cassandra.config.Config.CommitLogSync;
import org.apache.cassandra.config.Config.DiskAccessMode;
import org.apache.cassandra.config.Config.PaxosOnLinearizabilityViolation;
import org.apache.cassandra.config.Config.PaxosStatePurging;
import org.apache.cassandra.config.DurationSpec.IntMillisecondsBound;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.commitlog.AbstractCommitLogSegmentManager;
import org.apache.cassandra.db.commitlog.CommitLog;
@ -4047,6 +4048,11 @@ public class DatabaseDescriptor
return conf.hints_flush_period.toMilliseconds();
}
public static void setHintsFlushPeriodInMS(int milliseconds)
{
conf.hints_flush_period = new IntMillisecondsBound(milliseconds);
}
public static long getMaxHintsFileSize()
{
return conf.max_hints_file_size.toBytesInLong();

View File

@ -354,7 +354,9 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement
ClientWarn.instance.warn(MessageFormatter.arrayFormat(LOGGED_BATCH_LOW_GCGS_WARNING, new Object[] { suffix, tablesWithZeroGcGs })
.getMessage());
}
return collector.toMutations(state);
// local is either executeWithoutConditions modifying a virtual table (doesn't support txns) or executeLocal
// which is called by test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local);
}
/**

View File

@ -137,14 +137,14 @@ final class BatchUpdatesCollector implements UpdatesCollector
* @return a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state)
public List<IMutation> toMutations(ClientState state, boolean allowPotentialTxnConflicts)
{
List<IMutation> ms = new ArrayList<>();
for (Map<ByteBuffer, IMutationBuilder> ksMap : mutationBuilders.values())
{
for (IMutationBuilder builder : ksMap.values())
{
IMutation mutation = builder.build();
IMutation mutation = builder.build(allowPotentialTxnConflicts);
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);
ms.add(mutation);
@ -182,7 +182,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
/**
* Build the immutable mutation
*/
IMutation build();
IMutation build(boolean allowPotentialTxnConflicts);
/**
* Get the builder for the given tableId
@ -215,7 +215,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
return this;
}
public Mutation build()
public Mutation build(boolean allowPotentialTxnConflicts)
{
ImmutableMap.Builder<TableId, PartitionUpdate> updates = new ImmutableMap.Builder<>();
for (Map.Entry<TableId, PartitionUpdate.Builder> updateEntry : modifications.entrySet())
@ -223,7 +223,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
PartitionUpdate update = updateEntry.getValue().build();
updates.put(updateEntry.getKey(), update);
}
return new Mutation(keyspaceName, key, updates.build(), createdAt, false);
return new Mutation(keyspaceName, key, updates.build(), createdAt, allowPotentialTxnConflicts);
}
public PartitionUpdate.Builder get(TableId tableId)
@ -263,9 +263,9 @@ final class BatchUpdatesCollector implements UpdatesCollector
return mutationBuilder.add(builder);
}
public IMutation build()
public IMutation build(boolean allowPotentialTxnConflicts)
{
return new CounterMutation(mutationBuilder.build(), cl);
return new CounterMutation(mutationBuilder.build(allowPotentialTxnConflicts), cl);
}
public PartitionUpdate.Builder get(TableId id)
@ -297,7 +297,7 @@ final class BatchUpdatesCollector implements UpdatesCollector
}
@Override
public VirtualMutation build()
public VirtualMutation build(boolean allowPotentialTxnConflicts)
{
ImmutableMap.Builder<TableId, PartitionUpdate> updates = new ImmutableMap.Builder<>();
modifications.forEach((tableId, updateBuilder) -> updates.put(tableId, updateBuilder.build()));

View File

@ -511,10 +511,11 @@ public class CQL3CasRequest implements CASRequest
private Update createUpdate(ClientState clientState, ConsistencyLevel commitConsistencyLevel)
{
// Potentially ignore commit consistency level if non-SERIAL write strategy is Accord
// Potentially ignore commit consistency level if TransactionalMode is full
// since it is safe to match what non-SERIAL writes do
commitConsistencyLevel = metadata.params.transactionalMode.commitCLForStrategy(commitConsistencyLevel);
return new TxnUpdate(createWriteFragments(clientState), createCondition(), commitConsistencyLevel);
// CAS requires using the new txn timestamp to correctly linearize some kinds of updates
return new TxnUpdate(createWriteFragments(clientState), createCondition(), commitConsistencyLevel, false);
}
private TxnCondition createCondition()

View File

@ -44,10 +44,9 @@ import org.apache.cassandra.cql3.Attributes;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.ColumnSpecification;
import org.apache.cassandra.cql3.Ordering;
import org.apache.cassandra.cql3.terms.Constants;
import org.apache.cassandra.cql3.Operation;
import org.apache.cassandra.cql3.Operations;
import org.apache.cassandra.cql3.Ordering;
import org.apache.cassandra.cql3.QualifiedName;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
@ -66,6 +65,7 @@ import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
import org.apache.cassandra.cql3.selection.ResultSetBuilder;
import org.apache.cassandra.cql3.selection.Selection;
import org.apache.cassandra.cql3.selection.Selection.Selectors;
import org.apache.cassandra.cql3.terms.Constants;
import org.apache.cassandra.cql3.transactions.ReferenceOperation;
import org.apache.cassandra.db.CBuilder;
import org.apache.cassandra.db.Clustering;
@ -869,14 +869,16 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
{
SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime, constructingAccordBaseUpdate);
return collector.toMutations(state);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local);
}
else
{
HashMultiset<ByteBuffer> perPartitionKeyCounts = HashMultiset.create(keys);
SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts);
addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime, constructingAccordBaseUpdate);
return collector.toMutations(state);
// local means this is test or internal things that are bypassing distributed system modification/checks
return collector.toMutations(state, local);
}
}

View File

@ -78,16 +78,16 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect
* Returns a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state)
public List<IMutation> toMutations(ClientState state, boolean allowPotentialTxnConflicts)
{
// it is possible that a modification statement does not create any mutations
// for example: DELETE FROM some_table WHERE part_key = 1 AND clust_key < 3 AND clust_key > 5
if (builder == null)
return Collections.emptyList();
return Collections.singletonList(createMutation(state, builder));
return Collections.singletonList(createMutation(state, builder, allowPotentialTxnConflicts));
}
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder)
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, boolean allowPotentialTxnConflicts)
{
IMutation mutation;
@ -96,7 +96,7 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect
else if (metadata.isCounter())
mutation = new CounterMutation(new Mutation(builder.build()), counterConsistencyLevel);
else
mutation = new Mutation(builder.build());
mutation = new Mutation(builder.build(), allowPotentialTxnConflicts);
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);

View File

@ -95,24 +95,24 @@ final class SingleTableUpdatesCollector implements UpdatesCollector
* @return a collection containing all the mutations.
*/
@Override
public List<IMutation> toMutations(ClientState state)
public List<IMutation> toMutations(ClientState state, boolean allowPotentialTxnConflicts)
{
if (puBuilders.size() == 1)
{
PartitionUpdate.Builder builder = puBuilders.values().iterator().next();
return Collections.singletonList(createMutation(state, builder));
return Collections.singletonList(createMutation(state, builder, allowPotentialTxnConflicts));
}
List<IMutation> ms = new ArrayList<>(puBuilders.size());
for (PartitionUpdate.Builder builder : puBuilders.values())
{
IMutation mutation = createMutation(state, builder);
IMutation mutation = createMutation(state, builder, allowPotentialTxnConflicts);
ms.add(mutation);
}
return ms;
}
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder)
private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, boolean allowPotentialTxnConflicts)
{
IMutation mutation;
@ -121,7 +121,7 @@ final class SingleTableUpdatesCollector implements UpdatesCollector
else if (metadata.isCounter())
mutation = new CounterMutation(new Mutation(builder.build()), counterConsistencyLevel);
else
mutation = new Mutation(builder.build());
mutation = new Mutation(builder.build(), allowPotentialTxnConflicts);
mutation.validateIndexedColumns(state);
mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE);

View File

@ -310,7 +310,7 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement,
AccordUpdate createUpdate(ClientState state, QueryOptions options, Map<TxnDataName, NamedSelect> autoReads, Consumer<Key> keyConsumer)
{
return new TxnUpdate(createWriteFragments(state, options, autoReads, keyConsumer), createCondition(options), null);
return new TxnUpdate(createWriteFragments(state, options, autoReads, keyConsumer), createCondition(options), null, false);
}
Keys toKeys(SortedSet<Key> keySet)

View File

@ -30,5 +30,5 @@ import org.apache.cassandra.service.ClientState;
public interface UpdatesCollector
{
PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, DecoratedKey dk, ConsistencyLevel consistency);
List<IMutation> toMutations(ClientState state);
List<IMutation> toMutations(ClientState state, boolean allowPotentialTxnConflicts);
}

View File

@ -62,6 +62,7 @@ import org.apache.cassandra.schema.MemtableParams;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableParams;
import org.apache.cassandra.schema.TableParams.Option;
import org.apache.cassandra.schema.UserFunctions;
import org.apache.cassandra.schema.ViewMetadata;
import org.apache.cassandra.schema.Views;
@ -594,7 +595,10 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
boolean modeChange = prev.transactionalMode != next.transactionalMode;
boolean wasMigrating = prev.transactionalMigrationFrom.isMigrating();
boolean forceMigrationChange = prev.transactionalMigrationFrom != next.transactionalMigrationFrom;
boolean explicitlySetMigrationFrom = attrs.hasOption(Option.TRANSACTIONAL_MIGRATION_FROM);
// set table to migrating
TransactionalMigrationFromMode newMigrateFrom = TransactionalMigrationFromMode.fromMode(prev.transactionalMode, next.transactionalMode);
boolean forceMigrationChange = modeChange && explicitlySetMigrationFrom && next.transactionalMigrationFrom != newMigrateFrom;
if (modeChange && next.transactionalMode.accordIsEnabled && !DatabaseDescriptor.getAccordTransactionsEnabled())
throw ire(format("Cannot change transactional mode to %s for %s.%s with accord_transactions_enabled set to false",
@ -617,9 +621,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
prev.transactionalMode, next.transactionalMode,
keyspaceName, tableName));
// set table to migrating
TransactionalMigrationFromMode migrateFrom = TransactionalMigrationFromMode.fromMode(prev.transactionalMode, next.transactionalMode);
return next.unbuild().transactionalMigrationFrom(migrateFrom).build();
return next.unbuild().transactionalMigrationFrom(newMigrateFrom).build();
}

View File

@ -27,18 +27,23 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRoutingException;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.locator.InetAddressAndPort;
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.tcm.ownership.VersionedEndpoints;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.NoSpamLogger;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
public abstract class AbstractMutationVerbHandler<T extends IMutation> implements IVerbHandler<T>
{
private static final Logger logger = LoggerFactory.getLogger(AbstractMutationVerbHandler.class);
@ -57,7 +62,17 @@ public abstract class AbstractMutationVerbHandler<T extends IMutation> implement
metadata = checkTokenOwnership(metadata, message, respondTo);
metadata = checkSchemaVersion(metadata, message, respondTo);
}
applyMutation(message, respondTo);
try
{
applyMutation(message, respondTo);
}
catch (RetryOnDifferentSystemException e)
{
logger.debug("Responding with retry on different system");
MessagingService.instance().respondWithFailure(RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM, message);
Tracing.trace("Payload application resulted in RetryOnDifferentSysten");
}
}
abstract void applyMutation(Message<T> message, InetAddressAndPort respondToAddress);
@ -85,9 +100,7 @@ public abstract class AbstractMutationVerbHandler<T extends IMutation> implement
}
}
// Mutations may intentionally be sent against an older Epoch so out of range checking doesn't work
// and could cause data to not end up where it needs to be for future operations
if (!message.payload.allowsOutOfRangeMutations() && !forToken.get().containsSelf())
if (!forToken.get().containsSelf())
{
StorageService.instance.incOutOfRangeOperationCount();
Keyspace.open(message.payload.getKeyspaceName()).metric.outOfRangeTokenWrites.inc();
@ -95,7 +108,7 @@ public abstract class AbstractMutationVerbHandler<T extends IMutation> implement
throw InvalidRoutingException.forWrite(respondTo, key.getToken(), metadata.epoch, message.payload);
}
if (!message.payload.allowsOutOfRangeMutations() && forToken.lastModified().isAfter(message.epoch()))
if (forToken.lastModified().isAfter(message.epoch()))
{
TCMMetrics.instance.coordinatorBehindPlacements.mark();
throw new CoordinatorBehindException(String.format("Routing is correct, but coordinator needs to catch-up at least to epoch %s to maintain consistency. Current coordinator epoch is %s",

View File

@ -86,10 +86,10 @@ public class CassandraKeyspaceWriteHandler implements KeyspaceWriteHandler
Set<TableId> ids = new HashSet<>();
for (PartitionUpdate update : mutation.getPartitionUpdates())
{
if (update.metadata().params.memtable.factory().writesShouldSkipCommitLog())
if (!update.metadata().params.memtable.factory().writesShouldSkipCommitLog())
ids.add(update.metadata().id);
}
mutation = mutation.without(ids);
mutation = mutation.filter(ids::contains);
}
}
// Note: It may be a good idea to precalculate none/all for the set of all tables in the keyspace,

View File

@ -18,10 +18,15 @@
package org.apache.cassandra.db;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.google.common.base.Function;
import com.google.common.base.Objects;
@ -31,11 +36,16 @@ import com.google.common.collect.PeekingIterator;
import com.google.common.util.concurrent.Striped;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.db.filter.*;
import org.apache.cassandra.db.partitions.*;
import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter;
import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.ColumnData;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowIterators;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
@ -44,10 +54,11 @@ import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.CacheService;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.*;
import org.apache.cassandra.utils.CounterId;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.btree.BTreeSet;
import static java.util.concurrent.TimeUnit.*;
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_51;
@ -83,6 +94,12 @@ public class CounterMutation implements IMutation
return mutation.getPartitionUpdates();
}
@Override
public boolean hasUpdateForTable(TableId tableId)
{
return mutation.hasUpdateForTable(tableId);
}
@Override
public Supplier<Mutation> hintOnFailure()
{
@ -156,6 +173,27 @@ public class CounterMutation implements IMutation
applyCounterMutation();
}
@Override
public @Nullable CounterMutation filter(Predicate<TableId> test)
{
Mutation m = mutation.filter(test);
if (m == null)
return null;
if (m == mutation)
return this;
return new CounterMutation(m, consistency);
}
/*
* Accord currently doesn't support interoperability with counters so no Accord transactions should read them
* anyways and it's safe to continue non-transactionally updating them
*/
@Override
public boolean allowsPotentialTransactionConflicts()
{
return true;
}
private void grabCounterLocks(Keyspace keyspace, List<Lock> locks) throws WriteTimeoutException
{
long startTime = nanoTime();

View File

@ -19,7 +19,9 @@ package org.apache.cassandra.db;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.partitions.PartitionUpdate;
@ -37,6 +39,7 @@ public interface IMutation
long getTimeout(TimeUnit unit);
String toString(boolean shallow);
Collection<PartitionUpdate> getPartitionUpdates();
boolean hasUpdateForTable(TableId tableId);
Supplier<Mutation> hintOnFailure();
default void validateIndexedColumns(ClientState state)
@ -71,8 +74,23 @@ public interface IMutation
return size;
}
default boolean allowsOutOfRangeMutations()
/**
* True if this mutation is being applied by a transaction system or doesn't need to be
* and conflicts between this mutation and transactions systems that are managing all or part of this table
* should be assumed to be handled already (by either Paxos or Accord) and the mutation should be applied.
*
* This causes mutations against tables to fail if they are from a non-transaction sub-system such as mutations,
* logged and unlogged batches, hints, and read repair against tables that are being managed by a transaction system
* like Accord that can't safely read data that is written non-transactionally.
*
*/
default boolean allowsPotentialTransactionConflicts()
{
return false;
}
// Construct replacement mutation that is identical except it only includes updates for the specified tables
@Nullable IMutation filter(Predicate<TableId> predicate);
default void clearCachedSerializationsForRetry() {}
}

View File

@ -47,8 +47,8 @@ import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.SecondaryIndexManager;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.sstable.format.SSTableReader;
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;
@ -58,14 +58,15 @@ import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.SchemaProvider;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.OpOrder;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import org.apache.cassandra.utils.concurrent.Promise;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
@ -545,6 +546,7 @@ public class Keyspace
}
try (WriteContext ctx = getWriteHandler().beginWrite(mutation, makeDurable))
{
ConsensusMigrationMutationHelper.validateSafeToExecuteNonTransactionally(mutation);
for (PartitionUpdate upd : mutation.getPartitionUpdates())
{
ColumnFamilyStore cfs = columnFamilyStores.get(upd.metadata().id);

View File

@ -19,15 +19,17 @@ package org.apache.cassandra.db;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
@ -54,6 +56,7 @@ import org.apache.cassandra.service.AbstractWriteResponseHandler;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.concurrent.Future;
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_51;
@ -62,7 +65,8 @@ import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
public class Mutation implements IMutation, Supplier<Mutation>
{
public static final MutationSerializer serializer = new MutationSerializer();
public static final int ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG = 0x01;
public static final int ALLOW_POTENTIAL_TRANSACTION_CONFLICTS = 0x01;
// todo this is redundant
// when we remove it, also restore SerializationsTest.testMutationRead to not regenerate new Mutations each test
@ -84,33 +88,41 @@ public class Mutation implements IMutation, Supplier<Mutation>
private static final int SERIALIZATION_VERSION_COUNT = MessagingService.Version.values().length;
// Contains serialized representations of this mutation.
// Note: there is no functionality to clear/remove serialized instances, because a mutation must never
// be modified (e.g. calling add(PartitionUpdate)) when it's being serialized.
// Note: The cached serializations can be cleared when CoordinatorBehindException is being retried
private final Serialization[] cachedSerializations = new Serialization[SERIALIZATION_VERSION_COUNT];
/** @see CassandraRelevantProperties#CACHEABLE_MUTATION_SIZE_LIMIT */
private static final long CACHEABLE_MUTATION_SIZE_LIMIT = CassandraRelevantProperties.CACHEABLE_MUTATION_SIZE_LIMIT.getLong();
private boolean allowOutOfRangeMutations;
// Paxos & Accord manage conflicts directly and needs to apply mutations to tables/ranges
// that are only safe to write to from a transaction system.
// Don't refuse to apply this mutation because it should go through a transaction system
// because it is being applied by one or in a context where transaction conflicts don't occur
private boolean allowPotentialTransactionConflicts;
public Mutation(PartitionUpdate update)
{
this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc);
this(update, false);
}
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean allowOutOfRangeMutations)
public Mutation(PartitionUpdate update, boolean allowPotentialTransactionConflicts)
{
this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), allowOutOfRangeMutations);
this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, allowPotentialTransactionConflicts);
}
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, boolean allowOutOfRangeMutations)
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean allowPotentialTransactionConflicts)
{
this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), allowPotentialTransactionConflicts);
}
public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap<TableId, PartitionUpdate> modifications, long approxCreatedAtNanos, boolean cdcEnabled, boolean allowPotentialTransactionConflicts)
{
this.keyspaceName = keyspaceName;
this.key = key;
this.modifications = modifications;
this.cdcEnabled = cdcEnabled;
this.approxCreatedAtNanos = approxCreatedAtNanos;
this.allowOutOfRangeMutations = allowOutOfRangeMutations;
this.allowPotentialTransactionConflicts = allowPotentialTransactionConflicts;
}
private static boolean cdcEnabled(Iterable<PartitionUpdate> modifications)
@ -121,26 +133,35 @@ public class Mutation implements IMutation, Supplier<Mutation>
return cdc;
}
public Mutation without(Set<TableId> tableIds)
@Override
public @Nullable Mutation filter(Predicate<TableId> predicate)
{
if (tableIds.isEmpty())
boolean allMatch = true;
boolean noneMatch = true;
for (TableId tableId : modifications.keySet())
{
boolean test = predicate.test(tableId);
allMatch &= test;
noneMatch &= !test;
}
if (allMatch)
return this;
if (noneMatch)
return null;
ImmutableMap.Builder<TableId, PartitionUpdate> builder = new ImmutableMap.Builder<>();
for (Map.Entry<TableId, PartitionUpdate> update : modifications.entrySet())
{
if (!tableIds.contains(update.getKey()))
{
if (predicate.test(update.getKey()))
builder.put(update);
}
}
return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, allowOutOfRangeMutations);
Map<TableId, PartitionUpdate> updates = builder.build();
checkState(!updates.isEmpty(), "Updates should not be empty");
return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, allowPotentialTransactionConflicts);
}
public Mutation without(TableId tableId)
public @Nullable Mutation without(TableId tableId)
{
return without(Collections.singleton(tableId));
return filter(otherTableId -> !tableId.equals(otherTableId));
}
public String getKeyspaceName()
@ -168,6 +189,12 @@ public class Mutation implements IMutation, Supplier<Mutation>
return approxCreatedAtNanos;
}
@Override
public boolean hasUpdateForTable(TableId tableId)
{
return modifications.containsKey(tableId);
}
@Override
public Supplier<Mutation> hintOnFailure()
{
@ -215,18 +242,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
{
assert !mutations.isEmpty();
if (mutations.size() == ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG)
if (mutations.size() == 1)
return mutations.iterator().next();
Set<TableId> updatedTables = new HashSet<>();
String ks = null;
DecoratedKey key = null;
Boolean allowOutOfRangeMutations = null;
Boolean allowPotentialTransactionConflicts = null;
for (Mutation mutation : mutations)
{
if (allowOutOfRangeMutations != null && allowOutOfRangeMutations != mutation.allowOutOfRangeMutations)
throw new IllegalArgumentException("Can't merge mutations with differing policies on allowing out of range mutations");
allowOutOfRangeMutations = mutation.allowOutOfRangeMutations;
if (allowPotentialTransactionConflicts != null && allowPotentialTransactionConflicts != mutation.allowPotentialTransactionConflicts)
throw new IllegalArgumentException("Can't merge mutations with differing policies on allowing potential transaction conflicts");
allowPotentialTransactionConflicts = mutation.allowPotentialTransactionConflicts;
updatedTables.addAll(mutation.modifications.keySet());
if (ks != null && !ks.equals(mutation.keyspaceName))
throw new IllegalArgumentException();
@ -250,10 +277,10 @@ public class Mutation implements IMutation, Supplier<Mutation>
if (updates.isEmpty())
continue;
modifications.put(table, updates.size() == ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG ? updates.get(0) : PartitionUpdate.merge(updates));
modifications.put(table, updates.size() == 1 ? updates.get(0) : PartitionUpdate.merge(updates));
updates.clear();
}
return new Mutation(ks, key, modifications.build(), approxTime.now(), allowOutOfRangeMutations);
return new Mutation(ks, key, modifications.build(), approxTime.now(), allowPotentialTransactionConflicts);
}
public Future<?> applyFuture()
@ -310,25 +337,26 @@ public class Mutation implements IMutation, Supplier<Mutation>
return cdcEnabled;
}
public Mutation allowOutOfRangeMutations()
public void allowPotentialTransactionConflicts()
{
allowOutOfRangeMutations = true;
return this;
allowPotentialTransactionConflicts = true;
Arrays.fill(cachedSerializations, null);
}
public boolean allowsOutOfRangeMutations()
@Override
public boolean allowsPotentialTransactionConflicts()
{
return allowOutOfRangeMutations;
return allowPotentialTransactionConflicts;
}
private static int allowsOutOfRangeMutationsFlag(boolean allowOutOfRangeMutations)
private static int allowPotentialTransactionConflictsFlag(boolean allowPotentialTransactionConflicts)
{
return allowOutOfRangeMutations ? ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG : 0;
return allowPotentialTransactionConflicts ? ALLOW_POTENTIAL_TRANSACTION_CONFLICTS : 0;
}
private static boolean allowsOutOfRangeMutations(int flags)
public static boolean allowPotentialTransactionConflicts(int flags)
{
return (flags & ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG) != 0;
return (flags & ALLOW_POTENTIAL_TRANSACTION_CONFLICTS) != 0;
}
public String toString()
@ -336,6 +364,12 @@ public class Mutation implements IMutation, Supplier<Mutation>
return toString(false);
}
@Override
public void clearCachedSerializationsForRetry()
{
Arrays.fill(cachedSerializations, null);
}
public String toString(boolean shallow)
{
StringBuilder buff = new StringBuilder("Mutation(");
@ -404,6 +438,13 @@ public class Mutation implements IMutation, Supplier<Mutation>
*/
public interface SimpleBuilder
{
/**
* Assume any potential transaction conflicts that might occur by applying this mutation are already
* being handled by the caller
* @return this builder
*/
public SimpleBuilder allowPotentialTransactionConflicts();
/**
* Sets the timestamp to use for the following additions to this builder or any derived (update or row) builder.
*
@ -517,7 +558,11 @@ public class Mutation implements IMutation, Supplier<Mutation>
Map<TableId, PartitionUpdate> modifications = mutation.modifications;
if (version >= VERSION_51)
out.write(allowsOutOfRangeMutationsFlag(mutation.allowsOutOfRangeMutations()));
{
int flags = 0;
flags |= allowPotentialTransactionConflictsFlag(mutation.allowPotentialTransactionConflicts);
out.write(flags);
}
/* serialize the modifications in the mutation */
int size = modifications.size();
@ -538,11 +583,11 @@ public class Mutation implements IMutation, Supplier<Mutation>
{
teeIn = new TeeDataInputPlus(in, dob, CACHEABLE_MUTATION_SIZE_LIMIT);
boolean allowsOutOfRangeMutations = false;
boolean allowPotentialTransactionConflicts = false;
if (version >= VERSION_51)
{
int flags = teeIn.readByte();
allowsOutOfRangeMutations = allowsOutOfRangeMutations(flags);
allowPotentialTransactionConflicts = allowPotentialTransactionConflicts(flags);
}
int size = teeIn.readUnsignedVInt32();
assert size > 0;
@ -550,7 +595,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
PartitionUpdate update = PartitionUpdate.serializer.deserialize(teeIn, version, flag);
if (size == 1)
{
m = new Mutation(update);
m = new Mutation(update, allowPotentialTransactionConflicts);
}
else
{
@ -563,7 +608,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(), allowsOutOfRangeMutations);
m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now(), allowPotentialTransactionConflicts);
}
//Only cache serializations that don't hit the limit
@ -642,7 +687,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
if (size == 0L)
{
if (version >= VERSION_51)
size += TypeSizes.sizeof((byte)ALLOW_OUT_OF_RANGE_MUTATIONS_FLAG); // flags
size += TypeSizes.sizeof((byte)ALLOW_POTENTIAL_TRANSACTION_CONFLICTS); // flags
size += TypeSizes.sizeofUnsignedVInt(mutation.modifications.size());
for (PartitionUpdate partitionUpdate : mutation.modifications.values())
size += serializer.serializedSize(partitionUpdate, version);
@ -663,10 +708,18 @@ public class Mutation implements IMutation, Supplier<Mutation>
private final long approxCreatedAtNanos = approxTime.now();
private boolean empty = true;
private boolean allowPotentialTransactionConflicts;
public PartitionUpdateCollector(String keyspaceName, DecoratedKey key)
{
this(keyspaceName, key, false);
}
public PartitionUpdateCollector(String keyspaceName, DecoratedKey key, boolean allowPotentialTransactionConflicts)
{
this.keyspaceName = keyspaceName;
this.key = key;
this.allowPotentialTransactionConflicts = allowPotentialTransactionConflicts;
}
public PartitionUpdateCollector add(PartitionUpdate partitionUpdate)
@ -700,7 +753,7 @@ public class Mutation implements IMutation, Supplier<Mutation>
public Mutation build()
{
return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, false);
return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, allowPotentialTransactionConflicts);
}
}
}

View File

@ -18,21 +18,30 @@
package org.apache.cassandra.db;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.db.marshal.SetType;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.BTreeRow;
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.db.marshal.*;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.CounterId;
import org.apache.cassandra.utils.FBUtilities;
@ -111,12 +120,20 @@ public abstract class SimpleBuilders
private final Map<TableId, PartitionUpdateBuilder> updateBuilders = new HashMap<>();
private boolean allowPotentialTransactionConflicts = false;
public MutationBuilder(String keyspaceName, DecoratedKey key)
{
this.keyspaceName = keyspaceName;
this.key = key;
}
public MutationBuilder allowPotentialTransactionConflicts()
{
allowPotentialTransactionConflicts = true;
return this;
}
public PartitionUpdate.SimpleBuilder update(TableMetadata metadata)
{
assert metadata.keyspace.equals(keyspaceName);
@ -145,9 +162,9 @@ public abstract class SimpleBuilders
assert !updateBuilders.isEmpty() : "Cannot create empty mutation";
if (updateBuilders.size() == 1)
return new Mutation(updateBuilders.values().iterator().next().build());
return new Mutation(updateBuilders.values().iterator().next().build(), allowPotentialTransactionConflicts);
Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key);
Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key, allowPotentialTransactionConflicts);
for (PartitionUpdateBuilder builder : updateBuilders.values())
mutationBuilder.add(builder.build());
return mutationBuilder.build();

View File

@ -19,7 +19,9 @@ package org.apache.cassandra.db.virtual;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap;
@ -107,6 +109,12 @@ public final class VirtualMutation implements IMutation
return modifications.values();
}
@Override
public boolean hasUpdateForTable(TableId tableId)
{
return modifications.containsKey(tableId);
}
@Override
public Supplier<Mutation> hintOnFailure()
{
@ -123,4 +131,19 @@ public final class VirtualMutation implements IMutation
{
// no-op
}
@Override
public @Nullable VirtualMutation filter(Predicate<TableId> test)
{
throw new UnsupportedOperationException();
}
/*
* Accord doesn't support reading/writing virtual tables yet so updating them non-transactionally is always safe
*/
@Override
public boolean allowsPotentialTransactionConflicts()
{
return true;
}
}

View File

@ -35,7 +35,10 @@ import static org.apache.cassandra.exceptions.ExceptionSerializer.nullableRemote
/**
* Allow inclusion of a serialized exception in failure response messages
* This continues to use the same verb as the old failure response (whether a message payload or parameter)
* and has a nullable failure field that may contain a serialized in later versions.
* and has a nullable failure field that may contain a serialized exception in later versions.
*
* It's important to note RequestFailure is not a singleton for each type, unlike RequestFailureReason,
* since it might include a stack trace so don't compare using identity.
*/
public class RequestFailure
{
@ -50,6 +53,7 @@ public class RequestFailure
public static final RequestFailure INDEX_NOT_AVAILABLE = new RequestFailure(RequestFailureReason.INDEX_NOT_AVAILABLE);
public static final RequestFailure COORDINATOR_BEHIND = new RequestFailure(RequestFailureReason.COORDINATOR_BEHIND);
public static final RequestFailure READ_TOO_MANY_INDEXES = new RequestFailure(RequestFailureReason.READ_TOO_MANY_INDEXES);
public static final RequestFailure RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM = new RequestFailure(RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM);
static
{
@ -114,6 +118,12 @@ public class RequestFailure
if (t instanceof InvalidRoutingException)
return INVALID_ROUTING;
if (t instanceof RetryOnDifferentSystemException)
return RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
if (t instanceof CoordinatorBehindException)
return COORDINATOR_BEHIND;
return UNKNOWN;
}
@ -133,6 +143,7 @@ public class RequestFailure
case INDEX_NOT_AVAILABLE: return INDEX_NOT_AVAILABLE;
case COORDINATOR_BEHIND: return COORDINATOR_BEHIND;
case READ_TOO_MANY_INDEXES: return READ_TOO_MANY_INDEXES;
case RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM: return RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
}
}

View File

@ -34,21 +34,23 @@ import static org.apache.cassandra.net.MessagingService.VERSION_40;
public enum RequestFailureReason
{
UNKNOWN (0),
READ_TOO_MANY_TOMBSTONES (1),
TIMEOUT (2),
INCOMPATIBLE_SCHEMA (3),
READ_SIZE (4),
UNKNOWN (0),
READ_TOO_MANY_TOMBSTONES (1),
TIMEOUT (2),
INCOMPATIBLE_SCHEMA (3),
READ_SIZE (4),
// below reason is only logged, but it does not have associated exception
NODE_DOWN (5),
INDEX_NOT_AVAILABLE (6),
NODE_DOWN (5),
INDEX_NOT_AVAILABLE (6),
// below reason does not have an associated exception
READ_TOO_MANY_INDEXES (7),
NOT_CMS (8),
INVALID_ROUTING (9),
COORDINATOR_BEHIND (10),
READ_TOO_MANY_INDEXES (7),
NOT_CMS (8),
INVALID_ROUTING (9),
COORDINATOR_BEHIND (10),
// The following codes have been ported from an external fork, where they were offset explicitly to avoid conflicts.
INDEX_BUILD_IN_PROGRESS (503);
INDEX_BUILD_IN_PROGRESS (503),
RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM (504),
;
static
{

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.exceptions;
/**
* Thrown when a non-transactional operation is attempted when the operation needs to be done transactionally (or vice versa)
* and it could interfere with operations performed transactionally or can't be applied by the chosen transaction system.
*
* The correct way to handle this is to forward the error the originator of the operation who can then retry it on
* the correct system.
*/
public class RetryOnDifferentSystemException extends RuntimeException
{
}

View File

@ -37,44 +37,44 @@ final class HintDiagnostics
{
if (isEnabled(HintEventType.DISPATCHER_CREATED))
service.publish(new HintEvent(HintEventType.DISPATCHER_CREATED, dispatcher,
dispatcher.hostId, dispatcher.address, null, null, null, null));
dispatcher.hostId, dispatcher.address, null, null, null, null, null));
}
static void dispatcherClosed(HintsDispatcher dispatcher)
{
if (isEnabled(HintEventType.DISPATCHER_CLOSED))
service.publish(new HintEvent(HintEventType.DISPATCHER_CLOSED, dispatcher,
dispatcher.hostId, dispatcher.address, null, null, null, null));
dispatcher.hostId, dispatcher.address, null, null, null, null, null));
}
static void dispatchPage(HintsDispatcher dispatcher)
{
if (isEnabled(HintEventType.DISPATCHER_PAGE))
service.publish(new HintEvent(HintEventType.DISPATCHER_PAGE, dispatcher,
dispatcher.hostId, dispatcher.address, null, null, null, null));
dispatcher.hostId, dispatcher.address, null, null, null, null, null));
}
static void abortRequested(HintsDispatcher dispatcher)
{
if (isEnabled(HintEventType.ABORT_REQUESTED))
service.publish(new HintEvent(HintEventType.ABORT_REQUESTED, dispatcher,
dispatcher.hostId, dispatcher.address, null, null, null, null));
dispatcher.hostId, dispatcher.address, null, null, null, null, null));
}
static void pageSuccessResult(HintsDispatcher dispatcher, long success, long failures, long timeouts)
static void pageSuccessResult(HintsDispatcher dispatcher, long success, long failures, long timeouts, long retryDifferentSystem)
{
if (isEnabled(HintEventType.DISPATCHER_HINT_RESULT))
service.publish(new HintEvent(HintEventType.DISPATCHER_HINT_RESULT, dispatcher,
dispatcher.hostId, dispatcher.address, HintResult.PAGE_SUCCESS,
success, failures, timeouts));
success, failures, timeouts, retryDifferentSystem));
}
static void pageFailureResult(HintsDispatcher dispatcher, long success, long failures, long timeouts)
static void pageFailureResult(HintsDispatcher dispatcher, long success, long failures, long timeouts, long retryDifferentSystem)
{
if (isEnabled(HintEventType.DISPATCHER_HINT_RESULT))
service.publish(new HintEvent(HintEventType.DISPATCHER_HINT_RESULT, dispatcher,
dispatcher.hostId, dispatcher.address, HintResult.PAGE_FAILURE,
success, failures, timeouts));
success, failures, timeouts, retryDifferentSystem));
}
private static boolean isEnabled(HintEventType type)

View File

@ -64,10 +64,13 @@ final class HintEvent extends DiagnosticEvent
private final Long pageHintsFailed;
@Nullable
private final Long pageHintsTimeout;
@Nullable
private final Long pageHintsRetryDifferentSystem;
HintEvent(HintEventType type, HintsDispatcher dispatcher, UUID targetHostId, InetAddressAndPort targetAddress,
@Nullable HintResult dispatchResult, @Nullable Long pageHintsSuccessful,
@Nullable Long pageHintsFailed, @Nullable Long pageHintsTimeout)
@Nullable Long pageHintsFailed, @Nullable Long pageHintsTimeout,
@Nullable Long pageHintsRetryDifferentSystem)
{
this.type = type;
this.dispatcher = dispatcher;
@ -77,6 +80,7 @@ final class HintEvent extends DiagnosticEvent
this.pageHintsSuccessful = pageHintsSuccessful;
this.pageHintsFailed = pageHintsFailed;
this.pageHintsTimeout = pageHintsTimeout;
this.pageHintsRetryDifferentSystem = pageHintsRetryDifferentSystem;
}
public Enum<HintEventType> getType()
@ -96,6 +100,7 @@ final class HintEvent extends DiagnosticEvent
ret.put("hint.page.hints_succeeded", pageHintsSuccessful);
ret.put("hint.page.hints_failed", pageHintsFailed);
ret.put("hint.page.hints_timed_out", pageHintsTimeout);
ret.put("hint.page.hints_retry_different_system", pageHintsRetryDifferentSystem);
}
return ret;
}

View File

@ -24,6 +24,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
@ -34,6 +35,8 @@ import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.membership.NodeId;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
/**
* Verb handler used both for hint dispatch and streaming.
*
@ -100,8 +103,15 @@ public final class HintVerbHandler implements IVerbHandler<HintMessage>
}
else
{
// the common path - the node is both the destination and a valid replica for the hint.
hint.applyFuture().addCallback(o -> respond(message), e -> logger.debug("Failed to apply hint", e));
try
{
// the common path - the node is both the destination and a valid replica for the hint.
hint.applyFuture().addCallback(o -> respond(message), e -> logger.debug("Failed to apply hint", e));
}
catch (RetryOnDifferentSystemException e)
{
MessagingService.instance().respondWithFailure(RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM, message);
}
}
}

View File

@ -21,6 +21,8 @@ import java.io.Closeable;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
@ -133,6 +135,13 @@ final class HintsBufferPool implements Closeable
return HintsBuffer.create(bufferSize);
}
@VisibleForTesting
public void clearUnsafe()
{
if (currentBuffer != null)
currentBuffer = currentBuffer.recycle();
}
public void close()
{
currentBuffer.free();

View File

@ -20,7 +20,11 @@ package org.apache.cassandra.hints;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import javax.annotation.Nullable;
@ -30,10 +34,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.FSError;
import org.apache.cassandra.io.FSReadError;
import org.apache.cassandra.io.FSWriteError;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.NativeLibrary;
import org.apache.cassandra.utils.SyncUtil;
@ -110,6 +114,11 @@ final class HintsCatalog
return stores.get(hostId);
}
void deleteAllHintsUnsafe()
{
stores.values().forEach(HintsStore::deleteAllHintsUnsafe);
}
/**
* Delete all hints for all host ids.
*

View File

@ -26,9 +26,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.RateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -39,10 +39,11 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
import static org.apache.cassandra.hints.HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID;
/**
* A multi-threaded (by default) executor for dispatching hints.
@ -273,7 +274,7 @@ final class HintsDispatchExecutor
logger.trace("Dispatching hints file {}", descriptor.hintsFileName);
InetAddressAndPort address = StorageService.instance.getEndpointForHostId(hostId);
if (address != null)
if (address != null || hostId == RETRY_ON_DIFFERENT_SYSTEM_UUID)
return deliver(descriptor, address);
// address == null means the target no longer exist; find new home for each hint entry.
@ -281,12 +282,12 @@ final class HintsDispatchExecutor
return true;
}
private boolean deliver(HintsDescriptor descriptor, InetAddressAndPort address)
private boolean deliver(HintsDescriptor descriptor, @Nullable InetAddressAndPort address)
{
File file = descriptor.file(hintsDirectory);
InputPosition offset = store.getDispatchOffset(descriptor);
BooleanSupplier shouldAbort = () -> !isAlive.test(address) || isPaused.get();
BooleanSupplier shouldAbort = () -> (!hostId.equals(RETRY_ON_DIFFERENT_SYSTEM_UUID) && (address == null || !isAlive.test(address)) || isPaused.get());
try (HintsDispatcher dispatcher = HintsDispatcher.create(file, rateLimiter, address, descriptor.hostId, shouldAbort))
{
if (offset != null)
@ -298,7 +299,7 @@ final class HintsDispatchExecutor
{
store.delete(descriptor);
store.cleanUp(descriptor);
logger.info("Finished hinted handoff of file {} to endpoint {}: {}", descriptor.fileName(), address, hostId);
logger.info("Finished hinted handoff of file {} to destination {}: {}", descriptor.fileName(), dispatcher.destination(), hostId);
return true;
}
else
@ -322,7 +323,7 @@ final class HintsDispatchExecutor
{
store.markDispatchOffset(descriptor, dispatcher.dispatchPosition());
store.offerFirst(descriptor);
logger.info("Finished hinted handoff of file {} to endpoint {}: {}, partially", descriptor.fileName(), address, hostId);
logger.info("Finished hinted handoff of file {} to destination {}: {}, partially", descriptor.fileName(), dispatcher.destination(), hostId);
}
// for each hint in the hints file for a node that isn't part of the ring anymore, write RF hints for each replica

View File

@ -19,31 +19,64 @@ package org.apache.cassandra.hints;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.util.concurrent.RateLimiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.DebuggableTask.RunnableDebuggableTask;
import org.apache.cassandra.concurrent.ImmediateExecutor;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.metrics.HintsServiceMetrics;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.IAccordService;
import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.SplitMutation;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ownership.DataPlacement;
import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.MonotonicClock;
import org.apache.cassandra.utils.NoSpamLogger;
import org.apache.cassandra.utils.concurrent.Condition;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.hints.HintsDispatcher.Callback.Outcome.FAILURE;
import static org.apache.cassandra.hints.HintsDispatcher.Callback.Outcome.INTERRUPTED;
import static org.apache.cassandra.hints.HintsDispatcher.Callback.Outcome.RETRY_DIFFERENT_SYSTEM;
import static org.apache.cassandra.hints.HintsDispatcher.Callback.Outcome.SUCCESS;
import static org.apache.cassandra.hints.HintsDispatcher.Callback.Outcome.TIMEOUT;
import static org.apache.cassandra.hints.HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID;
import static org.apache.cassandra.metrics.HintsServiceMetrics.ACCORD_HINT_ENDPOINT;
import static org.apache.cassandra.metrics.HintsServiceMetrics.updateDelayMetrics;
import static org.apache.cassandra.net.Verb.HINT_REQ;
import static org.apache.cassandra.service.accord.txn.TxnResult.Kind.retry_new_protocol;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition;
@ -56,19 +89,28 @@ import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeConditio
final class HintsDispatcher implements AutoCloseable
{
private static final Logger logger = LoggerFactory.getLogger(HintsDispatcher.class);
private static final NoSpamLogger noSpamLogger = NoSpamLogger.getLogger(logger, 1, TimeUnit.MINUTES);
private enum Action { CONTINUE, ABORT }
private final HintsReader reader;
final UUID hostId;
@Nullable
final InetAddressAndPort address;
private final int messagingVersion;
private final BooleanSupplier abortRequested;
private InputPosition currentPagePosition;
private HintsDispatcher(HintsReader reader, UUID hostId, InetAddressAndPort address, int messagingVersion, BooleanSupplier abortRequested)
// Hints from the batch log that were attempted on Accord don't have a list of hosts that need hinting
// since Accord doesn't expose that on failure. If Accord no longer manages the range for this hint then we need
// to send the hint to all replicas after the page succeeds
private final Queue<Hint> hintsNeedingRehinting = new LinkedList<>();
private HintsDispatcher(HintsReader reader, UUID hostId, @Nullable InetAddressAndPort address, int messagingVersion, BooleanSupplier abortRequested)
{
checkArgument(address != null ^ hostId.equals(RETRY_ON_DIFFERENT_SYSTEM_UUID), "address must be nonnull or hostId must be " + RETRY_ON_DIFFERENT_SYSTEM_UUID);
currentPagePosition = null;
this.reader = reader;
@ -78,9 +120,9 @@ final class HintsDispatcher implements AutoCloseable
this.abortRequested = abortRequested;
}
static HintsDispatcher create(File file, RateLimiter rateLimiter, InetAddressAndPort address, UUID hostId, BooleanSupplier abortRequested)
static HintsDispatcher create(File file, RateLimiter rateLimiter, @Nullable InetAddressAndPort address, UUID hostId, BooleanSupplier abortRequested)
{
int messagingVersion = MessagingService.instance().versions.get(address);
int messagingVersion = address == null ? MessagingService.current_version : MessagingService.instance().versions.get(address);
HintsDispatcher dispatcher = new HintsDispatcher(HintsReader.open(file, rateLimiter), hostId, address, messagingVersion, abortRequested);
HintDiagnostics.dispatcherCreated(dispatcher);
return dispatcher;
@ -92,6 +134,11 @@ final class HintsDispatcher implements AutoCloseable
reader.close();
}
String destination()
{
return address == null ? "RETRY_ON_DIFFERENT_SYSTEM" : address.toString();
}
void seek(InputPosition position)
{
reader.seek(position);
@ -130,7 +177,19 @@ final class HintsDispatcher implements AutoCloseable
private Action sendHintsAndAwait(HintsReader.Page page)
{
Collection<Callback> callbacks = new ArrayList<>();
try
{
return doSendHintsAndAwait(page, null);
}
finally
{
hintsNeedingRehinting.clear();
}
}
private Action doSendHintsAndAwait(HintsReader.Page page, @Nullable BitSet hintsFilter)
{
List<Callback> callbacks = new ArrayList<>();
/*
* If hints file messaging version matches the version of the target host, we'll use the optimised path -
@ -138,50 +197,135 @@ final class HintsDispatcher implements AutoCloseable
*
* If that is not the case, we'll need to perform conversion to a newer (or an older) format, and decoding the hint
* is an unavoidable intermediate step.
*
* If these hints are from the batchlog and were originally attempted on Accord then
* we also need to decode so we can route the Hint contents appropriately.
*
* If filtering of hints is requested, because this is retrying a page that had some retry on different system
* errors, then also don't go down the sendEncodedHints path since it won't re-route the mutation and will trigger
* the same retry on different system error.
*/
Action action = reader.descriptor().messagingVersion() == messagingVersion
? sendHints(page.buffersIterator(), callbacks, this::sendEncodedHint)
: sendHints(page.hintsIterator(), callbacks, this::sendHint);
boolean isBatchLogHints = hostId.equals(RETRY_ON_DIFFERENT_SYSTEM_UUID);
boolean sendEncodedHints = reader.descriptor().messagingVersion() == messagingVersion && !isBatchLogHints && hintsFilter == null;
// If the hints filter is set then splitting the hints is needed and encoded hints can't do that
checkState(!sendEncodedHints || hintsFilter == null, "Should not send encoded hints if hints filter is set");
Action action = sendEncodedHints
? sendHints(page.buffersIterator(), null, callbacks, this::sendEncodedHint)
: sendHints(page.hintsIterator(), hintsFilter, callbacks, this::sendHint);
if (action == Action.ABORT)
return action;
long success = 0, failures = 0, timeouts = 0;
for (Callback cb : callbacks)
BitSet retryDifferentSystemHints = new BitSet(callbacks.size());
long success = 0, failures = 0, timeouts = 0, retryDifferentSystem = 0;
for (int i = 0; i < callbacks.size(); i++)
{
Callback cb = callbacks.get(i);
Callback.Outcome outcome = cb.await();
if (outcome == Callback.Outcome.SUCCESS) success++;
else if (outcome == Callback.Outcome.FAILURE) failures++;
else if (outcome == Callback.Outcome.TIMEOUT) timeouts++;
else if (outcome == RETRY_DIFFERENT_SYSTEM)
{
retryDifferentSystemHints.set(i);
retryDifferentSystem++;
}
else throw new IllegalStateException("Unhandled outcome: " + outcome);
}
updateMetrics(success, failures, timeouts);
updateMetrics(success, failures, timeouts, retryDifferentSystem);
if (failures > 0 || timeouts > 0)
// If the only errors were retryDifferentSystem and we aren't already filtering the hints then retry
// immediately otherwise we will repeat the page later including any successful hints we may have already delivered
// Hints for the batch log can hit RETRY_DIFFERENT_SYSTEM but don't need to be retried here and it could result
// in the same hint ending up in hintsNeedingRehinting twice
boolean failedRetryDifferentSystem = false;
if (retryDifferentSystem > 0 && failures < 1 && timeouts < 1 && hintsFilter == null && !isBatchLogHints)
{
HintDiagnostics.pageFailureResult(this, success, failures, timeouts);
reader.seek(currentPagePosition);
Action retryResult = doSendHintsAndAwait(page, retryDifferentSystemHints);
if (retryResult != Action.CONTINUE)
failedRetryDifferentSystem = true;
}
if (failures > 0 || timeouts > 0 || failedRetryDifferentSystem)
{
HintDiagnostics.pageFailureResult(this, success, failures, timeouts, retryDifferentSystem);
return Action.ABORT;
}
else
{
HintDiagnostics.pageSuccessResult(this, success, failures, timeouts);
HintDiagnostics.pageSuccessResult(this, success, failures, timeouts, retryDifferentSystem);
rehintHintsNeedingRehinting();
return Action.CONTINUE;
}
}
private void updateMetrics(long success, long failures, long timeouts)
private void rehintHintsNeedingRehinting()
{
ClusterMetadata cm = ClusterMetadata.current();
Hint hint;
while ((hint = hintsNeedingRehinting.poll()) != null)
{
HintsService.instance.writeForAllReplicas(hint);
Mutation mutation = hint.mutation;
// Also may need to apply locally because it's possible this is from the batchlog
// and we never applied it locally
// TODO (review): Additional error handling necessary? Hints are lossy
DataPlacement dataPlacement = cm.placements.get(cm.schema.getKeyspace(mutation.getKeyspaceName()).getMetadata().params.replication);
VersionedEndpoints.ForToken forToken = dataPlacement.writes.forToken(mutation.key().getToken());
Replica self = forToken.get().selfIfPresent();
if (self != null)
{
Stage.MUTATION.maybeExecuteImmediately(new RunnableDebuggableTask()
{
private final long approxCreationTimeNanos = MonotonicClock.Global.approxTime.now();
private volatile long approxStartTimeNanos;
@Override
public void run()
{
approxStartTimeNanos = MonotonicClock.Global.approxTime.now();
mutation.apply();
}
@Override
public long creationTimeNanos()
{
return approxCreationTimeNanos;
}
@Override
public long startTimeNanos()
{
return approxStartTimeNanos;
}
@Override
public String description()
{
return "HintsService rehinting Accord txn";
}
});
}
}
}
private void updateMetrics(long success, long failures, long timeouts, long retryDifferentSystem)
{
HintsServiceMetrics.hintsSucceeded.mark(success);
HintsServiceMetrics.hintsFailed.mark(failures);
HintsServiceMetrics.hintsTimedOut.mark(timeouts);
HintsServiceMetrics.hintsRetryDifferentSystem.mark(retryDifferentSystem);
}
/*
* Sending hints in compatibility mode.
*/
private <T> Action sendHints(Iterator<T> hints, Collection<Callback> callbacks, Function<T, Callback> sendFunction)
private <T> Action sendHints(Iterator<T> hints, @Nullable BitSet hintsFilter, Collection<Callback> callbacks, Function<T, Callback> sendFunction)
{
int hintIndex = -1;
while (hints.hasNext())
{
if (abortRequested.getAsBoolean())
@ -189,19 +333,95 @@ final class HintsDispatcher implements AutoCloseable
HintDiagnostics.abortRequested(this);
return Action.ABORT;
}
callbacks.add(sendFunction.apply(hints.next()));
T hint = hints.next();
hintIndex++;
if (hintsFilter != null && !hintsFilter.get(hintIndex))
continue;
callbacks.add(sendFunction.apply(hint));
}
return Action.CONTINUE;
}
private Callback sendHint(Hint hint)
{
Callback callback = new Callback(hint.creationTime);
Message<?> message = Message.out(HINT_REQ, new HintMessage(hostId, hint));
MessagingService.instance().sendWithCallback(message, address, callback);
ClusterMetadata cm = ClusterMetadata.current();
SplitHint splitHint = splitHintIntoAccordAndNormal(cm, hint);
Mutation accordHintMutation = splitHint.accordMutation;
Dispatcher.RequestTime requestTime = null;
AsyncTxnResult accordTxnResult = null;
if (accordHintMutation != null)
{
requestTime = Dispatcher.RequestTime.forImmediateExecution();
accordTxnResult = accordHintMutation != null ? ConsensusMigrationMutationHelper.instance().mutateWithAccordAsync(cm, accordHintMutation, null, requestTime) : null;
}
Hint normalHint = splitHint.normalHint;
Callback callback = new Callback(address, hint.creationTime, requestTime, accordTxnResult);
if (normalHint != null)
{
// We had a hint that was supposed to be done on Accord for the batch log (otherwise address would be non-null),
// but Accord no longer manages that table/range and now we don't know which nodes (if any) are missing the Mutation.
// Convert them to per replica hints *after* all the hints in this page have been applied so we can be reasonably sure
// this page isn't going to be played again thus avoiding any futher amplification from the same hint being
// replayed and repeatedly converted to per replica hints
if (address == null)
{
checkState(hostId.equals(RETRY_ON_DIFFERENT_SYSTEM_UUID), "If there is no address to send the hint to then the host ID should be BATCHLOG_ACCORD_HINT_UUID");
callback.onResponse(null);
hintsNeedingRehinting.add(normalHint);
}
else
{
Message<?> message = Message.out(HINT_REQ, new HintMessage(hostId, normalHint));
MessagingService.instance().sendWithCallback(message, address, callback);
}
}
else
{
// Don't wait for a normal response that will never come since no hints were sent
callback.onResponse(null);
}
return callback;
}
/**
* Result of splitting a hint across Accord and non-transactional boundaries
*/
private class SplitHint
{
private final Mutation accordMutation;
private final Hint normalHint;
public SplitHint(Mutation accordMutation, Hint normalHint)
{
this.accordMutation = accordMutation;
this.normalHint = normalHint;
}
@Override
public String toString()
{
return "SplitHint{" +
"accordMutation=" + accordMutation +
", normalHint=" + normalHint +
'}';
}
}
private SplitHint splitHintIntoAccordAndNormal(ClusterMetadata cm, Hint hint)
{
SplitMutation<Mutation> splitMutation = ConsensusMigrationMutationHelper.instance().splitMutationIntoAccordAndNormal(hint.mutation, cm);
if (splitMutation.accordMutation == null)
return new SplitHint(null, hint);
if (splitMutation.normalMutation == null)
return new SplitHint(splitMutation.accordMutation, null);
Hint normalHint = Hint.create(splitMutation.normalMutation, hint.creationTime, splitMutation.normalMutation.smallestGCGS());
return new SplitHint(splitMutation.accordMutation, normalHint);
}
/*
* Sending hints in raw mode.
*/
@ -209,23 +429,41 @@ final class HintsDispatcher implements AutoCloseable
private Callback sendEncodedHint(ByteBuffer hint)
{
HintMessage.Encoded message = new HintMessage.Encoded(hostId, hint, messagingVersion);
Callback callback = new Callback(message.getHintCreationTime());
Callback callback = new Callback(address, message.getHintCreationTime());
MessagingService.instance().sendWithCallback(Message.out(HINT_REQ, message), address, callback);
return callback;
}
static final class Callback implements RequestCallback
static final class Callback implements RequestCallback, Runnable
{
enum Outcome { SUCCESS, TIMEOUT, FAILURE, INTERRUPTED }
enum Outcome { SUCCESS, TIMEOUT, FAILURE, INTERRUPTED, RETRY_DIFFERENT_SYSTEM }
private final long start = approxTime.now();
private final Condition condition = newOneTimeCondition();
private volatile Outcome outcome;
private Outcome normalOutcome;
private Outcome accordOutcome;
@Nullable
private final InetAddressAndPort to;
private final long hintCreationNanoTime;
@Nullable
private final Dispatcher.RequestTime requestTime;
private final AsyncTxnResult accordTxnResult;
private Callback(long hintCreationTimeMillisSinceEpoch)
private Callback(@Nonnull InetAddressAndPort to, long hintCreationTimeMillisSinceEpoch)
{
this(to, hintCreationTimeMillisSinceEpoch, null, null);
}
private Callback(@Nullable InetAddressAndPort to, long hintCreationTimeMillisSinceEpoch, Dispatcher.RequestTime requestTime, @Nullable AsyncTxnResult accordTxnResult)
{
this.to = to != null ? to : ACCORD_HINT_ENDPOINT;
this.hintCreationNanoTime = approxTime.translate().fromMillisSinceEpoch(hintCreationTimeMillisSinceEpoch);
this.requestTime = requestTime;
this.accordTxnResult = accordTxnResult;
if (accordTxnResult != null)
accordTxnResult.addListener(this, ImmediateExecutor.INSTANCE);
else
accordOutcome = SUCCESS;
}
Outcome await()
@ -240,8 +478,31 @@ final class HintsDispatcher implements AutoCloseable
logger.warn("Hint dispatch was interrupted", e);
return INTERRUPTED;
}
normalOutcome = timedOut ? TIMEOUT : normalOutcome;
return timedOut ? TIMEOUT : outcome;
return outcome();
}
private Outcome outcome()
{
checkState((normalOutcome != null && accordOutcome != null) || (normalOutcome != SUCCESS || accordOutcome != SUCCESS), "Outcome for both normal and accord hint delivery should be known");
if (normalOutcome == RETRY_DIFFERENT_SYSTEM || accordOutcome == RETRY_DIFFERENT_SYSTEM)
return RETRY_DIFFERENT_SYSTEM;
if (normalOutcome == TIMEOUT || accordOutcome == TIMEOUT)
return TIMEOUT;
if (normalOutcome == FAILURE || accordOutcome == FAILURE)
return FAILURE;
checkState(normalOutcome == SUCCESS && accordOutcome == SUCCESS, "Hint delivery should have been successful");
return SUCCESS;
}
private synchronized void maybeSignal()
{
if ((normalOutcome != null && accordOutcome != null) || normalOutcome == FAILURE || accordOutcome == FAILURE)
{
updateDelayMetrics(to, approxTime.now() - this.hintCreationNanoTime);
condition.signalAll();
}
}
@Override
@ -253,16 +514,40 @@ final class HintsDispatcher implements AutoCloseable
@Override
public void onFailure(InetAddressAndPort from, RequestFailure failureMessage)
{
outcome = FAILURE;
condition.signalAll();
if (failureMessage.reason == RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM)
normalOutcome = RETRY_DIFFERENT_SYSTEM;
else
normalOutcome = FAILURE;
maybeSignal();
}
@Override
public void onResponse(Message msg)
{
updateDelayMetrics(msg.from(), approxTime.now() - this.hintCreationNanoTime);
outcome = SUCCESS;
condition.signalAll();
normalOutcome = SUCCESS;
maybeSignal();
}
@Override
public void run()
{
try
{
IAccordService accord = AccordService.instance();
TxnResult.Kind kind = accord.getTxnResult(accordTxnResult, true, null, requestTime).kind();
if (kind == retry_new_protocol)
accordOutcome = RETRY_DIFFERENT_SYSTEM;
else
accordOutcome = SUCCESS;
}
catch (Exception e)
{
accordOutcome = e instanceof WriteTimeoutException ? TIMEOUT : FAILURE;
String msg = "Accord hint delivery transaction failed";
if (noSpamLogger.getStatement(msg).shouldLog(Clock.Global.nanoTime()))
logger.error(msg, e);
}
maybeSignal();
}
}
}

View File

@ -33,30 +33,32 @@ import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.utils.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.gms.FailureDetector;
import org.apache.cassandra.gms.IFailureDetector;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.metrics.HintedHandoffMetrics;
import org.apache.cassandra.metrics.StorageMetrics;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.MBeanWrapper;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static org.apache.cassandra.config.CassandraRelevantProperties.HINT_DISPATCH_INTERVAL_MS;
/**
* A singleton-ish wrapper over various hints components:
@ -70,12 +72,35 @@ import static com.google.common.collect.Iterables.transform;
*/
public final class HintsService implements HintsServiceMBean
{
private static final Logger logger = LoggerFactory.getLogger(HintsService.class);
// Dummy address to use for storing metrics for hints that will be retried on a different transaction system
// and aren't being sent to a specific node
public static final InetAddressAndPort RETRY_ON_DIFFERENT_SYSTEM_ADDRESS;
static
{
try
{
RETRY_ON_DIFFERENT_SYSTEM_ADDRESS = InetAddressAndPort.getByNameOverrideDefaults("0.0.0.0", 65535);
}
catch (UnknownHostException e)
{
throw new RuntimeException(e);
}
}
// Batch log replay may need to route some mutations to Accord which may fail and Hints are used for retry by the batch log.
// Write them to this endpoint which indicates that on replay hints will need to calculate the endpoints
// to deliver to since it's not really a per node hint, but part of a batch that needs replaying.
// This can also occur with regular mutations as well when some replicas return a retry error but quorum
// is reached so hinting is used to bring the other replicas up to date
public static final UUID RETRY_ON_DIFFERENT_SYSTEM_UUID = TimeUUID.atUnixMicrosWithLsbAsUUID(-1, -1);
public static HintsService instance = new HintsService();
public static final String MBEAN_NAME = "org.apache.cassandra.hints:type=HintsService";
private static final Logger logger = LoggerFactory.getLogger(HintsService.class);
private static final int MIN_BUFFER_SIZE = 32 << 20;
static final ImmutableMap<String, Object> EMPTY_PARAMS = ImmutableMap.of();
@ -226,7 +251,8 @@ public final class HintsService implements HintsServiceMBean
HintsDispatchTrigger trigger = new HintsDispatchTrigger(catalog, writeExecutor, dispatchExecutor, isDispatchPaused);
// triggering hint dispatch is now very cheap, so we can do it more often - every 10 seconds vs. every 10 minutes,
// previously; this reduces mean time to delivery, and positively affects batchlog delivery latencies, too
triggerDispatchFuture = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(trigger, 10, 10, TimeUnit.SECONDS);
long hintDispatchIntervalMs = HINT_DISPATCH_INTERVAL_MS.getLong();
triggerDispatchFuture = ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(trigger, hintDispatchIntervalMs, hintDispatchIntervalMs, TimeUnit.MILLISECONDS);
}
public void pauseDispatch()
@ -322,6 +348,13 @@ public final class HintsService implements HintsServiceMBean
catalog.deleteAllHints();
}
@VisibleForTesting
public void deleteAllHintsUnsafe()
{
catalog.deleteAllHintsUnsafe();
bufferPool.clearUnsafe();
}
/**
* Deletes all hints for the provided destination. Doesn't make snapshots - should be used with care.
*

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.hints;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
@ -174,6 +175,8 @@ final class HintsStore
boolean isLive()
{
if (hostId.equals(HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID))
return true;
InetAddressAndPort address = address();
return address != null && FailureDetector.instance.isAlive(address);
}
@ -193,6 +196,20 @@ final class HintsStore
dispatchDequeue.offerLast(descriptor);
}
void deleteAllHintsUnsafe()
{
try
{
closeWriter();
}
catch (FSWriteError e)
{
if (!(e.getCause() instanceof ClosedChannelException))
throw e;
}
deleteAllHints();
}
void deleteAllHints()
{
HintsDescriptor descriptor;

View File

@ -39,6 +39,8 @@ public class AccordClientRequestMetrics extends ClientRequestMetrics
// Number of times a query was rejected by Accord in TxnQuery due to a migration back to Paxos
public final Meter accordMigrationRejects;
public final Meter preempted;
public final Meter topologyMismatches;
public AccordClientRequestMetrics(String scope)
{
@ -48,6 +50,8 @@ public class AccordClientRequestMetrics extends ClientRequestMetrics
migrationSkippedReads = Metrics.meter(factory.createMetricName("MigrationSkippedReads"));
paxosKeyMigrations = Metrics.meter(factory.createMetricName("PaxosKeyMigrations"));
accordMigrationRejects = Metrics.meter(factory.createMetricName("AccordMigrationRejects"));
preempted = Metrics.meter(factory.createMetricName("Preempted"));
topologyMismatches = Metrics.meter(factory.createMetricName("TopologyMismatches"));
}
@Override
@ -58,5 +62,8 @@ public class AccordClientRequestMetrics extends ClientRequestMetrics
Metrics.remove(factory.createMetricName("MigrationSkippedReads"));
Metrics.remove(factory.createMetricName("PaxosKeyMigrations"));
Metrics.remove(factory.createMetricName("AccordMigrationRejects"));
Metrics.remove(factory.createMetricName("Preempted"));
Metrics.remove(factory.createMetricName("TopologyMismatches"));
}
}

View File

@ -40,6 +40,8 @@ public class ClientRequestMetrics extends LatencyMetrics
public final Meter readSizeAborts;
public final Meter localRequests;
public final Meter remoteRequests;
public final Meter retryDifferentSystem;
public final Meter retryCoordinatorBehind;
public ClientRequestMetrics(String scope)
{
@ -53,6 +55,8 @@ public class ClientRequestMetrics extends LatencyMetrics
readSizeAborts = Metrics.meter(factory.createMetricName("ReadSizeAborts"));
localRequests = Metrics.meter(factory.createMetricName("LocalRequests"));
remoteRequests = Metrics.meter(factory.createMetricName("RemoteRequests"));
retryDifferentSystem = Metrics.meter(factory.createMetricName("RetryDifferentSystem"));
retryCoordinatorBehind = Metrics.meter(factory.createMetricName("RetryCoordinatorBehind"));
}
public void markAbort(Throwable cause)
@ -81,5 +85,7 @@ public class ClientRequestMetrics extends LatencyMetrics
Metrics.remove(factory.createMetricName("ReadSizeAborts"));
Metrics.remove(factory.createMetricName("LocalRequests"));
Metrics.remove(factory.createMetricName("RemoteRequests"));
Metrics.remove(factory.createMetricName("RetryDifferentSystem"));
Metrics.remove(factory.createMetricName("RetryCoordinatorBehind"));
}
}

View File

@ -17,6 +17,8 @@
*/
package org.apache.cassandra.metrics;
import java.net.UnknownHostException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,6 +37,22 @@ import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
public final class HintsServiceMetrics
{
public static final String TYPE_NAME = "HintsService";
// Hint metrics are by address and hints that are for Accord need an address
public static final InetAddressAndPort ACCORD_HINT_ENDPOINT;
static
{
try
{
ACCORD_HINT_ENDPOINT = InetAddressAndPort.getByNameOverrideDefaults("0.0.0.0", 0);
}
catch (UnknownHostException e)
{
throw new RuntimeException(e);
}
}
private static final Logger logger = LoggerFactory.getLogger(HintsServiceMetrics.class);
private static final MetricNameFactory factory = new DefaultNameFactory(TYPE_NAME);
@ -42,6 +60,8 @@ public final class HintsServiceMetrics
public static final Meter hintsSucceeded = Metrics.meter(factory.createMetricName("HintsSucceeded"));
public static final Meter hintsFailed = Metrics.meter(factory.createMetricName("HintsFailed"));
public static final Meter hintsTimedOut = Metrics.meter(factory.createMetricName("HintsTimedOut"));
public static final Meter hintsRetryDifferentSystem = Metrics.meter(factory.createMetricName("HintsRetryDifferentSystem"));
/** Histogram of all hint delivery delays */
private static final Histogram globalDelayHistogram = Metrics.histogram(factory.createMetricName("Hint_delays"), false);
@ -62,4 +82,9 @@ public final class HintsServiceMetrics
globalDelayHistogram.update(delay);
delayByEndpoint.get(endpoint).update(delay);
}
public static long getDelayCount(InetAddressAndPort endpoint)
{
return delayByEndpoint.get(endpoint).getCount();
}
}

View File

@ -107,6 +107,7 @@ public class KeyspaceMetrics
public final LatencyMetrics rangeMigration;
public final Meter rangeMigrationUnexpectedFailures;
public final Meter rangeMigrationDependencyLimitFailures;
public final Meter mutationsRejectedOnWrongSystem;
/** Writes failed ideal consistency **/
public final Counter writeFailedIdealCL;
/** Ideal CL write latency metrics */
@ -257,6 +258,7 @@ public class KeyspaceMetrics
rangeMigration = createLatencyMetrics("RangeMigration");
rangeMigrationUnexpectedFailures = createKeyspaceMeter("RangeMigrationUnexpectedFailures");
rangeMigrationDependencyLimitFailures = createKeyspaceMeter("RangeMigratingDependencyLimitFailures");
mutationsRejectedOnWrongSystem = createKeyspaceMeter("MutationsRejectedOnWrongSystem");
writeFailedIdealCL = createKeyspaceCounter("WriteFailedIdealCL");
idealCLWriteLatency = createLatencyMetrics("IdealCLWrite");

View File

@ -196,6 +196,7 @@ public class TableMetrics
public final LatencyMetrics accordRepair;
public final TableMeter accordRepairUnexpectedFailures;
public final TableMeter accordRepairDependencyLimitFailures;
public final TableMeter mutationsRejectedOnWrongSystem;
/** percent of the data that is repaired */
public final Gauge<Double> percentRepaired;
/** Reports the size of sstables in repaired, unrepaired, and any ongoing repair buckets */
@ -817,6 +818,7 @@ public class TableMetrics
accordRepair = createLatencyMetrics("AccordRepair", cfs.keyspace.metric.rangeMigration, GLOBAL_RANGE_MIGRATION_LATENCY);
accordRepairUnexpectedFailures = createTableMeter("AccordRepairUnexpectedFailures", cfs.keyspace.metric.rangeMigrationUnexpectedFailures);
accordRepairDependencyLimitFailures = createTableMeter("AccordRepairDependencyLimitFaiures", cfs.keyspace.metric.rangeMigrationDependencyLimitFailures);
mutationsRejectedOnWrongSystem = createTableMeter("MutationsRejectedOnWrongSystem", cfs.keyspace.metric.mutationsRejectedOnWrongSystem);
repairsStarted = createTableCounter("RepairJobsStarted");
repairsCompleted = createTableCounter("RepairJobsCompleted");

View File

@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tracing.Tracing;
@ -33,6 +34,7 @@ import org.apache.cassandra.utils.FBUtilities;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.apache.cassandra.exceptions.RequestFailureReason.COORDINATOR_BEHIND;
import static org.apache.cassandra.exceptions.RequestFailureReason.INVALID_ROUTING;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
class ResponseVerbHandler implements IVerbHandler
@ -101,8 +103,11 @@ class ResponseVerbHandler implements IVerbHandler
// Gossip stage is single-threaded, so we may end up in a deadlock with after-commit hook
// that executes something on the gossip stage as well.
if (message.isFailureResponse() &&
(message.payload == COORDINATOR_BEHIND || message.payload == INVALID_ROUTING) &&
boolean isFailureResponse = message.isFailureResponse();
// RequestFailure is not a singleton so we need to extract and compare against the reason
RequestFailureReason reason = isFailureResponse ? ((RequestFailure)message.payload).reason : null;
if (isFailureResponse &&
(reason == COORDINATOR_BEHIND || reason == INVALID_ROUTING || reason == RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM) &&
// Gossip stage is single-threaded, so we may end up in a deadlock with after-commit hook
// that executes something on the gossip stage as well.
!Stage.GOSSIP.executor().inExecutor())

View File

@ -39,6 +39,7 @@ import com.google.common.util.concurrent.FutureCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.primitives.Ranges;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
@ -68,6 +69,7 @@ import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.FutureCombiner;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import static com.google.common.util.concurrent.Futures.getUnchecked;
import static org.apache.cassandra.config.DatabaseDescriptor.paxosRepairEnabled;
import static org.apache.cassandra.schema.SchemaConstants.METADATA_KEYSPACE_NAME;
import static org.apache.cassandra.service.paxos.Paxos.useV2;
@ -193,7 +195,7 @@ public class RepairJob extends AsyncFuture<RepairResult> implements Runnable
return;
}
Future<Void> accordRepair;
Future<Ranges> accordRepair;
if (doAccordRepair)
{
accordRepair = paxosRepair.flatMap(unused -> {
@ -213,12 +215,12 @@ public class RepairJob extends AsyncFuture<RepairResult> implements Runnable
if (session.accordOnly)
{
accordRepair.addCallback(new FutureCallback<Void>()
accordRepair.addCallback(new FutureCallback<>()
{
public void onSuccess(Void ignored)
public void onSuccess(Ranges barrieredRanges)
{
logger.info("{} {}.{} accord repair completed", session.previewKind.logPrefix(session.getId()), desc.keyspace, desc.columnFamily);
trySuccess(new RepairResult(desc, Collections.emptyList(), ConsensusMigrationRepairResult.fromAccordOnlyRepair(repairStartingEpoch, session.excludedDeadNodes)));
trySuccess(new RepairResult(desc, Collections.emptyList(), ConsensusMigrationRepairResult.fromAccordOnlyRepair(repairStartingEpoch, barrieredRanges, session.excludedDeadNodes)));
}
public void onFailure(Throwable t)
@ -282,7 +284,7 @@ public class RepairJob extends AsyncFuture<RepairResult> implements Runnable
}
cfs.metric.repairsCompleted.inc();
logger.info("Completing repair with excludedDeadNodes {}", session.excludedDeadNodes);
trySuccess(new RepairResult(desc, stats, ConsensusMigrationRepairResult.fromRepair(repairStartingEpoch, doPaxosRepair, doAccordRepair, session.excludedDeadNodes)));
trySuccess(new RepairResult(desc, stats, ConsensusMigrationRepairResult.fromRepair(repairStartingEpoch, getUnchecked(accordRepair), doPaxosRepair, doAccordRepair, session.excludedDeadNodes)));
}
/**
@ -307,7 +309,7 @@ public class RepairJob extends AsyncFuture<RepairResult> implements Runnable
}, taskExecutor);
}
private Future<List<SyncTask>> createSyncTasks(Future<Void> accordRepair, Future<?> allSnapshotTasks, List<InetAddressAndPort> allEndpoints)
private Future<List<SyncTask>> createSyncTasks(Future<Ranges> accordRepair, Future<?> allSnapshotTasks, List<InetAddressAndPort> allEndpoints)
{
Future<List<TreeResponse>> treeResponses;
if (allSnapshotTasks != null)

View File

@ -18,20 +18,8 @@
package org.apache.cassandra.schema;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.auth.AuthKeyspace;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.functions.UserFunction;
@ -47,6 +35,18 @@ import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.tracing.TraceKeyspace;
import org.apache.cassandra.utils.FBUtilities;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import static org.apache.cassandra.db.TypeSizes.sizeof;
/**
@ -130,6 +130,11 @@ public class DistributedSchema implements MetadataValue<DistributedSchema>
return keyspaces.get(keyspace).get();
}
public Optional<KeyspaceMetadata> maybeGetKeyspaceMetadata(String keyspace)
{
return keyspaces.get(keyspace);
}
public TableMetadata getTableMetadata(TableId id)
{
return tables.get(id);

View File

@ -25,7 +25,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.slf4j.Logger;
@ -36,8 +35,10 @@ import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.EndpointsForToken;
@ -51,6 +52,7 @@ import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.concurrent.Condition;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static java.lang.Long.MAX_VALUE;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
@ -59,6 +61,8 @@ import static java.util.stream.Collectors.toList;
import static org.apache.cassandra.config.DatabaseDescriptor.getCounterWriteRpcTimeout;
import static org.apache.cassandra.config.DatabaseDescriptor.getWriteRpcTimeout;
import static org.apache.cassandra.db.WriteType.COUNTER;
import static org.apache.cassandra.exceptions.RequestFailureReason.COORDINATOR_BEHIND;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
import static org.apache.cassandra.locator.Replicas.countInOurDc;
import static org.apache.cassandra.schema.Schema.instance;
import static org.apache.cassandra.service.StorageProxy.WritePerformer;
@ -79,6 +83,10 @@ public abstract class AbstractWriteResponseHandler<T> implements RequestCallback
private static final AtomicIntegerFieldUpdater<AbstractWriteResponseHandler> failuresUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractWriteResponseHandler.class, "failures");
private volatile int failures = 0;
private static final AtomicIntegerFieldUpdater<AbstractWriteResponseHandler> alreadyHintedForRetryOnDifferentSystemUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractWriteResponseHandler.class, "alreadyHintedForRetryOnDifferentSystem");
// Only write a hint to be applied as a transaction once
private volatile int alreadyHintedForRetryOnDifferentSystem = 0;
private volatile Map<InetAddressAndPort, RequestFailureReason> failureReasonByEndpoint;
private final Dispatcher.RequestTime requestTime;
private @Nullable final Supplier<Mutation> hintOnFailure;
@ -111,7 +119,7 @@ public abstract class AbstractWriteResponseHandler<T> implements RequestCallback
this.requestTime = requestTime;
}
public void get() throws WriteTimeoutException, WriteFailureException
public void get() throws WriteTimeoutException, WriteFailureException, RetryOnDifferentSystemException
{
long timeoutNanos = currentTimeoutNanos();
@ -128,14 +136,41 @@ public abstract class AbstractWriteResponseHandler<T> implements RequestCallback
if (!signaled)
throwTimeout();
if (blockFor() + failures > candidateReplicaCount())
int candidateReplicaCount = candidateReplicaCount();
if (blockFor() + failures > candidateReplicaCount)
{
if (RequestCallback.isTimeout(this.getFailureReasonByEndpointMap().keySet().stream()
.filter(this::waitingFor) // DatacenterWriteResponseHandler filters errors from remote DCs
.collect(Collectors.toMap(Function.identity(), this.getFailureReasonByEndpointMap()::get))))
// failures keeps incrementing, and this.failureReasonByEndpoint keeps getting new entries after signaling.
// Simpler to reason about what happened by copying this.failureReasonByEndpoint and then inferring
// failures from it
final Map<InetAddressAndPort, RequestFailureReason> failureReasonByEndpoint = getFailureReasonByEndpointMap().keySet().stream()
.filter(this::waitingFor) // DatacenterWriteResponseHandler filters errors from remote DCs
.collect(toImmutableMap(Function.identity(), getFailureReasonByEndpointMap()::get));
final int failures = failureReasonByEndpoint.size();
if (RequestCallback.isTimeout(failureReasonByEndpoint))
throwTimeout();
throw new WriteFailureException(replicaPlan.consistencyLevel(), ackCount(), blockFor(), writeType, this.getFailureReasonByEndpointMap());
int transactionRetryErrors = 0;
int coordinatorBehindErrors = 0;
for (RequestFailureReason reason : failureReasonByEndpoint.values())
{
if (reason == RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM)
transactionRetryErrors++;
if (reason == COORDINATOR_BEHIND)
coordinatorBehindErrors++;
}
int totalRetriableFailures = transactionRetryErrors + coordinatorBehindErrors;
// Retrying might fix this
if (candidateReplicaCount - failures + totalRetriableFailures >= blockFor())
{
// Doesn't matter which we throw really but for clarity/metrics be specific
// Retrying on the correct system might make this write succeed
if (transactionRetryErrors > 0)
throw new RetryOnDifferentSystemException();
throw new CoordinatorBehindException("Write request failed due to coordinator behind");
}
throw new WriteFailureException(replicaPlan.consistencyLevel(), ackCount(), blockFor(), writeType, getFailureReasonByEndpointMap());
}
if (replicaPlan.stillAppliesTo(ClusterMetadata.current()))
@ -298,7 +333,7 @@ public abstract class AbstractWriteResponseHandler<T> implements RequestCallback
@Override
public void onFailure(InetAddressAndPort from, RequestFailure failure)
{
logger.trace("Got failure from {}", from);
logger.trace("Got failure {} from {}", failure, from);
int n = waitingFor(from)
? failuresUpdater.incrementAndGet(this)
@ -317,8 +352,20 @@ public abstract class AbstractWriteResponseHandler<T> implements RequestCallback
if (blockFor() + n > candidateReplicaCount())
signal();
if (hintOnFailure != null && StorageProxy.shouldHint(replicaPlan.lookup(from)) && requestTime.shouldSendHints())
StorageProxy.submitHint(hintOnFailure.get(), replicaPlan.lookup(from), null);
// If the failure was RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM then we only want to hint once
// and not for each instance since odds are it will be applied as a transaction at all replicas
if (hintOnFailure != null && StorageProxy.shouldHint(replicaPlan.lookup(from)) )
{
if (failure.reason == RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM)
{
if (alreadyHintedForRetryOnDifferentSystemUpdater.compareAndSet(this, 0, 1))
StorageProxy.submitHintForRetryOnDifferentSystem(hintOnFailure.get());
}
else
{
StorageProxy.submitHint(hintOnFailure.get(), replicaPlan.lookup(from), null);
}
}
}
@Override

View File

@ -29,9 +29,10 @@ import org.apache.cassandra.transport.Dispatcher;
public class BatchlogResponseHandler<T> extends AbstractWriteResponseHandler<T>
{
AbstractWriteResponseHandler<T> wrapped;
BatchlogCleanup cleanup;
final AbstractWriteResponseHandler<T> wrapped;
final BatchlogCleanup cleanup;
protected volatile int requiredBeforeFinish;
private static final AtomicIntegerFieldUpdater<BatchlogResponseHandler> requiredBeforeFinishUpdater
= AtomicIntegerFieldUpdater.newUpdater(BatchlogResponseHandler.class, "requiredBeforeFinish");
@ -104,6 +105,11 @@ public class BatchlogResponseHandler<T> extends AbstractWriteResponseHandler<T>
this.callback = callback;
}
public BatchlogCleanup(BatchlogCleanupCallback callback)
{
this.callback = callback;
}
public int decrement()
{
return mutationsWaitingForUpdater.decrementAndGet(this);
@ -114,6 +120,11 @@ public class BatchlogResponseHandler<T> extends AbstractWriteResponseHandler<T>
if (decrement() == 0)
callback.invoke();
}
public void setMutationsWaitingFor(int mutationsWaitingFor)
{
mutationsWaitingForUpdater.lazySet(this, mutationsWaitingFor);
}
}
public interface BatchlogCleanupCallback

View File

@ -31,7 +31,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@ -42,14 +41,13 @@ import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheLoader;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.primitives.Keys;
import accord.primitives.Txn;
import accord.utils.Invariants;
import org.apache.cassandra.batchlog.Batch;
@ -58,7 +56,6 @@ import org.apache.cassandra.concurrent.DebuggableTask.RunnableDebuggableTask;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
@ -88,6 +85,7 @@ import org.apache.cassandra.db.view.ViewUtils;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
import org.apache.cassandra.exceptions.CasWriteUnknownResultException;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.IsBootstrappingException;
import org.apache.cassandra.exceptions.OverloadedException;
@ -98,6 +96,7 @@ import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureException;
import org.apache.cassandra.exceptions.RequestTimeoutException;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.exceptions.UnavailableException;
import org.apache.cassandra.exceptions.WriteFailureException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
@ -131,15 +130,14 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.IAccordService;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.txn.TxnCondition;
import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult;
import org.apache.cassandra.service.accord.txn.TxnData;
import org.apache.cassandra.service.accord.txn.TxnQuery;
import org.apache.cassandra.service.accord.txn.TxnRead;
import org.apache.cassandra.service.accord.txn.TxnReferenceOperations;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.accord.txn.TxnUpdate;
import org.apache.cassandra.service.accord.txn.TxnWrite;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.SplitConsumer;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.SplitMutations;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
import org.apache.cassandra.service.paxos.Ballot;
import org.apache.cassandra.service.paxos.Commit;
@ -165,8 +163,10 @@ import org.apache.cassandra.utils.MBeanWrapper;
import org.apache.cassandra.utils.MonotonicClock;
import org.apache.cassandra.utils.NoSpamLogger;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.Throwables;
import org.apache.cassandra.utils.TimeUUID;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.primitives.Txn.Kind.EphemeralRead;
@ -197,6 +197,8 @@ import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult.R
import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult.casResult;
import static org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult.serialReadResult;
import static org.apache.cassandra.service.accord.txn.TxnResult.Kind.retry_new_protocol;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.mutateWithAccordAsync;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper.splitMutationsIntoAccordAndNormal;
import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.ConsensusRoutingDecision;
import static org.apache.cassandra.service.paxos.Ballot.Flag.GLOBAL;
import static org.apache.cassandra.service.paxos.Ballot.Flag.LOCAL;
@ -207,6 +209,8 @@ import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
import static org.apache.cassandra.utils.LocalizeString.toUpperCaseLocalized;
import static org.apache.cassandra.utils.MonotonicClock.Global.approxTime;
import static org.apache.cassandra.utils.Throwables.getStackTraceAsToString;
import static org.apache.cassandra.utils.Throwables.unchecked;
import static org.apache.cassandra.utils.TimeUUID.Generator.nextTimeUUID;
import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch;
import static org.apache.commons.lang3.StringUtils.join;
@ -1184,54 +1188,6 @@ public class StorageProxy implements StorageProxyMBean
}
}
private static ConsistencyLevel consistencyLevelForCommit(Collection<? extends IMutation> mutations, ConsistencyLevel consistencyLevel)
{
ConsistencyLevel result = null;
for (IMutation mutation : mutations)
{
for (TableId tableId : mutation.getTableIds())
{
TransactionalMode mode = Schema.instance.getTableMetadata(tableId).params.transactionalMode;
ConsistencyLevel commitCL = mode.commitCLForStrategy(consistencyLevel);
if (result == null || commitCL.compareTo(result) > 0)
result = commitCL;
}
}
return result;
}
private static boolean writesThroughAccord(List<? extends IMutation> mutations, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
boolean accordWrite = false;
boolean normalWrite = false;
for (int i=0,mi=mutations.size(); i<mi; i++)
{
IMutation mutation = mutations.get(i);
for (TableId tableId : mutation.getTableIds())
{
if (ConsensusRequestRouter.instance.shouldWriteThroughAccordAndMaybeMigrate(mutation.key(),
tableId,
consistencyLevel,
requestTime,
DatabaseDescriptor.getWriteRpcTimeout(NANOSECONDS),
true))
{
accordWrite = true;
}
else
{
normalWrite = true;
}
}
}
if (!accordWrite && !normalWrite)
throw new IllegalStateException("Mutation is neither a normal write nor an accord write");
if (accordWrite && normalWrite)
throw new InvalidRequestException("Cannot mix writes to accord and normal tables");
return accordWrite;
}
@SuppressWarnings("unchecked")
public static void mutateWithTriggers(List<? extends IMutation> mutations,
ConsistencyLevel consistencyLevel,
@ -1258,53 +1214,140 @@ public class StorageProxy implements StorageProxyMBean
}
}
Collection<Mutation> augmented = TriggerExecutor.instance.execute(mutations);
List<Mutation> augmented = TriggerExecutor.instance.execute(mutations);
String keyspaceName = mutations.iterator().next().getKeyspaceName();
boolean updatesView = Keyspace.open(mutations.iterator().next().getKeyspaceName())
boolean updatesView = Keyspace.open(keyspaceName)
.viewManager
.updatesAffectView(mutations, true);
long size = IMutation.dataSize(mutations);
long size = IMutation.dataSize(augmented != null ? augmented : mutations);
writeMetrics.mutationSize.update(size);
writeMetricsForLevel(consistencyLevel).mutationSize.update(size);
if (writesThroughAccord(mutations, consistencyLevel, requestTime))
{
Preconditions.checkState(!SchemaConstants.getSystemKeyspaces().contains(keyspaceName));
mutateWithAccord(augmented != null ? augmented : mutations, consistencyLevel, requestTime);
}
else if (augmented != null)
mutateAtomically(augmented, consistencyLevel, updatesView, requestTime);
if (augmented != null || mutateAtomically || updatesView)
mutateAtomically(augmented != null ? augmented : (List<Mutation>)mutations, consistencyLevel, updatesView, requestTime);
else
dispatchMutationsWithRetryOnDifferentSystem(mutations, consistencyLevel, requestTime);
}
public static void dispatchMutationsWithRetryOnDifferentSystem(List<? extends IMutation> mutations, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
while (true)
{
if (mutateAtomically || updatesView)
mutateAtomically((Collection<Mutation>) mutations, consistencyLevel, updatesView, requestTime);
else
mutate(mutations, consistencyLevel, requestTime);
ClusterMetadata cm = ClusterMetadata.current();
try
{
SplitMutations splitMutations = splitMutationsIntoAccordAndNormal(cm, (List<IMutation>)mutations);
List<? extends IMutation> accordMutations = splitMutations.accordMutations();
AsyncTxnResult accordResult = accordMutations != null ? mutateWithAccordAsync(cm, accordMutations, consistencyLevel, requestTime) : null;
List<? extends IMutation> normalMutations = splitMutations.normalMutations();
Tracing.trace("Split mutations into Accord {} and normal {}", accordMutations, normalMutations);
Throwable failure = null;
try
{
if (normalMutations != null)
{
mutate(normalMutations, consistencyLevel, requestTime);
Tracing.trace("Successfully wrote normal mutations");
}
}
catch (RetryOnDifferentSystemException e)
{
writeMetrics.retryDifferentSystem.mark();
writeMetricsForLevel(consistencyLevel).retryDifferentSystem.mark();
logger.debug("Retrying mutations on different system because some mutations were misrouted");
Tracing.trace("Got {} from normal mutations, will retry", e);
continue;
}
catch (CoordinatorBehindException e)
{
writeMetrics.retryCoordinatorBehind.mark();
writeMetricsForLevel(consistencyLevel).retryCoordinatorBehind.mark();
mutations.forEach(IMutation::clearCachedSerializationsForRetry);
logger.debug("Retrying mutations now that coordinator has caught up to cluster metadata");
Tracing.trace("Got {} from normal mutations, will retry", e);
continue;
}
catch (Exception e)
{
failure = Throwables.merge(failure, e);
}
// Check if the Accord mutations succeeded asynchronously
try
{
if (accordResult != null)
{
IAccordService accord = AccordService.instance();
TxnResult.Kind kind = accord.getTxnResult(accordResult, true, consistencyLevel, requestTime).kind();
if (kind == retry_new_protocol)
continue;
Tracing.trace("Successfully wrote Accord mutations");
}
}
catch (Exception e)
{
failure = Throwables.merge(failure, e);
}
if (failure != null)
throw unchecked(failure);
}
catch (Exception t)
{
// Unexpected error so it would be helpful to have details
Tracing.trace("{}", getStackTraceAsToString(t));
throw t;
}
break;
}
}
private static void mutateWithAccord(Collection<? extends IMutation> mutations, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
private static ConsistencyLevel consistencyLevelForBatchLog(ConsistencyLevel consistencyLevel, boolean requireQuorumForRemove)
{
int fragmentIndex = 0;
List<TxnWrite.Fragment> fragments = new ArrayList<>(mutations.size());
List<PartitionKey> partitionKeys = new ArrayList<>(mutations.size());
for (IMutation mutation : mutations)
// If we are requiring quorum nodes for removal, we upgrade consistency level to QUORUM unless we already
// require ALL, or EACH_QUORUM. This is so that *at least* QUORUM nodes see the update.
ConsistencyLevel batchConsistencyLevel = requireQuorumForRemove
? ConsistencyLevel.QUORUM
: consistencyLevel;
switch (consistencyLevel)
{
for (PartitionUpdate update : mutation.getPartitionUpdates())
{
PartitionKey pk = PartitionKey.of(update);
partitionKeys.add(pk);
fragments.add(new TxnWrite.Fragment(PartitionKey.of(update), fragmentIndex++, update, TxnReferenceOperations.empty()));
}
case ALL:
case EACH_QUORUM:
batchConsistencyLevel = consistencyLevel;
}
return batchConsistencyLevel;
}
private static void doFallibleWriteWithMetricTracking(Runnable r, ConsistencyLevel consistencyLevel)
{
try
{
r.run();
}
catch (UnavailableException e)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistencyLevel).unavailables.mark();
Tracing.trace("Unavailable");
throw e;
}
catch (WriteTimeoutException e)
{
writeMetrics.timeouts.mark();
writeMetricsForLevel(consistencyLevel).timeouts.mark();
Tracing.trace("Write timeout; received {} of {} required replies", e.received, e.blockFor);
throw e;
}
catch (WriteFailureException e)
{
writeMetrics.failures.mark();
writeMetricsForLevel(consistencyLevel).failures.mark();
Tracing.trace("Write failure; received {} of {} required replies", e.received, e.blockFor);
throw e;
}
// Potentially ignore commit consistency level if the strategy specifies accord and not migration
ConsistencyLevel clForCommit = consistencyLevelForCommit(mutations, consistencyLevel);
TxnUpdate update = new TxnUpdate(fragments, TxnCondition.none(), clForCommit);
Txn.InMemory txn = new Txn.InMemory(Keys.of(partitionKeys), TxnRead.EMPTY, TxnQuery.EMPTY, update);
IAccordService accordService = AccordService.instance();
accordService.coordinate(txn, consistencyLevel, requestTime);
}
/**
@ -1314,92 +1357,171 @@ public class StorageProxy implements StorageProxyMBean
* After: remove the batchlog entry (after writing hints for the batch rows, if necessary).
*
* @param mutations the Mutations to be applied across the replicas
* @param consistency_level the consistency level for the operation
* @param consistencyLevel the consistency level for the operation
* @param requireQuorumForRemove at least a quorum of nodes will see update before deleting batchlog
* @param requestTime object holding times when request got enqueued and started execution
*/
public static void mutateAtomically(Collection<Mutation> mutations,
ConsistencyLevel consistency_level,
public static void mutateAtomically(List<Mutation> mutations,
ConsistencyLevel consistencyLevel,
boolean requireQuorumForRemove,
Dispatcher.RequestTime requestTime)
throws UnavailableException, OverloadedException, WriteTimeoutException
{
Tracing.trace("Determining replicas for atomic batch");
long startTime = nanoTime();
List<WriteResponseHandlerWrapper> wrappers = new ArrayList<>(mutations.size());
boolean attributeNonAccordLatency = true;
long nonAccordEndTime = -1;
if (mutations.stream().anyMatch(mutation -> Keyspace.open(mutation.getKeyspaceName()).getReplicationStrategy().hasTransientReplicas()))
throw new AssertionError("Logged batches are unsupported with transient replication");
try
{
// If we are requiring quorum nodes for removal, we upgrade consistency level to QUORUM unless we already
// require ALL, or EACH_QUORUM. This is so that *at least* QUORUM nodes see the update.
ConsistencyLevel batchConsistencyLevel = requireQuorumForRemove
? ConsistencyLevel.QUORUM
: consistency_level;
switch (consistency_level)
{
case ALL:
case EACH_QUORUM:
batchConsistencyLevel = consistency_level;
}
ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forBatchlogWrite(batchConsistencyLevel == ConsistencyLevel.ANY);
ConsistencyLevel batchConsistencyLevel = consistencyLevelForBatchLog(consistencyLevel, requireQuorumForRemove);
// This can't be updated for each iteration because cleanup has to go to the correct replicas which is where the batchlog is originally written
ReplicaPlan.ForWrite batchlogReplicaPlan = ReplicaPlans.forBatchlogWrite(ClusterMetadata.current(), batchConsistencyLevel == ConsistencyLevel.ANY);
final TimeUUID batchUUID = nextTimeUUID();
BatchlogCleanup cleanup = new BatchlogCleanup(mutations.size(),
() -> asyncRemoveFromBatchlog(replicaPlan, batchUUID, requestTime));
// add a handler for each mutation - includes checking availability, but doesn't initiate any writes, yet
for (Mutation mutation : mutations)
boolean wroteToBatchLog = false;
while (true)
{
WriteResponseHandlerWrapper wrapper = wrapBatchResponseHandler(mutation,
consistency_level,
batchConsistencyLevel,
WriteType.BATCH,
cleanup,
requestTime);
// exit early if we can't fulfill the CL at this time.
wrappers.add(wrapper);
// In case we hit an error in before/during splitting
attributeNonAccordLatency = true;
ClusterMetadata cm = ClusterMetadata.current();
List<WriteResponseHandlerWrapper> wrappers = new ArrayList<>(mutations.size());
List<Mutation> accordMutations = new ArrayList<>(mutations.size());
BatchlogCleanup cleanup = new BatchlogCleanup(() -> asyncRemoveFromBatchlog(batchlogReplicaPlan, batchUUID, requestTime));
// add a handler for each mutation that will not be written on Accord - includes checking availability, but doesn't initiate any writes, yet
SplitConsumer<Mutation> splitConsumer = (accordMutation, normalMutation, originalMutations, mutationIndex) -> {
Mutation eitherMutation = normalMutation != null ? normalMutation : accordMutation;
Keyspace keyspace = Keyspace.open(eitherMutation.getKeyspaceName());
Token tk = eitherMutation.key().getToken();
if (accordMutation != null)
accordMutations.add(accordMutation);
if (normalMutation == null)
return;
// Always construct the replica plan to check availability
ReplicaPlan.ForWrite dataReplicaPlan = ReplicaPlans.forWrite(cm, keyspace, consistencyLevel, tk, ReplicaPlans.writeNormal);
if (dataReplicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null)
writeMetrics.localRequests.mark();
else
writeMetrics.remoteRequests.mark();
WriteResponseHandlerWrapper wrapper = wrapBatchResponseHandler(normalMutation,
dataReplicaPlan,
batchConsistencyLevel,
WriteType.BATCH,
cleanup,
requestTime);
wrappers.add(wrapper);
};
splitMutationsIntoAccordAndNormal(cm, mutations, splitConsumer);
attributeNonAccordLatency = !wrappers.isEmpty();
cleanup.setMutationsWaitingFor(wrappers.size() + (accordMutations.isEmpty() ? 0 : 1));
Tracing.trace("Split batch into Accord {} and normal {}", accordMutations, wrappers);
// If the entire batch can execute on Accord then we can skip the batch log entirely
// Write to the batch log first in case it fails so we don't end up with Accord applying
// part of the batch independently
if (!wrappers.isEmpty() && !wroteToBatchLog)
{
// write to the batchlog, including writes that will be routed to Accord to preserve the behavior
// of the batch log where if part of a batch is visible then eventually the entire batch is visible.
// If the Accord routed mutations depend on the Accord txn succeeding then it is no longer consistent
// with the mutations delivered by the batch log since an unacknowledged Accord txn won't be retried
// unless those mutations are also written to the batch log
// Only write to the log once and reuse the batchUUID for every attempt to route the mutations correctly
doFallibleWriteWithMetricTracking(() -> syncWriteToBatchlog(mutations, batchlogReplicaPlan, batchUUID, requestTime), consistencyLevel);
Tracing.trace("Successfully wrote to batchlog");
wroteToBatchLog = true;
}
// Start Accord executing so it executes while the mutations are synchronously applied
AsyncTxnResult accordResult = !accordMutations.isEmpty() ? mutateWithAccordAsync(cm, accordMutations, consistencyLevel, requestTime) : null;
Throwable failure = null;
try
{
// now actually perform the writes and wait for them to complete
if (!wrappers.isEmpty())
{
doFallibleWriteWithMetricTracking(() -> syncWriteBatchedMutations(wrappers, Stage.MUTATION, requestTime), consistencyLevel);
Tracing.trace("Successfully wrote normal mutations");
}
}
catch (RetryOnDifferentSystemException e)
{
writeMetrics.retryDifferentSystem.mark();
writeMetricsForLevel(consistencyLevel).retryDifferentSystem.mark();
logger.debug("Retrying batch txn on different system because some mutations were misrouted");
Tracing.trace("Got {} from normal mutations, will retry", e);
continue;
}
catch (CoordinatorBehindException e)
{
writeMetrics.retryCoordinatorBehind.mark();
writeMetricsForLevel(consistencyLevel).retryCoordinatorBehind.mark();
mutations.forEach(IMutation::clearCachedSerializationsForRetry);
logger.debug("Retrying batch now that coordinator has caught up to cluster metadata");
Tracing.trace("Got {} from normal mutations, will retry", e);
continue;
}
catch (Exception e)
{
failure = Throwables.merge(failure, e);
}
finally
{
// Try to exclude most of the Accord time
nonAccordEndTime = nanoTime();
}
// Check if the Accord mutations succeeded asynchronously
try
{
// It's notable here that the Accord portion of the batch will not be hinted
// while the regular mutations are hinted on failure and also going to be replayed later from
// the batch log. It wouldn't be difficult to add hinting here, but it does seem redundant with
// the batch log.
if (accordResult != null)
{
IAccordService accord = AccordService.instance();
TxnResult.Kind kind = accord.getTxnResult(accordResult, true, consistencyLevel, requestTime).kind();
if (kind == retry_new_protocol && failure == null)
continue;
Tracing.trace("Successfully wrote Accord mutations");
cleanup.ackMutation();
}
}
catch (Exception e)
{
failure = Throwables.merge(failure, e);
}
if (failure != null)
throw unchecked(failure);
break;
}
// write to the batchlog
syncWriteToBatchlog(mutations, replicaPlan, batchUUID, requestTime);
// now actually perform the writes and wait for them to complete
syncWriteBatchedMutations(wrappers, Stage.MUTATION, requestTime);
}
catch (UnavailableException e)
catch (Exception t)
{
writeMetrics.unavailables.mark();
writeMetricsForLevel(consistency_level).unavailables.mark();
Tracing.trace("Unavailable");
throw e;
}
catch (WriteTimeoutException e)
{
writeMetrics.timeouts.mark();
writeMetricsForLevel(consistency_level).timeouts.mark();
Tracing.trace("Write timeout; received {} of {} required replies", e.received, e.blockFor);
throw e;
}
catch (WriteFailureException e)
{
writeMetrics.failures.mark();
writeMetricsForLevel(consistency_level).failures.mark();
Tracing.trace("Write failure; received {} of {} required replies", e.received, e.blockFor);
throw e;
// Unexpected error so it would be helpful to have details
Tracing.trace("{}", getStackTraceAsToString(t));
throw t;
}
finally
{
long latency = nanoTime() - startTime;
writeMetrics.addNano(latency);
writeMetricsForLevel(consistency_level).addNano(latency);
updateCoordinatorWriteLatencyTableMetric(mutations, latency);
if (attributeNonAccordLatency)
{
// On the exception path nonAccordEndTime will be -1
long latency = nonAccordEndTime != -1 ? nonAccordEndTime : nanoTime() - startTime;
writeMetrics.addNano(latency);
writeMetricsForLevel(consistencyLevel).addNano(latency);
updateCoordinatorWriteLatencyTableMetric(mutations, latency);
}
}
}
@ -1501,7 +1623,7 @@ public class StorageProxy implements StorageProxyMBean
}
}
private static void syncWriteBatchedMutations(List<WriteResponseHandlerWrapper> wrappers, Stage stage, Dispatcher.RequestTime requestTime)
private static void syncWriteBatchedMutations(Iterable<WriteResponseHandlerWrapper> wrappers, Stage stage, Dispatcher.RequestTime requestTime)
throws WriteTimeoutException, OverloadedException
{
String localDataCenter = DatabaseDescriptor.getLocator().local().datacenter;
@ -1559,22 +1681,12 @@ public class StorageProxy implements StorageProxyMBean
// same as performWrites except does not initiate writes (but does perform availability checks).
private static WriteResponseHandlerWrapper wrapBatchResponseHandler(Mutation mutation,
ConsistencyLevel consistencyLevel,
ReplicaPlan.ForWrite replicaPlan,
ConsistencyLevel batchConsistencyLevel,
WriteType writeType,
BatchlogResponseHandler.BatchlogCleanup cleanup,
Dispatcher.RequestTime requestTime)
{
Keyspace keyspace = Keyspace.open(mutation.getKeyspaceName());
Token tk = mutation.key().getToken();
ReplicaPlan.ForWrite replicaPlan = ReplicaPlans.forWrite(keyspace, consistencyLevel, tk, ReplicaPlans.writeNormal);
if (replicaPlan.lookup(FBUtilities.getBroadcastAddressAndPort()) != null)
writeMetrics.localRequests.mark();
else
writeMetrics.remoteRequests.mark();
AbstractReplicationStrategy rs = replicaPlan.replicationStrategy();
AbstractWriteResponseHandler<IMutation> writeHandler = rs.getWriteResponseHandler(replicaPlan, null, writeType, mutation, requestTime);
BatchlogResponseHandler<IMutation> batchHandler = new BatchlogResponseHandler<>(writeHandler, batchConsistencyLevel.blockFor(rs), cleanup, requestTime);
@ -1603,13 +1715,17 @@ public class StorageProxy implements StorageProxyMBean
}
// used by atomic_batch_mutate to decouple availability check from the write itself, caches consistency level and endpoints.
private static class WriteResponseHandlerWrapper
public static class WriteResponseHandlerWrapper
{
final BatchlogResponseHandler<IMutation> handler;
final Mutation mutation;
@Nonnull
public final BatchlogResponseHandler<IMutation> handler;
@Nonnull
public final Mutation mutation;
WriteResponseHandlerWrapper(BatchlogResponseHandler<IMutation> handler, Mutation mutation)
public WriteResponseHandlerWrapper(@Nonnull BatchlogResponseHandler<IMutation> handler, @Nonnull Mutation mutation)
{
checkNotNull(handler);
checkNotNull(mutation);
this.handler = handler;
this.mutation = mutation;
}
@ -1844,7 +1960,7 @@ public class StorageProxy implements StorageProxyMBean
}
catch (Exception ex)
{
if (!(ex instanceof WriteTimeoutException))
if (!(ex instanceof WriteTimeoutException) && !(ex instanceof RetryOnDifferentSystemException))
logger.error("Failed to apply mutation locally : ", ex);
handler.onFailure(FBUtilities.getBroadcastAddressAndPort(), RequestFailure.forException(ex));
}
@ -2807,17 +2923,18 @@ public class StorageProxy implements StorageProxyMBean
long timeTakenNanos = now - startTimeNanos();
MessagingService.instance().metrics.recordSelfDroppedMessage(Verb.MUTATION_REQ, timeTakenNanos, NANOSECONDS);
if (requestTime.shouldSendHints())
// Don't submit a hint if this replica is transient
if (localReplica.isTransient())
return;
HintRunnable runnable = new HintRunnable(ImmutableSet.of(localReplica.endpoint()))
{
HintRunnable runnable = new HintRunnable(EndpointsForToken.of(localReplica.range().right, localReplica))
protected void runMayThrow() throws Exception
{
protected void runMayThrow() throws Exception
{
LocalMutationRunnable.this.runMayThrow();
}
};
submitHint(runnable);
}
LocalMutationRunnable.this.runMayThrow();
}
};
submitHint(runnable);
return;
}
@ -2872,9 +2989,9 @@ public class StorageProxy implements StorageProxyMBean
*/
private abstract static class HintRunnable implements Runnable
{
public final EndpointsForToken targets;
public final Set<InetAddressAndPort> targets;
protected HintRunnable(EndpointsForToken targets)
protected HintRunnable(Set<InetAddressAndPort> targets)
{
this.targets = targets;
}
@ -2892,7 +3009,7 @@ public class StorageProxy implements StorageProxyMBean
finally
{
StorageMetrics.totalHintsInProgress.dec(targets.size());
for (InetAddressAndPort target : targets.endpoints())
for (InetAddressAndPort target : targets)
getHintsInProgressFor(target).decrementAndGet();
}
}
@ -2938,25 +3055,43 @@ public class StorageProxy implements StorageProxyMBean
}
}
public static void submitHint(Mutation mutation, Replica target, AbstractWriteResponseHandler<IMutation> responseHandler)
public static void submitHintForRetryOnDifferentSystem(Mutation mutation)
{
submitHint(mutation, EndpointsForToken.of(target.range().right, target), responseHandler);
submitHint(mutation, ImmutableSet.of(HintsService.RETRY_ON_DIFFERENT_SYSTEM_ADDRESS), null);
}
private static void submitHint(Mutation mutation,
EndpointsForToken targets,
AbstractWriteResponseHandler<IMutation> responseHandler)
public static Future<Void> submitHint(Mutation mutation, Replica target, AbstractWriteResponseHandler<IMutation> responseHandler)
{
return submitHint(mutation, EndpointsForToken.of(target.range().right, target), responseHandler);
}
private static Future<Void> submitHint(Mutation mutation,
EndpointsForToken targets,
AbstractWriteResponseHandler<IMutation> responseHandler)
{
// hints should not be written for transient replicas because there is no point if they didn't contribute
// to quorum, they would eventually be removed anyways after running incremental repair.
// This logic assumes we don't always write to transient replicas to minimize incremental repair mismatches
// so we may want to walk this back when revisiting transient replication
Replicas.assertFull(targets);
return submitHint(mutation, targets.endpoints(), responseHandler);
}
private static Future<Void> submitHint(Mutation mutation,
Set<InetAddressAndPort> targets,
AbstractWriteResponseHandler<IMutation> responseHandler)
{
Replicas.assertFull(targets); // hints should not be written for transient replicas
HintRunnable runnable = new HintRunnable(targets)
{
public void runMayThrow()
{
Set<InetAddressAndPort> validTargets = new HashSet<>(targets.size());
Set<UUID> hostIds = new HashSet<>(targets.size());
for (InetAddressAndPort target : targets.endpoints())
for (InetAddressAndPort target : targets)
{
UUID hostId = StorageService.instance.getHostIdForEndpoint(target);
UUID hostId = target == HintsService.RETRY_ON_DIFFERENT_SYSTEM_ADDRESS ?
HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID :
StorageService.instance.getHostIdForEndpoint(target);
if (hostId != null)
{
hostIds.add(hostId);
@ -2981,14 +3116,14 @@ public class StorageProxy implements StorageProxyMBean
}
};
submitHint(runnable);
return submitHint(runnable);
}
private static Future<Void> submitHint(HintRunnable runnable)
{
StorageMetrics.totalHintsInProgress.inc(runnable.targets.size());
for (Replica target : runnable.targets)
getHintsInProgressFor(target.endpoint()).incrementAndGet();
for (InetAddressAndPort target : runnable.targets)
getHintsInProgressFor(target).incrementAndGet();
return (Future<Void>) Stage.MUTATION.submit(runnable);
}
@ -3181,9 +3316,9 @@ public class StorageProxy implements StorageProxyMBean
RowIterator casResult;
@Nonnull
PartitionIterator serialReadResult;
public final PartitionIterator serialReadResult;
boolean shouldRetryOnNewConsensusProtocol;
public final boolean shouldRetryOnNewConsensusProtocol;
private ConsensusAttemptResult(@Nullable RowIterator casResult, @Nullable PartitionIterator serialReadResult, boolean shouldRetryOnNewConsensusProtocol)
{

View File

@ -36,6 +36,7 @@ import com.google.common.primitives.Ints;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.coordinate.Timeout;
import accord.local.Command;
import accord.local.Node;
import accord.messages.AbstractEpochRequest;
@ -660,7 +661,26 @@ public class AccordJournal implements IJournal, Shutdownable
if (l.isEmpty())
waitForEpochs.pushLong(waitForEpoch);
l.add(context);
node.withEpoch(waitForEpoch, this::runOnce);
BiConsumer<Void, Throwable> withEpochCallback = new BiConsumer<>()
{
@Override
public void accept(Void unused, Throwable withEpochFailure)
{
if (withEpochFailure != null)
{
// Nothing to do but keep waiting
if (withEpochFailure instanceof Timeout)
{
node.withEpoch(waitForEpoch, this);
return;
}
else
throw new RuntimeException(withEpochFailure);
}
runOnce();
}
};
node.withEpoch(waitForEpoch, withEpochCallback);
}
// Next, process all delayed epochs

View File

@ -27,49 +27,37 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import accord.coordinate.Barrier;
import accord.coordinate.CoordinateSyncPoint;
import accord.coordinate.Exhausted;
import accord.coordinate.FailureAccumulator;
import accord.coordinate.TopologyMismatch;
import accord.impl.CoordinateDurabilityScheduling;
import accord.local.CommandStores;
import accord.primitives.SyncPoint;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.cql3.statements.RequestValidations;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.accord.exceptions.ReadExhaustedException;
import org.apache.cassandra.service.accord.interop.AccordInteropAdapter.AccordInteropFactory;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.service.accord.repair.RepairSyncPointAdapter;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.service.accord.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.BarrierType;
import accord.api.Result;
import accord.config.LocalConfig;
import accord.coordinate.Barrier;
import accord.coordinate.CoordinateSyncPoint;
import accord.coordinate.CoordinationFailed;
import accord.coordinate.Exhausted;
import accord.coordinate.FailureAccumulator;
import accord.coordinate.Preempted;
import accord.coordinate.Timeout;
import accord.coordinate.TopologyMismatch;
import accord.impl.AbstractConfigurationService;
import accord.impl.CoordinateDurabilityScheduling;
import accord.impl.SimpleProgressLog;
import accord.impl.SizeOfIntersectionSorter;
import accord.local.CommandStores;
import accord.local.DurableBefore;
import accord.local.Node;
import accord.local.Node.Id;
@ -78,7 +66,11 @@ import accord.local.RedundantBefore;
import accord.local.ShardDistributor.EvenSplit;
import accord.messages.LocalRequest;
import accord.messages.Request;
import accord.primitives.Keys;
import accord.primitives.Ranges;
import accord.primitives.Seekable;
import accord.primitives.Seekables;
import accord.primitives.SyncPoint;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.Txn.Kind;
@ -91,25 +83,46 @@ import accord.utils.async.AsyncChains;
import accord.utils.async.AsyncResult;
import org.agrona.collections.Int2ObjectHashMap;
import org.apache.cassandra.concurrent.Shutdownable;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.statements.RequestValidations;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.exceptions.RequestTimeoutException;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.metrics.AccordClientRequestMetrics;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessageDelivery;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordSyncPropagator.Notification;
import org.apache.cassandra.service.accord.api.AccordAgent;
import org.apache.cassandra.service.accord.api.AccordRoutingKey.KeyspaceSplitter;
import org.apache.cassandra.service.accord.api.AccordScheduler;
import org.apache.cassandra.service.accord.api.AccordTopologySorter;
import org.apache.cassandra.service.accord.api.CompositeTopologySorter;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.exceptions.ReadExhaustedException;
import org.apache.cassandra.service.accord.exceptions.ReadPreemptedException;
import org.apache.cassandra.service.accord.exceptions.WritePreemptedException;
import org.apache.cassandra.service.accord.interop.AccordInteropAdapter.AccordInteropFactory;
import org.apache.cassandra.service.accord.repair.RepairSyncPointAdapter;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.consensus.migration.TableMigrationState;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.membership.NodeId;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.Blocking;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.ExecutorUtils;
@ -120,7 +133,10 @@ import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static accord.messages.SimpleReply.Ok;
import static accord.primitives.Routable.Domain.Key;
import static accord.primitives.Routable.Domain.Range;
import static accord.utils.Invariants.checkState;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.cassandra.config.DatabaseDescriptor.getPartitioner;
@ -161,15 +177,21 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite)
public Seekables barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
{
throw new UnsupportedOperationException();
}
@Override
public Seekables barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite)
{
throw new UnsupportedOperationException("No accord barriers should be executed when accord.enabled = false in cassandra.yaml");
}
@Override
public long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
public Seekables repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints)
{
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("No accord repairs should be executed when accord.enabled = false in cassandra.yaml");
}
@Override
@ -179,9 +201,15 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints)
public @Nonnull AsyncTxnResult coordinateAsync(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
throw new UnsupportedOperationException("No accord repairs should be executed when accord.enabled = false in cassandra.yaml");
throw new UnsupportedOperationException("No accord transaction should be executed when accord.enabled = false in cassandra.yaml");
}
@Override
public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
throw new UnsupportedOperationException("No accord transaction should be executed when accord.enabled = false in cassandra.yaml");
}
@Override
@ -328,7 +356,7 @@ public class AccordService implements IAccordService, Shutdownable
this::handleLocalRequest,
configService,
AccordService::uniqueNow,
NodeTimeService.unixWrapper(TimeUnit.MICROSECONDS, AccordService::uniqueNow),
NodeTimeService.elapsedWrapperFromMonotonicSource(NANOSECONDS, Clock.Global::nanoTime),
() -> dataStore,
new KeyspaceSplitter(new EvenSplit<>(DatabaseDescriptor.getAccordShardCount(), getPartitioner().accordSplitter())),
agent,
@ -369,8 +397,17 @@ public class AccordService implements IAccordService, Shutdownable
return requestHandler;
}
private <S extends Seekables<?, ?>> long barrier(@Nonnull S keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, BiFunction<Node, S, AsyncResult<SyncPoint<S>>> syncPoint)
private <S extends Seekables<?, ?>> Seekables barrier(@Nonnull S keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, BiFunction<Node, S, AsyncResult<SyncPoint<S>>> syncPoint)
{
Stopwatch sw = Stopwatch.createStarted();
keysOrRanges = (S)intersectionWithAccordManagedRanges(keysOrRanges);
// It's possible none of them were Accord managed and we aren't going to treat that as an error
if (keysOrRanges.isEmpty())
{
logger.info("Skipping barrier because there are no ranges managed by Accord");
return keysOrRanges;
}
AccordClientRequestMetrics metrics = isForWrite ? accordWriteMetrics : accordReadMetrics;
TxnId txnId = null;
try
@ -382,14 +419,15 @@ public class AccordService implements IAccordService, Shutdownable
: Barrier.barrier(node, keysOrRanges, epoch, barrierType, syncPoint);
long deadlineNanos = requestTime.startedAtNanos() + timeoutNanos;
Timestamp barrierExecuteAt = AsyncChains.getBlocking(asyncResult, deadlineNanos - nanoTime(), NANOSECONDS);
logger.debug("Completed in {}ms barrier key: {} epoch: {} barrierType: {} isForWrite {}",
logger.debug("Completed barrier attempt in {}ms, {}ms since attempts start, barrier key: {} epoch: {} barrierType: {} isForWrite {}",
sw.elapsed(MILLISECONDS),
NANOSECONDS.toMillis(nanoTime() - requestTime.startedAtNanos()),
keysOrRanges, epoch, barrierType, isForWrite);
return barrierExecuteAt.epoch();
return keysOrRanges;
}
catch (ExecutionException e)
{
Throwable cause = e.getCause();
Throwable cause = Throwables.getRootCause(e);
if (cause instanceof Timeout)
{
metrics.timeouts.mark();
@ -431,7 +469,7 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long barrier(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite)
public Seekables barrier(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite)
{
return barrier(keysOrRanges, epoch, requestTime, timeoutNanos, barrierType, isForWrite, null);
}
@ -442,12 +480,59 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints)
public Seekables repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints)
{
Set<Node.Id> allNodes = allEndpoints.stream().map(configService::mappedId).collect(Collectors.toUnmodifiableSet());
return barrier(keysOrRanges, epoch, requestTime, timeoutNanos, barrierType, isForWrite, repairSyncPoint(allNodes));
}
private static <S extends Seekables<?,?>> Seekables intersectionWithAccordManagedRanges(Seekables<?, ?> keysOrRanges)
{
TableId tableId = null;
for (Seekable seekable : keysOrRanges)
{
TableId newTableId;
if (keysOrRanges.domain() == Key)
newTableId = ((PartitionKey)seekable).table();
else if (keysOrRanges.domain() == Range)
newTableId = ((TokenRange)seekable).table();
else
throw new IllegalStateException("Unexpected domain " + keysOrRanges.domain());
if (tableId == null)
tableId = newTableId;
else if (!tableId.equals(newTableId))
throw new IllegalArgumentException("Currently only one table is handled here.");
}
ClusterMetadata cm = ClusterMetadata.current();
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId);
TableMetadata tm = cfs.metadata();
// Barriers can be needed just because it's an Accord managed range, but it could also be a migration back to Paxos
// in which case we do want to barrier the migrating/migrated ranges even though the target for the migration is not Accord
// In either case Accord should be aware of those ranges and not generate a topology mismatch
if (tm.params.transactionalMode != TransactionalMode.off || tm.params.transactionalMigrationFrom.from != TransactionalMode.off)
{
TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tm.id);
// null is fine could be completely migrated or was always an Accord table on creation
if (tms == null)
return keysOrRanges;
Ranges migratingAndMigratedRanges = AccordTopology.toAccordRanges(tms.tableId, tms.migratingAndMigratedRanges);
return keysOrRanges.slice(migratingAndMigratedRanges);
}
switch (keysOrRanges.domain())
{
case Key:
return Keys.EMPTY;
case Range:
return Ranges.EMPTY;
default:
throw new IllegalStateException("Only keys and ranges are supported");
}
}
@VisibleForTesting
static ReadTimeoutException newBarrierTimeout(TxnId txnId, boolean global)
{
@ -474,21 +559,27 @@ public class AccordService implements IAccordService, Shutdownable
}
@VisibleForTesting
static long doWithRetries(Blocking blocking, LongSupplier action, int retryAttempts, long initialBackoffMillis, long maxBackoffMillis) throws InterruptedException
static Seekables doWithRetries(Blocking blocking, Supplier<Seekables> action, int retryAttempts, long initialBackoffMillis, long maxBackoffMillis) throws InterruptedException
{
// Since we could end up having the barrier transaction or the transaction it listens to invalidated
Throwable existingFailures = null;
Long success = null;
Seekables success = null;
long backoffMillis = initialBackoffMillis;
for (int attempt = 0; attempt < retryAttempts; attempt++)
{
try
{
success = action.getAsLong();
success = action.get();
break;
}
catch (TopologyMismatch topologyMismatch)
{
// Retry topology mismatch immediately because we should be able calculate the correct ranges immediately
backoffMillis = 0;
}
catch (RequestExecutionException | CoordinationFailed newFailures)
{
logger.error("Had failure on barrier", newFailures);
existingFailures = FailureAccumulator.append(existingFailures, newFailures, AccordService::isTimeout);
try
@ -514,6 +605,7 @@ public class AccordService implements IAccordService, Shutdownable
}
if (success == null)
{
logger.error("Ran out of retries for barrier");
checkState(existingFailures != null, "Didn't have success, but also didn't have failures");
Throwables.throwIfUnchecked(existingFailures);
throw new RuntimeException(existingFailures);
@ -522,7 +614,7 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
public Seekables barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException
{
return doWithRetries(Blocking.Default.instance, () -> AccordService.instance().barrier(keysOrRanges, minEpoch, Dispatcher.RequestTime.forImmediateExecution(), DatabaseDescriptor.getAccordRangeBarrierTimeoutNanos(), barrierType, isForWrite),
DatabaseDescriptor.getAccordBarrierRetryAttempts(),
@ -531,7 +623,7 @@ public class AccordService implements IAccordService, Shutdownable
}
@Override
public long repairWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints) throws InterruptedException
public Seekables repairWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints) throws InterruptedException
{
return doWithRetries(Blocking.Default.instance, () -> AccordService.instance().repair(keysOrRanges, minEpoch, Dispatcher.RequestTime.forImmediateExecution(), DatabaseDescriptor.getAccordRangeBarrierTimeoutNanos(), barrierType, isForWrite, allEndpoints),
DatabaseDescriptor.getAccordBarrierRetryAttempts(),
@ -558,38 +650,86 @@ public class AccordService implements IAccordService, Shutdownable
@Override
public @Nonnull TxnResult coordinate(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
AsyncTxnResult asyncTxnResult = coordinateAsync(txn, consistencyLevel, requestTime);
return getTxnResult(asyncTxnResult, txn.isWrite(), consistencyLevel, requestTime);
}
@Override
public @Nonnull AsyncTxnResult coordinateAsync(Txn txn, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
TxnId txnId = node.nextTxnId(txn.kind(), txn.keys().domain());
AccordClientRequestMetrics metrics = txn.isWrite() ? accordWriteMetrics : accordReadMetrics;
TxnId txnId = null;
try
{
metrics.keySize.update(txn.keys().size());
txnId = node.nextTxnId(txn.kind(), txn.keys().domain());
long deadlineNanos = requestTime.startedAtNanos() + DatabaseDescriptor.getTransactionTimeout(NANOSECONDS);
AsyncResult<Result> asyncResult = node.coordinate(txnId, txn);
Result result = AsyncChains.getBlocking(asyncResult, deadlineNanos - nanoTime(), NANOSECONDS);
return (TxnResult) result;
}
catch (ExecutionException e)
{
Throwable cause = e.getCause();
metrics.keySize.update(txn.keys().size());
AsyncResult<Result> asyncResult = node.coordinate(txnId, txn);
AsyncTxnResult asyncTxnResult = new AsyncTxnResult(txnId);
asyncResult.addCallback((success, failure) -> {
long durationNanos = nanoTime() - requestTime.startedAtNanos();
metrics.addNano(durationNanos);
Throwable cause = failure != null ? Throwables.getRootCause(failure) : null;
if (success != null)
{
if (((TxnResult)success).kind() == TxnResult.Kind.retry_new_protocol)
{
metrics.retryDifferentSystem.mark();
Tracing.trace("Got retry different system error from Accord, will retry");
}
asyncTxnResult.trySuccess((TxnResult)success);
return;
}
if (cause instanceof Timeout)
{
metrics.timeouts.mark();
throw newTimeout(txnId, txn, consistencyLevel);
// Don't mark the metric here, should be done in getTxnResult to ensure it only happens once
// since both Accord and the thread blocked on the result can trigger a timeout
asyncTxnResult.tryFailure(newTimeout(txnId, txn.isWrite(), consistencyLevel));
return;
}
if (cause instanceof Preempted)
{
metrics.preempted.mark();
//TODO need to improve
// Coordinator "could" query the accord state to see whats going on but that doesn't exist yet.
// Protocol also doesn't have a way to denote "unknown" outcome, so using a timeout as the closest match
throw newPreempted(txnId, txn, consistencyLevel);
asyncTxnResult.tryFailure(newPreempted(txnId, txn.isWrite(), consistencyLevel));
return;
}
if (cause instanceof TopologyMismatch)
{
throw RequestValidations.invalidRequest(cause.getMessage());
metrics.topologyMismatches.mark();
asyncTxnResult.tryFailure(RequestValidations.invalidRequest(cause.getMessage()));
return;
}
metrics.failures.mark();
throw new RuntimeException(cause);
asyncTxnResult.tryFailure(new RuntimeException(cause));
});
return asyncTxnResult;
}
@Override
public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
AccordClientRequestMetrics metrics = isWrite ? accordWriteMetrics : accordReadMetrics;
try
{
long deadlineNanos = requestTime.startedAtNanos() + DatabaseDescriptor.getTransactionTimeout(NANOSECONDS);
TxnResult result = asyncTxnResult.get(deadlineNanos - nanoTime(), NANOSECONDS);
return result;
}
catch (ExecutionException e)
{
// Metrics except timeout have already been handled
Throwable cause = e.getCause();
if (cause instanceof RequestTimeoutException)
{
// Mark here instead of in coordinate async since this is where the request timeout actually occurs
metrics.timeouts.mark();
cause.addSuppressed(e);
throw (RequestTimeoutException) cause;
}
else if (cause instanceof RuntimeException)
throw (RuntimeException)cause;
else
throw new RuntimeException(cause);
}
catch (InterruptedException e)
{
@ -599,11 +739,7 @@ public class AccordService implements IAccordService, Shutdownable
catch (TimeoutException e)
{
metrics.timeouts.mark();
throw newTimeout(txnId, txn, consistencyLevel);
}
finally
{
metrics.addNano(nanoTime() - requestTime.startedAtNanos());
throw newTimeout(asyncTxnResult.txnId, isWrite, consistencyLevel);
}
}
@ -614,15 +750,20 @@ public class AccordService implements IAccordService, Shutdownable
journal.processLocalRequest(request, callback);
}
private static RequestTimeoutException newTimeout(TxnId txnId, Txn txn, ConsistencyLevel consistencyLevel)
private static RequestTimeoutException newTimeout(TxnId txnId, boolean isWrite, ConsistencyLevel consistencyLevel)
{
throw txn.isWrite() ? new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, 0, txnId.toString())
// Client protocol doesn't handle null consistency level so use ANY
if (consistencyLevel == null)
consistencyLevel = ConsistencyLevel.ANY;
return isWrite ? new WriteTimeoutException(WriteType.CAS, consistencyLevel, 0, 0, txnId.toString())
: new ReadTimeoutException(consistencyLevel, 0, 0, false, txnId.toString());
}
private static RuntimeException newPreempted(TxnId txnId, Txn txn, ConsistencyLevel consistencyLevel)
private static RuntimeException newPreempted(TxnId txnId, boolean isWrite, ConsistencyLevel consistencyLevel)
{
throw txn.isWrite() ? new WritePreemptedException(WriteType.CAS, consistencyLevel, 0, 0, txnId.toString())
if (consistencyLevel == null)
consistencyLevel = ConsistencyLevel.ANY;
return isWrite ? new WritePreemptedException(WriteType.CAS, consistencyLevel, 0, 0, txnId.toString())
: new ReadPreemptedException(consistencyLevel, 0, 0, false, txnId.toString());
}

View File

@ -67,6 +67,10 @@ public class AccordVerbHandler<T extends Request> implements IVerbHandler<T>
if (node.topology().hasEpoch(waitForEpoch))
request.process(node, fromNodeId, message);
else
node.withEpoch(waitForEpoch, () -> request.process(node, fromNodeId, message));
node.withEpoch(waitForEpoch, (ignored, withEpochFailure) -> {
if (withEpochFailure != null)
throw new RuntimeException("Timed out waiting for epoch when processing message from " + fromNodeId + " to " + node + " message " + message, withEpochFailure);
request.process(node, fromNodeId, message);
});
}
}

View File

@ -18,6 +18,15 @@
package org.apache.cassandra.service.accord;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableSet;
import accord.api.BarrierType;
import accord.local.CommandStores;
import accord.local.DurableBefore;
@ -27,8 +36,8 @@ import accord.messages.Request;
import accord.primitives.Ranges;
import accord.primitives.Seekables;
import accord.primitives.Txn;
import accord.primitives.TxnId;
import accord.topology.TopologyManager;
import com.google.common.collect.ImmutableSet;
import org.agrona.collections.Int2ObjectHashMap;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
@ -37,19 +46,14 @@ import org.apache.cassandra.dht.Token;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey;
import org.apache.cassandra.service.accord.api.AccordScheduler;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.transport.Dispatcher;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Future;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
public interface IAccordService
{
@ -58,25 +62,21 @@ public interface IAccordService
IVerbHandler<? extends Request> verbHandler();
long barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException;
Seekables barrierWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite) throws InterruptedException;
long barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite);
Seekables barrier(@Nonnull Seekables keysOrRanges, long minEpoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite);
default long repairWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints) throws InterruptedException
default Seekables repairWithRetries(Seekables keysOrRanges, long minEpoch, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints) throws InterruptedException
{
throw new UnsupportedOperationException();
}
long repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints);
Seekables repair(@Nonnull Seekables keysOrRanges, long epoch, Dispatcher.RequestTime requestTime, long timeoutNanos, BarrierType barrierType, boolean isForWrite, List<InetAddressAndPort> allEndpoints);
default void postStreamReceivingBarrier(ColumnFamilyStore cfs, List<Range<Token>> ranges)
{
String ks = cfs.keyspace.getName();
Ranges accordRanges = Ranges.of(ranges
.stream()
.map(r -> new TokenRange(new TokenKey(cfs.getTableId(), r.left), new TokenKey(cfs.getTableId(), r.right)))
.collect(Collectors.toList())
.toArray(new accord.primitives.Range[0]));
Ranges accordRanges = AccordTopology.toAccordRanges(ks, ranges);
try
{
barrierWithRetries(accordRanges, Epoch.FIRST.getEpoch(), BarrierType.global_async, true);
@ -89,6 +89,21 @@ public interface IAccordService
@Nonnull TxnResult coordinate(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime);
class AsyncTxnResult extends AsyncPromise<TxnResult>
{
public final @Nonnull TxnId txnId;
public AsyncTxnResult(@Nonnull TxnId txnId)
{
checkNotNull(txnId);
this.txnId = txnId;
}
}
@Nonnull
AsyncTxnResult coordinateAsync(@Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime);
TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime);
long currentEpoch();
void setCacheSize(long kb);

View File

@ -132,10 +132,14 @@ public class AccordAgent implements Agent
return 32;
}
/**
* Create an empty transaction that Accord can use for its internal transactions. This is not suitable
* for tests since it skips validation done by regular transactions.
*/
@Override
public Txn emptyTxn(Kind kind, Seekables<?, ?> seekables)
public Txn emptySystemTxn(Kind kind, Seekables<?, ?> seekables)
{
return new Txn.InMemory(kind, seekables, TxnRead.EMPTY, TxnQuery.EMPTY, null);
return new Txn.InMemory(kind, seekables, TxnRead.EMPTY, TxnQuery.UNSAFE_EMPTY, null);
}
@Override

View File

@ -28,9 +28,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import accord.messages.ReadTxnData;
import accord.primitives.Ballot;
import org.apache.cassandra.schema.TableId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -43,6 +40,8 @@ import accord.local.Node;
import accord.local.Node.Id;
import accord.messages.Commit;
import accord.messages.Commit.Kind;
import accord.messages.ReadTxnData;
import accord.primitives.Ballot;
import accord.primitives.Deps;
import accord.primitives.FullRoute;
import accord.primitives.Participants;
@ -74,6 +73,7 @@ import org.apache.cassandra.metrics.AccordClientRequestMetrics;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.RequestCallback;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.accord.AccordEndpointMapper;
import org.apache.cassandra.service.accord.TokenRange;
@ -239,9 +239,9 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen
@Override
public void sendReadRepairMutation(Message<Mutation> message, InetAddressAndPort to, RequestCallback<Object> callback)
{
checkArgument(message.payload.allowsPotentialTransactionConflicts());
Node.Id id = endpointMapper.mappedId(to);
Mutation mutation = message.payload;
AccordInteropReadRepair readRepair = new AccordInteropReadRepair(id, executes, txnId, readScope, executeAt.epoch(), mutation);
AccordInteropReadRepair readRepair = new AccordInteropReadRepair(id, executes, txnId, readScope, executeAt.epoch(), message.payload);
node.send(id, readRepair, executor, new AccordInteropReadRepair.ReadRepairCallback(id, to, message, callback, this));
}
@ -260,8 +260,8 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen
// This should only rarely occur when coordinators start a transaction in a migrating range
// because they haven't yet updated their cluster metadata.
// It would be harmless to do the read, but we can respond faster skipping it
// and getting the transaction on the correct protocol
// It would be harmless to do the read, because it will be rejected in `TxnQuery` anyways,
// but it's faster to skip the read
TableMigrationState tms = ConsensusTableMigration.getTableMigrationState(command.metadata().id);
AccordClientRequestMetrics metrics = txn.kind().isWrite() ? accordWriteMetrics : accordReadMetrics;
if (ConsensusRequestRouter.instance.isKeyInMigratingOrMigratedRangeFromAccord(command.metadata(), tms, command.partitionKey()))
@ -376,13 +376,7 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen
return readCommand.allowOutOfRangeReads();
}
@Override
public Mutation maybeAllowOutOfRangeMutations(Mutation m)
{
return m.allowOutOfRangeMutations();
}
// Prrovide request callbacks with a way to send maximal commits on Insufficient responses
// Provide request callbacks with a way to send maximal commits on Insufficient responses
@Override
public void sendMaximalCommit(Id to)
{

View File

@ -20,12 +20,14 @@ package org.apache.cassandra.service.accord.interop;
import javax.annotation.Nonnull;
import accord.coordinate.Timeout;
import accord.local.Node;
import accord.messages.Callback;
import accord.messages.ReadData.ReadOk;
import accord.messages.ReadData.ReadReply;
import accord.utils.Invariants;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.RequestCallback;
@ -78,7 +80,14 @@ public abstract class AccordInteropReadCallback<T> implements Callback<ReadReply
public void onFailure(Node.Id from, Throwable failure)
{
wrapped.onFailure(endpoint, RequestFailure.UNKNOWN);
RequestFailure requestFailure;
// Convert from Accord's timeout exception to our failure reason because timeout is something
// that is useful for metrics and can be handled differently
if (failure instanceof Timeout)
requestFailure = new RequestFailure(RequestFailureReason.TIMEOUT, failure);
else
requestFailure = new RequestFailure(RequestFailureReason.UNKNOWN, failure);
wrapped.onFailure(endpoint, requestFailure);
}
public void onCallbackFailure(Node.Id from, Throwable failure)

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.service.accord.repair;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
@ -89,20 +90,21 @@ public class AccordRepair
return minEpoch;
}
public void repair() throws Throwable
public Ranges repair() throws Throwable
{
List<accord.primitives.Range> repairedRanges = new ArrayList<>();
for (accord.primitives.Range range : ranges)
repairRange((TokenRange)range);
repairedRanges.addAll(repairRange((TokenRange)range));
return Ranges.of(repairedRanges.toArray(new accord.primitives.Range[0]));
}
public Future<Void> repair(Executor executor)
public Future<Ranges> repair(Executor executor)
{
AsyncPromise<Void> future = new AsyncPromise<>();
AsyncPromise<Ranges> future = new AsyncPromise<>();
executor.execute(() -> {
try
{
repair();
future.trySuccess(null);
future.trySuccess(repair());
}
catch (Throwable e)
{
@ -117,8 +119,9 @@ public class AccordRepair
shouldAbort = reason == null ? new RuntimeException("Abort") : reason;
}
private void repairRange(TokenRange range) throws Throwable
private List<accord.primitives.Range> repairRange(TokenRange range) throws Throwable
{
List<accord.primitives.Range> repairedRanges = new ArrayList<>();
int rangeStepUpdateInterval = ACCORD_REPAIR_RANGE_STEP_UPDATE_INTERVAL.getInt();
RoutingKey remainingStart = range.start();
BigInteger rangeSize = splitter.sizeOf(range);
@ -154,7 +157,7 @@ public class AccordRepair
if (remainingStart.equals(range.end()))
{
logger.info("Completed barriers for {} in {} iterations", range, iteration - 1);
return;
return repairedRanges;
}
// Final repair is whatever remains
@ -169,14 +172,13 @@ public class AccordRepair
checkState(lastRepaired == null || toRepair.start().equals(lastRepaired.end()), "Next range should directly follow previous range");
lastRepaired = toRepair;
Ranges barrieredRanges;
if (requireAllEndpoints)
{
AccordService.instance().repairWithRetries(Seekables.of(toRepair), minEpoch.getEpoch(), BarrierType.global_sync, false, endpoints);
}
barrieredRanges = (Ranges)AccordService.instance().repairWithRetries(Seekables.of(toRepair), minEpoch.getEpoch(), BarrierType.global_sync, false, endpoints);
else
{
AccordService.instance().barrierWithRetries(Seekables.of(toRepair), minEpoch.getEpoch(), BarrierType.global_sync, false);
}
barrieredRanges = (Ranges)AccordService.instance().barrierWithRetries(Seekables.of(toRepair), minEpoch.getEpoch(), BarrierType.global_sync, false);
for (accord.primitives.Range barrieredRange : barrieredRanges)
repairedRanges.add(barrieredRange);
remainingStart = toRepair.end();
}

View File

@ -50,6 +50,11 @@ import static org.apache.cassandra.service.accord.txn.TxnRead.CAS_READ;
public abstract class TxnQuery implements Query
{
/**
* Used by transaction statements which will have Accord pass back to the C* coordinator code all the data that is
* read even if it is not returned as part of the result to the client. TxnDataName.returning() will fetch the data
* that is returned from TxnData.
*/
public static final TxnQuery ALL = new TxnQuery()
{
@Override
@ -65,6 +70,10 @@ public abstract class TxnQuery implements Query
}
};
/**
* For transactions that return no results but do still care that they don't apply if the tokens/ranges
* are not owned/managed by Accord
*/
public static final TxnQuery NONE = new TxnQuery()
{
@Override
@ -80,6 +89,9 @@ public abstract class TxnQuery implements Query
}
};
/**
* For supporting CQL CAS compatible transactions
*/
public static final TxnQuery CONDITION = new TxnQuery()
{
@Override
@ -114,7 +126,13 @@ public abstract class TxnQuery implements Query
}
};
public static final TxnQuery EMPTY = new TxnQuery()
/**
* UNSAFE_EMPTY doesn't validate that the range is owned by Accord so you want to be careful and use NONE
* if your transaction simply doesn't have results because that will validate that Accord owns the range
* for things like blind writes. Empty is used by Accord for things like sync points which may need to exeucte
* for ranges Accord used to manage, but no longer does.
*/
public static final TxnQuery UNSAFE_EMPTY = new TxnQuery()
{
@Override
@ -171,7 +189,7 @@ public abstract class TxnQuery implements Query
@Override
public void serialize(TxnQuery query, DataOutputPlus out, int version) throws IOException
{
Preconditions.checkArgument(query == null | query == ALL | query == NONE | query == CONDITION | query == EMPTY);
Preconditions.checkArgument(query == null | query == ALL | query == NONE | query == CONDITION | query == UNSAFE_EMPTY);
out.writeByte(query == null ? 0 : query.type());
}
@ -185,14 +203,14 @@ public abstract class TxnQuery implements Query
case 1: return ALL;
case 2: return NONE;
case 3: return CONDITION;
case 4: return EMPTY;
case 4: return UNSAFE_EMPTY;
}
}
@Override
public long serializedSize(TxnQuery query, int version)
{
Preconditions.checkArgument(query == null | query == ALL | query == NONE | query == CONDITION | query == EMPTY);
Preconditions.checkArgument(query == null | query == ALL | query == NONE | query == CONDITION | query == UNSAFE_EMPTY);
return TypeSizes.sizeof((byte)2);
}
};

View File

@ -69,7 +69,8 @@ import static org.apache.cassandra.utils.NullableSerializer.serializedNullableSi
public class TxnUpdate extends AccordUpdate
{
private static final long EMPTY_SIZE = ObjectSizes.measure(new TxnUpdate(null, new ByteBuffer[0], null, null));
private static final long EMPTY_SIZE = ObjectSizes.measure(new TxnUpdate(null, new ByteBuffer[0], null, null, false));
private static final int FLAG_PRESERVE_TIMESTAMPS = 0x1;
private final Keys keys;
private final ByteBuffer[] fragments;
@ -78,10 +79,15 @@ public class TxnUpdate extends AccordUpdate
@Nullable
private final ConsistencyLevel cassandraCommitCL;
// Hints and batchlog want to write with the lower timestamp they generated when applying their writes via Accord
// so they don't resurrect data if they are applied at a later time. Accord should be fine with this because
// the writes are still deterministic from the perspective of coordinators/recovery coordinators.
private final boolean preserveTimestamps;
// Memoize computation of condition
private Boolean conditionResult;
public TxnUpdate(List<TxnWrite.Fragment> fragments, TxnCondition condition, @Nullable ConsistencyLevel cassandraCommitCL)
public TxnUpdate(List<TxnWrite.Fragment> fragments, TxnCondition condition, @Nullable ConsistencyLevel cassandraCommitCL, boolean preserveTimestamps)
{
checkArgument(cassandraCommitCL == null || IAccordService.SUPPORTED_COMMIT_CONSISTENCY_LEVELS.contains(cassandraCommitCL));
// TODO: Figure out a way to shove keys into TxnCondition, and have it implement slice/merge.
@ -90,14 +96,16 @@ public class TxnUpdate extends AccordUpdate
this.fragments = toSerializedValuesArray(keys, fragments, fragment -> fragment.key, TxnWrite.Fragment.serializer);
this.condition = serialize(condition, TxnCondition.serializer);
this.cassandraCommitCL = cassandraCommitCL;
this.preserveTimestamps = preserveTimestamps;
}
private TxnUpdate(Keys keys, ByteBuffer[] fragments, ByteBuffer condition, ConsistencyLevel cassandraCommitCL)
private TxnUpdate(Keys keys, ByteBuffer[] fragments, ByteBuffer condition, ConsistencyLevel cassandraCommitCL, boolean preserveTimestamps)
{
this.keys = keys;
this.fragments = fragments;
this.condition = condition;
this.cassandraCommitCL = cassandraCommitCL;
this.preserveTimestamps = preserveTimestamps;
}
@Override
@ -141,12 +149,19 @@ public class TxnUpdate extends AccordUpdate
return keys;
}
// Batch log and hints want to keep their lower timestamp for the applied writes to avoid resurrecting old data
// when they are applied later, possibly after further updates have already been acknowledged.
public boolean preserveTimestamps()
{
return preserveTimestamps;
}
@Override
public Update slice(Ranges ranges)
{
Keys keys = this.keys.slice(ranges);
// TODO: Slice the condition.
return new TxnUpdate(keys, select(this.keys, keys, fragments), condition, cassandraCommitCL);
return new TxnUpdate(keys, select(this.keys, keys, fragments), condition, cassandraCommitCL, preserveTimestamps);
}
@Override
@ -154,7 +169,7 @@ public class TxnUpdate extends AccordUpdate
{
Keys keys = this.keys.intersecting(participants);
// TODO: Slice the condition.
return new TxnUpdate(keys, select(this.keys, keys, fragments), condition, cassandraCommitCL);
return new TxnUpdate(keys, select(this.keys, keys, fragments), condition, cassandraCommitCL, preserveTimestamps);
}
private static ByteBuffer[] select(Keys in, Keys out, ByteBuffer[] from)
@ -176,7 +191,7 @@ public class TxnUpdate extends AccordUpdate
TxnUpdate that = (TxnUpdate) update;
Keys mergedKeys = this.keys.with(that.keys);
ByteBuffer[] mergedFragments = merge(this.keys, that.keys, this.fragments, that.fragments, mergedKeys.size());
return new TxnUpdate(mergedKeys, mergedFragments, condition, cassandraCommitCL);
return new TxnUpdate(mergedKeys, mergedFragments, condition, cassandraCommitCL, preserveTimestamps);
}
private static ByteBuffer[] merge(Keys leftKeys, Keys rightKeys, ByteBuffer[] left, ByteBuffer[] right, int outputSize)
@ -207,7 +222,6 @@ public class TxnUpdate extends AccordUpdate
QueryOptions options = QueryOptions.forProtocolVersion(ProtocolVersion.CURRENT);
AccordUpdateParameters parameters = new AccordUpdateParameters((TxnData) data, options);
// First completes all fragments and join them with the repairs pending for those partitions
for (TxnWrite.Fragment fragment : fragments)
// Filter out fragments that already constitute complete updates to avoid persisting them via TxnWrite:
if (!fragment.isComplete())
@ -233,6 +247,7 @@ public class TxnUpdate extends AccordUpdate
@Override
public void serialize(TxnUpdate update, DataOutputPlus out, int version) throws IOException
{
out.writeByte(update.preserveTimestamps ? FLAG_PRESERVE_TIMESTAMPS : 0);
KeySerializers.keys.serialize(update.keys, out, version);
writeWithVIntLength(update.condition, out);
serializeArray(update.fragments, out, version, ByteBufferUtil.byteBufferSerializer);
@ -242,17 +257,20 @@ public class TxnUpdate extends AccordUpdate
@Override
public TxnUpdate deserialize(DataInputPlus in, int version) throws IOException
{
int flags = in.readByte();
boolean preserveTimestamps = (FLAG_PRESERVE_TIMESTAMPS & flags) == 1;
Keys keys = KeySerializers.keys.deserialize(in, version);
ByteBuffer condition = readWithVIntLength(in);
ByteBuffer[] fragments = deserializeArray(in, version, ByteBufferUtil.byteBufferSerializer, ByteBuffer[]::new);
ConsistencyLevel consistencyLevel = deserializeNullable(in, version, consistencyLevelSerializer);
return new TxnUpdate(keys, fragments, condition, consistencyLevel);
return new TxnUpdate(keys, fragments, condition, consistencyLevel, preserveTimestamps);
}
@Override
public long serializedSize(TxnUpdate update, int version)
{
long size = KeySerializers.keys.serializedSize(update.keys, version);
long size = 1; // flags
size += KeySerializers.keys.serializedSize(update.keys, version);
size += serializedSizeWithVIntLength(update.condition);
size += serializedArraySize(update.fragments, version, ByteBufferUtil.byteBufferSerializer);
size += serializedNullableSize(update.cassandraCommitCL, version, consistencyLevelSerializer);

View File

@ -38,8 +38,8 @@ import accord.api.DataStore;
import accord.api.Key;
import accord.api.Write;
import accord.impl.AbstractSafeCommandStore;
import accord.impl.TimestampsForKeys;
import accord.impl.TimestampsForKey;
import accord.impl.TimestampsForKeys;
import accord.local.SafeCommandStore;
import accord.primitives.PartialTxn;
import accord.primitives.RoutableKey;
@ -138,10 +138,12 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
'}';
}
public AsyncChain<Void> write(@Nonnull Function<Cell, CellPath> cellToMaybeNewListPath, long timestamp, int nowInSeconds)
public AsyncChain<Void> write(boolean preserveTimestamps, @Nonnull Function<Cell, CellPath> cellToMaybeNewListPath, long timestamp, int nowInSeconds)
{
PartitionUpdate update = new PartitionUpdate.Builder(get(), 0).updateTimesAndPathsForAccord(cellToMaybeNewListPath, timestamp, nowInSeconds).build();
Mutation mutation = new Mutation(update);
PartitionUpdate update = get();
if (!preserveTimestamps)
update = new PartitionUpdate.Builder(get(), 0).updateTimesAndPathsForAccord(cellToMaybeNewListPath, timestamp, nowInSeconds).build();
Mutation mutation = new Mutation(update, true);
return AsyncChains.ofRunnable(Stage.MUTATION.executor(), mutation::apply);
}
@ -383,10 +385,11 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
List<AsyncChain<Void>> results = new ArrayList<>();
boolean preserveTimestamps = ((TxnUpdate)txn.update()).preserveTimestamps();
// Apply updates not specified fully by the client but built from fragments completed by data from reads.
// This occurs, for example, when an UPDATE statement uses a value assigned by a LET statement.
Function<Cell, CellPath> accordListPathSuppler = accordListPathSupplier(timestamp);
forEachWithKey((PartitionKey) key, write -> results.add(write.write(accordListPathSuppler, timestamp, nowInSeconds)));
forEachWithKey((PartitionKey) key, write -> results.add(write.write(preserveTimestamps, accordListPathSuppler, timestamp, nowInSeconds)));
if (isConditionMet)
{
@ -396,7 +399,7 @@ public class TxnWrite extends AbstractKeySorted<TxnWrite.Update> implements Writ
TxnUpdate txnUpdate = (TxnUpdate) txn.update();
assert txnUpdate != null : "PartialTxn should contain an update if we're applying a write!";
List<Update> updates = txnUpdate.completeUpdatesForKey((RoutableKey) key);
updates.forEach(update -> results.add(update.write(accordListPathSuppler, timestamp, nowInSeconds)));
updates.forEach(update -> results.add(update.write(preserveTimestamps, accordListPathSuppler, timestamp, nowInSeconds)));
}
if (results.isEmpty())

View File

@ -47,7 +47,7 @@ import org.apache.cassandra.service.reads.repair.BlockingReadRepair;
/**
* This update is used to support blocking read repair from non-transactional Cassandra reads. Cassandra creates
* a read repair mutation per node and this enables some partitiosn to be readable that would otherwise run into messages
* a read repair mutation per node and this enables some partitions to be readable that would otherwise run into messages
* size limits.
*
* This update is used during the `Execute` phase to apply the repair mutations directly in AccordInteropExecution similar
@ -58,7 +58,8 @@ import org.apache.cassandra.service.reads.repair.BlockingReadRepair;
* The state for this update is always kept in memory and is never serialized. Only the Id is propagated so the cache
* can evict the update and then load it back. We don't need to persist it or have it be recoverable because if the original
* coordinator fails to complete the transaction then the dependent Cassandra read that triggered the read repair will
* also fail and it doesn't matter if the read repair is partially applied or not applied at all.
* also fail and it doesn't matter if the read repair is partially applied or not applied at all since it doesn't propose
* new values.
*/
public class UnrecoverableRepairUpdate<E extends Endpoints<E>, P extends ReplicaPlan.ForRead<E, P>> extends AccordUpdate
{
@ -117,6 +118,7 @@ public class UnrecoverableRepairUpdate<E extends Endpoints<E>, P extends Replica
this.keys = keys;
this.dk = dk;
this.mutations = mutations;
mutations.values().forEach(Mutation::allowPotentialTransactionConflicts);
this.writePlan = writePlan;
this.updateKey = new Key(nodeId.id, nextCounter.getAndIncrement());
}

View File

@ -113,6 +113,10 @@ public enum TransactionalMode
return consistencyLevel;
}
// TODO (required): This won't work for migration directly from none to full because there is no safe system to read from
// during the first phase (repair). Accord won't read correctly beacuse it won't honor the CL and miss non-transactional writes that haven't been repaired and non-transactional
// reads will miss all the writes being routed through Accord since they occur asynchronously. Something has to give here where either writes routed through are Accord are synchronous at CL
// or reads are routed through Accord and read at quorum as long as the range has not completed the first phase (repair).
public ConsistencyLevel readCLForStrategy(ConsistencyLevel consistencyLevel)
{
if (ignoresSuppliedConsistencyLevel)

View File

@ -46,6 +46,7 @@ import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.db.WriteType;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputPlus;
@ -173,7 +174,7 @@ public abstract class ConsensusKeyMigrationState
// TODO (desired): Better query start time
TableMigrationState tms = tableMigrationState;
repairKeyAccord(key, tms.keyspaceName, tms.tableId, tms.minMigrationEpoch(key.getToken()).getEpoch(), Dispatcher.RequestTime.forImmediateExecution(), false, isForWrite);
repairKeyAccord(key, tms.tableId, tms.minMigrationEpoch(key.getToken()).getEpoch(), Dispatcher.RequestTime.forImmediateExecution(), false, isForWrite);
}
private boolean paxosReadSatisfiedByKeyMigration()
@ -264,7 +265,6 @@ public abstract class ConsensusKeyMigrationState
* Trigger a distributed repair of Accord state for this key.
*/
static void repairKeyAccord(DecoratedKey key,
String keyspace,
TableId tableId,
long minEpoch,
Dispatcher.RequestTime requestTime,
@ -283,7 +283,9 @@ public abstract class ConsensusKeyMigrationState
// will soon be ready to execute, but only waits for the local replica to be ready
// Local will only create a transaction if it can't find an existing one to wait on
BarrierType barrierType = global ? BarrierType.global_async : BarrierType.local;
AccordService.instance().barrier(Seekables.of(new PartitionKey(tableId, key)), minEpoch, requestTime, DatabaseDescriptor.getTransactionTimeout(TimeUnit.NANOSECONDS), barrierType, isForWrite);
Seekables keysOrRanges = AccordService.instance().barrier(Seekables.of(new PartitionKey(tableId, key)), minEpoch, requestTime, DatabaseDescriptor.getTransactionTimeout(TimeUnit.NANOSECONDS), barrierType, isForWrite);
if (keysOrRanges.isEmpty())
throw new RetryOnDifferentSystemException();
// We don't save the state to the cache here. Accord will notify the agent every time a barrier happens.
}
finally

View File

@ -0,0 +1,359 @@
/*
* 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.consensus.migration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.coordinate.CoordinationFailed;
import accord.primitives.Keys;
import accord.primitives.Txn;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.exceptions.RetryOnDifferentSystemException;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.IAccordService;
import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult;
import org.apache.cassandra.service.accord.api.PartitionKey;
import org.apache.cassandra.service.accord.txn.TxnCondition;
import org.apache.cassandra.service.accord.txn.TxnQuery;
import org.apache.cassandra.service.accord.txn.TxnRead;
import org.apache.cassandra.service.accord.txn.TxnReferenceOperations;
import org.apache.cassandra.service.accord.txn.TxnUpdate;
import org.apache.cassandra.service.accord.txn.TxnWrite;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.transport.Dispatcher;
import static com.google.common.base.Preconditions.checkState;
import static java.util.function.Predicate.not;
import static org.apache.cassandra.dht.Range.isInNormalizedRanges;
import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.getTableMetadata;
/**
* Applying mutations can fail with RetryOnDifferentSystemException if a
* mutation conflicts with a table and range that needs to be managed
* transactionally. This impacts mutations, logged/unlogged batches, hints,and blocking read repair.
*
* This class contains the logic needed for managing these retry loops and splitting the mutations up
*/
public class ConsensusMigrationMutationHelper
{
private static final Logger logger = LoggerFactory.getLogger(ConsensusMigrationMutationHelper.class);
private static ConsensusMigrationMutationHelper instance = new ConsensusMigrationMutationHelper();
public static ConsensusMigrationMutationHelper instance()
{
return instance;
}
@VisibleForTesting
public static void replaceInstanceForTest(ConsensusMigrationMutationHelper testInstance)
{
instance = testInstance;
}
@VisibleForTesting
public static void resetInstanceForTest()
{
instance = new ConsensusMigrationMutationHelper();
}
public ConsensusMigrationMutationHelper() {}
private static ConsistencyLevel consistencyLevelForCommit(ClusterMetadata cm, Collection<? extends IMutation> mutations, @Nullable ConsistencyLevel consistencyLevel)
{
// Null means no specific consistency behavior is required from Accord, it's functionally similar to ANY
// if you aren't reading the result back via Accord
if (consistencyLevel == null)
return null;
for (IMutation mutation : mutations)
{
for (TableId tableId : mutation.getTableIds())
{
TransactionalMode mode = getTableMetadata(cm, tableId).params.transactionalMode;
// commitCLForStrategy should return either null or the supplied consistency level
// in which case we will commit everything at that CL since Accord doesn't support per table
// commit consistency
ConsistencyLevel commitCL = mode.commitCLForStrategy(consistencyLevel);
if (commitCL != null)
return commitCL;
}
}
return null;
}
/**
* Result of splitting mutations across Accord and non-transactional boundaries
*/
public static class SplitMutations<T extends IMutation> implements SplitConsumer<T>
{
@Nullable
private List<T> accordMutations;
@Nullable
private List<T> normalMutations;
private SplitMutations() {}
public List<T> accordMutations()
{
return accordMutations;
}
public List<T> normalMutations()
{
return normalMutations;
}
@Override
public void consume(@Nullable T accordMutation, @Nullable T normalMutation, List<T> mutations, int mutationIndex)
{
// Avoid allocating an ArrayList in common single mutation single system case
if (mutations.size() == 1 && (accordMutation != null ^ normalMutation != null))
{
if (accordMutation != null)
accordMutations = mutations;
else
normalMutations = mutations;
return;
}
if (accordMutation != null)
{
if (accordMutations == null)
accordMutations = new ArrayList<>(Math.min(mutations.size(), 10));
accordMutations.add(accordMutation);
}
if (normalMutation != null)
{
if (normalMutations == null)
normalMutations = new ArrayList<>(Math.min(mutations.size(), 10));
normalMutations.add(normalMutation);
}
}
}
public interface SplitConsumer<T extends IMutation>
{
void consume(@Nullable T accordMutation, @Nullable T normalMutation, List<T> mutations, int mutationIndex);
}
public static <T extends IMutation, N> SplitMutations<T> splitMutationsIntoAccordAndNormal(ClusterMetadata cm, List<T> mutations)
{
SplitMutations<T> splitMutations = new SplitMutations<>();
splitMutationsIntoAccordAndNormal(cm, mutations, splitMutations);
return splitMutations;
}
public static <T extends IMutation> void splitMutationsIntoAccordAndNormal(ClusterMetadata cm, List<T> mutations, SplitConsumer<T> splitConsumer)
{
for (int i=0,mi=mutations.size(); i<mi; i++)
{
SplitMutation<T> splitMutation = instance.splitMutationIntoAccordAndNormal(mutations.get(i), cm);
splitConsumer.consume(splitMutation.accordMutation, splitMutation.normalMutation, mutations, i);
}
}
/**
* Result of splitting a mutation across Accord and non-transactional boundaries
*/
public static class SplitMutation<T extends IMutation>
{
@Nullable
public final T accordMutation;
@Nullable
public final T normalMutation;
public SplitMutation(@Nullable T accordMutation, @Nullable T normalMutation)
{
this.accordMutation = accordMutation;
this.normalMutation = normalMutation;
}
}
public <T extends IMutation> SplitMutation<T> splitMutationIntoAccordAndNormal(T mutation, ClusterMetadata cm)
{
if (mutation.allowsPotentialTransactionConflicts())
return new SplitMutation<>(null, mutation);
Token token = mutation.key().getToken();
Predicate<TableId> isAccordUpdate = tableId -> tokenShouldBeWrittenThroughAccord(cm, tableId, token);
T accordMutation = (T)mutation.filter(isAccordUpdate);
T normalMutation = (T)mutation.filter(not(isAccordUpdate));
for (PartitionUpdate pu : mutation.getPartitionUpdates())
checkState((accordMutation == null ? false : accordMutation.hasUpdateForTable(pu.metadata().id))
|| (normalMutation == null ? false : normalMutation.hasUpdateForTable(pu.metadata().id)),
"All partition updates should still be present after splitting");
return new SplitMutation(accordMutation, normalMutation);
}
public AsyncTxnResult mutateWithAccordAsync(ClusterMetadata cm, Mutation mutation, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
return mutateWithAccordAsync(cm, ImmutableList.of(mutation), consistencyLevel, requestTime);
}
public static AsyncTxnResult mutateWithAccordAsync(ClusterMetadata cm, Collection<? extends IMutation> mutations, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime)
{
int fragmentIndex = 0;
List<TxnWrite.Fragment> fragments = new ArrayList<>(mutations.size());
List<PartitionKey> partitionKeys = new ArrayList<>(mutations.size());
for (IMutation mutation : mutations)
{
for (PartitionUpdate update : mutation.getPartitionUpdates())
{
PartitionKey pk = PartitionKey.of(update);
partitionKeys.add(pk);
fragments.add(new TxnWrite.Fragment(PartitionKey.of(update), fragmentIndex++, update, TxnReferenceOperations.empty()));
}
}
// Potentially ignore commit consistency level if the TransactionalMode specifies full
ConsistencyLevel clForCommit = consistencyLevelForCommit(cm, mutations, consistencyLevel);
TxnUpdate update = new TxnUpdate(fragments, TxnCondition.none(), clForCommit, true);
Txn.InMemory txn = new Txn.InMemory(Keys.of(partitionKeys), TxnRead.EMPTY, TxnQuery.NONE, update);
IAccordService accordService = AccordService.instance();
try
{
return accordService.coordinateAsync(txn, consistencyLevel, requestTime);
}
catch (CoordinationFailed coordinationFailed)
{
AsyncTxnResult failure = new AsyncTxnResult(coordinationFailed.txnId());
failure.setFailure(coordinationFailed.wrap());
return failure;
}
}
public static void validateSafeToExecuteNonTransactionally(IMutation mutation) throws RetryOnDifferentSystemException
{
if (mutation.allowsPotentialTransactionConflicts())
return;
// System keyspaces are never managed by Accord
if (SchemaConstants.isSystemKeyspace(mutation.getKeyspaceName()))
return;
// Local keyspaces are never managed by Accord
if (Schema.instance.localKeyspaces().containsKeyspace(mutation.getKeyspaceName()))
return;
ClusterMetadata cm = ClusterMetadata.current();
DecoratedKey dk = mutation.key();
// Check all the partition updates and if any can't be done return an error response
// and the coordinator can retry with things correctly routed
boolean throwRetryOnDifferentSystem = false;
// Track CFS so we only mark each one once
Set<TableId> markedColumnFamilies = null;
for (PartitionUpdate pu : mutation.getPartitionUpdates())
{
TableId tableId = pu.metadata().id;
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId);
if (tokenShouldBeWrittenThroughAccord(cm, tableId, dk.getToken()))
{
throwRetryOnDifferentSystem = true;
if (markedColumnFamilies == null)
markedColumnFamilies = new HashSet<>();
if (markedColumnFamilies.add(tableId))
cfs.metric.mutationsRejectedOnWrongSystem.mark();
logger.debug("Rejecting mutation on wrong system to table {}.{}", cfs.keyspace.getName(), cfs.name);
Tracing.trace("Rejecting mutation on wrong system to table {}.{} token {}", cfs.keyspace.getName(), cfs.name, dk.getToken());
}
}
if (throwRetryOnDifferentSystem)
throw new RetryOnDifferentSystemException();
}
public static boolean tokenShouldBeWrittenThroughAccord(@Nonnull ClusterMetadata cm, @Nonnull TableId tableId, @Nonnull Token token)
{
TableMetadata tm = getTableMetadata(cm, tableId);
if (tm == null)
return false;
boolean transactionalModeWritesThroughAccord = tm.params.transactionalMode.writesThroughAccord;
TransactionalMigrationFromMode transactionalMigrationFromMode = tm.params.transactionalMigrationFrom;
boolean migrationFromWritesThroughAccord = transactionalMigrationFromMode.writesThroughAccord();
if (transactionalModeWritesThroughAccord && migrationFromWritesThroughAccord)
return true;
// Could be migrating or could be completely migrated, if it's migrating check if the key for this mutation
if (transactionalModeWritesThroughAccord || migrationFromWritesThroughAccord)
{
TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tm.id);
if (tms == null)
{
if (transactionalMigrationFromMode == TransactionalMigrationFromMode.none)
// There is no migration and no TMS so do what the schema says since no migration should be required
return transactionalModeWritesThroughAccord;
else
// If we are migrating from something and there is no migration state the migration hasn't begun
// so continue to do what we are migrating from does until the range is marked as migrating
return migrationFromWritesThroughAccord;
}
// This logic is driven by the fact that Paxos is not picky about how data is written since it's txn recovery
// is deterministic in the face of non-deterministic reads because consensus is agreeing on the writes that will be done to the database
// Accord agrees on what computation will produce those writes and then asynchronously executes those computations, potentially multiple times
// with different results if Accord reads non-transactionally written data that could be seen differently by different coordinators
// If the current mode writes through Accord then we should always write though Accord for ranges managed by Accord.
// Accord needs to do synchronous commit and respect the consistency level so that Accord will later be able to
// read its own writes
if (transactionalModeWritesThroughAccord)
return isInNormalizedRanges(token, tms.migratingAndMigratedRanges);
// If we are migrating from a mode that used to write to Accord then any range that isn't migrating/migrated
// should continue to write through Accord.
// It's not completely symmetrical because Paxos is able to read Accord's writes by performing a single key barrier
// and regular mutations will be able to do the same thing (needs to be added along with non-transactional reads)
// This means that migrating ranges don't need to be written through Accord because we are running Paxos now
// and not Accord. When migrating to Accord we need to do all the writes through Accord even if we aren't
// reading through Accord so that repair + Accord metadata is sufficient for Accord to be able to read
// safely and deterministically from any coordinator
if (migrationFromWritesThroughAccord)
return !isInNormalizedRanges(token, tms.migratingAndMigratedRanges);
}
return false;
}
}

View File

@ -18,40 +18,46 @@
package org.apache.cassandra.service.consensus.migration;
import javax.annotation.Nullable;
import accord.primitives.Ranges;
import org.apache.cassandra.tcm.Epoch;
import static com.google.common.base.Preconditions.checkArgument;
public class ConsensusMigrationRepairResult
{
private static final ConsensusMigrationRepairResult INELIGIBLE = new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.ineligible, Epoch.EMPTY);
private static final ConsensusMigrationRepairResult INELIGIBLE = new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.ineligible, Epoch.EMPTY, null);
public final ConsensusMigrationRepairType type;
public final Epoch minEpoch;
@Nullable
public final Ranges barrieredRanges;
private ConsensusMigrationRepairResult(ConsensusMigrationRepairType type, Epoch minEpoch)
private ConsensusMigrationRepairResult(ConsensusMigrationRepairType type, Epoch minEpoch, @Nullable Ranges barrieredRanges)
{
this.type = type;
this.minEpoch = minEpoch;
this.barrieredRanges = barrieredRanges;
}
public static ConsensusMigrationRepairResult fromRepair(Epoch minEpoch, boolean paxosAndDataRepaired, boolean accordRepaired, boolean deadNodesExcluded)
public static ConsensusMigrationRepairResult fromRepair(Epoch minEpoch, Ranges barrieredRanges, boolean paxosAndDataRepaired, boolean accordRepaired, boolean deadNodesExcluded)
{
checkArgument((!paxosAndDataRepaired && !accordRepaired) || minEpoch.isAfter(Epoch.EMPTY), "Epoch should not be empty if Paxos and regular repairs were performed");
if (deadNodesExcluded) return INELIGIBLE;
if (paxosAndDataRepaired && accordRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.either, minEpoch);
if (paxosAndDataRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.paxos, minEpoch);
if (accordRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.accord, minEpoch);
if (paxosAndDataRepaired && accordRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.either, minEpoch, barrieredRanges);
if (paxosAndDataRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.paxos, minEpoch, barrieredRanges);
if (accordRepaired) return new ConsensusMigrationRepairResult(ConsensusMigrationRepairType.accord, minEpoch, barrieredRanges);
return INELIGIBLE;
}
public static ConsensusMigrationRepairResult fromPaxosOnlyRepair(Epoch minEpoch, boolean deadNodesExcluded)
{
return fromRepair(minEpoch, false, false, deadNodesExcluded);
return fromRepair(minEpoch, null, false, false, deadNodesExcluded);
}
public static ConsensusMigrationRepairResult fromAccordOnlyRepair(Epoch minEpoch, boolean deadNodesExcluded)
public static ConsensusMigrationRepairResult fromAccordOnlyRepair(Epoch minEpoch, Ranges barrieredRanges, boolean deadNodesExcluded)
{
return fromRepair(minEpoch, false, true, deadNodesExcluded);
return fromRepair(minEpoch, barrieredRanges, false, true, deadNodesExcluded);
}
}

View File

@ -24,16 +24,22 @@ import org.apache.cassandra.utils.LocalizeString;
public enum ConsensusMigrationRepairType
{
ineligible(0),
paxos(1),
accord(2),
either(3);
ineligible(0, false, false),
paxos(1, false, true),
accord(2, true, false),
either(3, true, true);
public final byte value;
ConsensusMigrationRepairType(int value)
public final boolean accordMigrationEligible;
public final boolean paxosMigrationEligible;
ConsensusMigrationRepairType(int value, boolean accordMigrationEligible, boolean paxosMigrationEligible)
{
this.value = SignedBytes.checkedCast(value);
this.accordMigrationEligible = accordMigrationEligible;
this.paxosMigrationEligible = paxosMigrationEligible;
}
public static ConsensusMigrationRepairType fromString(String repairType)

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.service.consensus.migration;
import java.util.Optional;
import javax.annotation.Nonnull;
import com.google.common.annotations.VisibleForTesting;
@ -26,9 +27,12 @@ import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.Keyspaces;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.consensus.TransactionalMode;
@ -41,12 +45,11 @@ import org.apache.cassandra.utils.FBUtilities;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.getConsensusMigratedAt;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationTarget.paxos;
import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.ConsensusRoutingDecision.accord;
import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.ConsensusRoutingDecision.paxosV1;
import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.ConsensusRoutingDecision.paxosV2;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationTarget.paxos;
/**
* Helper class to decide where to route a request that requires consensus, migrating a key if necessary
* before rerouting.
@ -84,13 +87,30 @@ public class ConsensusRequestRouter
return pickPaxos();
}
/*
* Accord never handles local tables, but if the table doesn't exist then we need to generate the correct
* InvalidRequestException.
*/
private static TableMetadata metadata(ClusterMetadata cm, String keyspace, String table)
{
KeyspaceMetadata ksm = cm.schema.getKeyspaceMetadata(keyspace);
TableMetadata tbm = ksm != null ? ksm.getTableOrViewNullable(table) : null;
Optional<KeyspaceMetadata> ksm = cm.schema.maybeGetKeyspaceMetadata(keyspace);
if (ksm.isEmpty())
{
// It's a non-distributed table which is fine, but we want to error if it doesn't exist
// We should never actually reach here unless there is a race with dropping the table
Keyspaces localKeyspaces = Schema.instance.localKeyspaces();
KeyspaceMetadata ksm2 = localKeyspaces.getNullable(keyspace);
if (ksm2 == null)
throw new InvalidRequestException("Keyspace " + keyspace + " does not exist");
// Explicitly including views in case they get used in non-distributed tables
TableMetadata tbm2 = ksm2.getTableOrViewNullable(table);
if (tbm2 == null)
throw new InvalidRequestException("Table " + keyspace + "." + table + " does not exist");
return null;
}
TableMetadata tbm = ksm.get().getTableNullable(table);
if (tbm == null)
throw new IllegalStateException("Can't route consensus request to nonexistent CFS %s.%s".format(keyspace, table));
throw new InvalidRequestException("Table " + keyspace + "." + table + " does not exist");
return tbm;
}
@ -99,18 +119,39 @@ public class ConsensusRequestRouter
{
ClusterMetadata cm = ClusterMetadata.current();
TableMetadata metadata = metadata(cm, keyspace, table);
// Non-distributed tables always take the Paxos path
if (metadata == null)
return pickPaxos();
return routeAndMaybeMigrate(cm, metadata, key, consistencyLevel, requestTime, timeoutNanos, isForWrite);
}
public ConsensusRoutingDecision routeAndMaybeMigrate(@Nonnull DecoratedKey key, @Nonnull TableId tableId, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime, long timeoutNanos, boolean isForWrite)
{
ClusterMetadata cm = ClusterMetadata.current();
TableMetadata metadata = cm.schema.getTableMetadata(tableId);
TableMetadata metadata = getTableMetadata(cm, tableId);
// Non-distributed tables always take the Paxos path
if (metadata == null)
throw new IllegalStateException("Can't route consensus request for nonexistent table %s".format(tableId.toString()));
pickPaxos();
return routeAndMaybeMigrate(cm, metadata, key, consistencyLevel, requestTime, timeoutNanos, isForWrite);
}
public static TableMetadata getTableMetadata(ClusterMetadata cm, TableId tableId)
{
TableMetadata tm = cm.schema.getTableMetadata(tableId);
if (tm == null)
{
// It's a non-distributed table which is fine, but we want to error if it doesn't exist
// We should never actually reach here unless there is a race with dropping the table
Keyspaces localKeyspaces = Schema.instance.localKeyspaces();
TableMetadata tm2 = localKeyspaces.getTableOrViewNullable(tableId);
if (tm2 == null)
throw new InvalidRequestException("Table with id " + tableId + " does not exist");
return null;
}
return tm;
}
protected static boolean mayWriteThroughAccord(TableMetadata metadata)
{
return metadata.params.transactionalMode.writesThroughAccord || metadata.params.transactionalMigrationFrom.writesThroughAccord();
@ -170,7 +211,7 @@ public class ConsensusRequestRouter
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tmd.id);
if (cfs == null)
throw new IllegalStateException("Can't route consensus request to nonexistent CFS %s.%s".format(tmd.keyspace, tmd.name));
throw new InvalidRequestException("Can't route consensus request to nonexistent CFS %s.%s".format(tmd.keyspace, tmd.name));
// If it is locally replicated we can check our local migration state to see if it was already migrated
EndpointsForToken naturalReplicas = ReplicaLayout.forNonLocalStrategyTokenRead(cm, cfs.keyspace.getMetadata(), key.getToken());
boolean isLocallyReplicated = naturalReplicas.lookup(FBUtilities.getBroadcastAddressAndPort()) != null;
@ -191,7 +232,7 @@ public class ConsensusRequestRouter
// barrier transactions to accomplish the migration
// They still might need to go through the fast local path for barrier txns
// at each replica, but they won't create their own txn since we created it here
ConsensusKeyMigrationState.repairKeyAccord(key, tms.keyspaceName, tms.tableId, tms.minMigrationEpoch(key.getToken()).getEpoch(), requestTime, true, isForWrite);
ConsensusKeyMigrationState.repairKeyAccord(key, tms.tableId, tms.minMigrationEpoch(key.getToken()).getEpoch(), requestTime, true, isForWrite);
return paxosV2;
}
// Fall through for repairKeyPaxos

View File

@ -46,6 +46,7 @@ import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.TokenRange;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.paxos.Paxos;
import org.apache.cassandra.streaming.PreviewKind;
@ -56,6 +57,7 @@ import org.apache.cassandra.tcm.serialization.MetadataSerializer;
import org.apache.cassandra.tcm.transformations.BeginConsensusMigrationForTableAndRange;
import org.apache.cassandra.tcm.transformations.MaybeFinishConsensusMigrationForTableAndRange;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@ -97,9 +99,24 @@ public abstract class ConsensusTableMigration
if (!tms.targetProtocol.isMigratedBy(repairResult.consensusMigrationRepairResult.type))
return;
List<Range<Token>> paxosRepairedRanges = ImmutableList.of();
if (migrationResult.type.paxosMigrationEligible)
// Paxos always repairs all ranges requested by the repair although there should be nothing
// repaired in the migrated and Accord managed ranges
paxosRepairedRanges = ImmutableList.copyOf(desc.ranges);
List<Range<Token>> accordBarrieredRanges = ImmutableList.of();
if (migrationResult.type.accordMigrationEligible)
// Accord only barriers ranges it thinks it manages and repair collects which it barriered
// precisely which doesn't have to match what the entire repair covers
accordBarrieredRanges = migrationResult.barrieredRanges.stream()
.map(range -> ((TokenRange)range).toKeyspaceRange())
.collect(toImmutableList());
accordBarrieredRanges = Range.normalize(accordBarrieredRanges);
ClusterMetadataService.instance().commit(
new MaybeFinishConsensusMigrationForTableAndRange(
desc.keyspace, desc.columnFamily, ImmutableList.copyOf(desc.ranges),
desc.keyspace, desc.columnFamily, paxosRepairedRanges, accordBarrieredRanges,
migrationResult.minEpoch, migrationResult.type));
}
@ -283,7 +300,7 @@ public abstract class ConsensusTableMigration
.map(tableName -> {
TableMetadata tm = Schema.instance.getTableMetadata(keyspaceName, tableName);
if (tm == null)
throw new IllegalArgumentException("Unknown table %s.%s".format(keyspaceName, tableName));
throw new IllegalArgumentException(format("Unknown table %s.%s", keyspaceName, tableName));
return tm.id;
})
.collect(toImmutableList()));

View File

@ -23,25 +23,25 @@ package org.apache.cassandra.service.paxos;
import java.io.IOException;
import java.util.function.BiFunction;
import javax.annotation.Nullable;
import com.google.common.base.Objects;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.PartitionUpdate;
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.schema.TableMetadata;
import static org.apache.cassandra.db.SystemKeyspace.*;
import static org.apache.cassandra.db.SystemKeyspace.legacyPaxosTtlSec;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.AFTER;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.BEFORE;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.IS_REPROPOSAL;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.WAS_REPROPOSED_BY;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.SAME;
import static org.apache.cassandra.service.paxos.Commit.CompareResult.WAS_REPROPOSED_BY;
import static org.apache.cassandra.utils.FBUtilities.nowInSeconds;
public class Commit
@ -314,7 +314,7 @@ public class Commit
public Mutation makeMutation()
{
return new Mutation(update);
return new Mutation(update, true);
}
@Override

View File

@ -71,9 +71,9 @@ public interface ReadCoordinator
void sendReadCommand(Message<ReadCommand> message, InetAddressAndPort to, RequestCallback<ReadResponse> callback);
default void notifyOfInitialContacts(EndpointsForToken fullDataRequests, EndpointsForToken transientRequests, EndpointsForToken digestRequests) {}
void sendReadRepairMutation(Message<Mutation> message, InetAddressAndPort to, RequestCallback<Object> callback);
default Mutation maybeAllowOutOfRangeMutations(Mutation m)
default boolean allowsPotentialTransactionConflicts()
{
return m;
return !isEventuallyConsistent();
}
boolean isEventuallyConsistent();
}

View File

@ -52,6 +52,7 @@ import org.apache.cassandra.utils.concurrent.AsyncFuture;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.cassandra.net.Verb.READ_REPAIR_REQ;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
import static org.apache.cassandra.utils.concurrent.CountDownLatch.newCountDownLatch;
@ -154,6 +155,7 @@ public class BlockingPartitionRepair
@VisibleForTesting
protected void sendRR(Message<Mutation> message, InetAddressAndPort endpoint)
{
checkArgument(message.payload.allowsPotentialTransactionConflicts() == coordinator.allowsPotentialTransactionConflicts(), "Mutation allowing transaction conflicts should match coordinator");
coordinator.sendReadRepairMutation(message, endpoint, this);
}
@ -165,8 +167,8 @@ public class BlockingPartitionRepair
for (Map.Entry<Replica, Mutation> entry: pendingRepairs.entrySet())
{
Replica destination = entry.getKey();
Preconditions.checkArgument(destination.isFull(), "Can't send repairs to transient replicas: %s", destination);
Mutation mutation = coordinator.maybeAllowOutOfRangeMutations(entry.getValue());
checkArgument(destination.isFull(), "Can't send repairs to transient replicas: %s", destination);
Mutation mutation = entry.getValue();
TableId tableId = extractUpdate(mutation).metadata().id;
Tracing.trace("Sending read-repair-mutation to {}", destination);
@ -239,7 +241,7 @@ public class BlockingPartitionRepair
if (mutation == null)
{
mutation = BlockingReadRepairs.createRepairMutation(update, repairPlan.consistencyLevel(), replica.endpoint(), true);
mutation = BlockingReadRepairs.createRepairMutation(update, repairPlan.consistencyLevel(), replica.endpoint(), true, coordinator.allowsPotentialTransactionConflicts());
versionedMutations[versionIdx] = mutation;
}
@ -250,7 +252,6 @@ public class BlockingPartitionRepair
continue;
}
mutation = coordinator.maybeAllowOutOfRangeMutations(mutation);
Tracing.trace("Sending speculative read-repair-mutation to {}", replica);
sendRR(Message.out(READ_REPAIR_REQ, mutation), replica.endpoint());
ReadRepairDiagnostics.speculatedWrite(this, replica.endpoint(), mutation);

View File

@ -18,7 +18,6 @@
package org.apache.cassandra.service.reads.repair;
import java.util.Collection;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -34,13 +33,11 @@ import accord.primitives.Keys;
import accord.primitives.Txn;
import com.codahale.metrics.Meter;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.exceptions.ReadTimeoutException;
import org.apache.cassandra.locator.Endpoints;
@ -54,6 +51,7 @@ import org.apache.cassandra.service.accord.txn.TxnQuery;
import org.apache.cassandra.service.accord.txn.TxnRead;
import org.apache.cassandra.service.accord.txn.TxnResult;
import org.apache.cassandra.service.accord.txn.UnrecoverableRepairUpdate;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationMutationHelper;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tracing.Tracing;
@ -195,70 +193,83 @@ public class BlockingReadRepair<E extends Endpoints<E>, P extends ReplicaPlan.Fo
@Override
public void repairPartition(DecoratedKey dk, Map<Replica, Mutation> mutations, ReplicaPlan.ForWrite writePlan)
{
TransactionalMode transactionalMode = command.metadata().params.transactionalMode;
if (coordinator.isEventuallyConsistent() && transactionalMode.blockingReadRepairThroughAccord)
// non-Accord reads only ever touch one table and key so all mutations need to be applied either transactionally
// or non-transactionally (not a mix). There is no retry loop here because read repair is relatively rare so it racing
// with changes to migrating ranges should also be pretty rare so it isn't worth the added complexity. If you were
// to add a retry loop you would need to be careful to correctly set/unset allowPotentialTransactionConflicts in the mutations
// since that is set if it is routed to Accord
//
// If this is an Accord transaction that is in interoperability mode and executing a read repair
// then we take the non-transactional path and the mutations are intercepted in ReadCoordinator.sendRepairMutation
// which will ensure the repair mutation runs in the command store thread after preceding transactions are done
ClusterMetadata cm = ClusterMetadata.current();
if (coordinator.isEventuallyConsistent() && ConsensusMigrationMutationHelper.tokenShouldBeWrittenThroughAccord(cm, command.metadata().id, dk.getToken()))
repairTransactionally(dk, mutations, writePlan);
else
repairNonTransactionally(dk, mutations, writePlan);
}
private void repairTransactionally(DecoratedKey dk, Map<Replica, Mutation> accordMutations, ForWrite writePlan)
{
checkState(coordinator.isEventuallyConsistent(), "Should only repair transactionally for an eventually consistent read coordinator");
PartitionKey partitionKey = new PartitionKey(command.metadata().id, dk);
Keys key = Keys.of(partitionKey);
// This is going create a new BlockingReadRepair inside an Accord transaction which will go down
// the !isEventuallyConsistent path and apply the repairs through Accord command stores using AccordInteropExecution
UnrecoverableRepairUpdate<E, P> repairUpdate = UnrecoverableRepairUpdate.create(AccordService.instance().nodeId(), this, key, dk, accordMutations, writePlan);
Future<TxnResult> repairFuture;
try
{
Collection<PartitionUpdate> partitionUpdates = Mutation.merge(mutations.values()).getPartitionUpdates();
checkState(partitionUpdates.size() == 1, "Expect only one PartitionUpdate");
PartitionUpdate update = partitionUpdates.iterator().next();
PartitionKey partitionKey = PartitionKey.of(update);
Keys key = Keys.of(partitionKey);
// This is going create a new BlockingReadRepair inside an Accord transaction which will go down
// the !isEventuallyConsistent path and apply the repairs through Accord command stores using AccordInteropExecution
UnrecoverableRepairUpdate<E, P> repairUpdate = UnrecoverableRepairUpdate.create(AccordService.instance().nodeId(), this, key, dk, mutations, writePlan);
Future<TxnResult> repairFuture;
try
{
Txn txn = new Txn.InMemory(Txn.Kind.Read, key, TxnRead.createNoOpRead(key), TxnQuery.NONE, repairUpdate);
repairFuture = Stage.ACCORD_MIGRATION.submit(() -> {
try
{
return AccordService.instance().coordinate(txn, ConsistencyLevel.ANY, requestTime);
}
finally
{
// If we successfully ran the repair txn then the update should definitely
// be there for us to clear which means we are sure it was there to be sent
checkNotNull(UnrecoverableRepairUpdate.removeInflightUpdate(repairUpdate.updateKey));
}
});
}
catch (Throwable t)
{
UnrecoverableRepairUpdate.removeInflightUpdate(repairUpdate.updateKey);
throw t;
}
repairs.add(new PendingPartitionRepair()
{
@Override
public boolean awaitRepairs(long remaining, TimeUnit timeUnit) throws InterruptedException, ExecutionException
Txn txn = new Txn.InMemory(Txn.Kind.Read, key, TxnRead.createNoOpRead(key), TxnQuery.NONE, repairUpdate);
repairFuture = Stage.ACCORD_MIGRATION.submit(() -> {
try
{
try
{
repairFuture.get(remaining, timeUnit);
return true;
}
catch (TimeoutException e)
{
return false;
}
return AccordService.instance().coordinate(txn, ConsistencyLevel.ANY, requestTime);
}
@Override
public ForWrite repairPlan()
finally
{
return writePlan;
// If we successfully ran the repair txn then the update should definitely
// be there for us to clear which means we are sure it was there to be sent
checkNotNull(UnrecoverableRepairUpdate.removeInflightUpdate(repairUpdate.updateKey));
}
});
}
else
catch (Throwable t)
{
BlockingPartitionRepair blockingRepair = new BlockingPartitionRepair(coordinator, dk, mutations, writePlan);
blockingRepair.sendInitialRepairs();
repairs.add(blockingRepair);
UnrecoverableRepairUpdate.removeInflightUpdate(repairUpdate.updateKey);
throw t;
}
repairs.add(new PendingPartitionRepair()
{
@Override
public boolean awaitRepairs(long remaining, TimeUnit timeUnit) throws InterruptedException, ExecutionException
{
try
{
repairFuture.get(remaining, timeUnit);
return true;
}
catch (TimeoutException e)
{
return false;
}
}
@Override
public ForWrite repairPlan()
{
return writePlan;
}
});
}
private void repairNonTransactionally(DecoratedKey dk, Map<Replica, Mutation> mutations, ForWrite writePlan)
{
BlockingPartitionRepair blockingRepair = new BlockingPartitionRepair(coordinator, dk, mutations, writePlan);
blockingRepair.sendInitialRepairs();
repairs.add(blockingRepair);
}
public void repairPartitionDirectly(ReadCoordinator readCoordinator, DecoratedKey dk, Map<Replica, Mutation> mutations, ForWrite writePlan)
@ -268,4 +279,10 @@ public class BlockingReadRepair<E extends Endpoints<E>, P extends ReplicaPlan.Fo
delegateRR.maybeSendAdditionalWrites();
delegateRR.awaitWrites();
}
@Override
public boolean coordinatorAllowsPotentialTransactionConflicts()
{
return coordinator.allowsPotentialTransactionConflicts();
}
}

View File

@ -46,13 +46,13 @@ public class BlockingReadRepairs
* Create a read repair mutation from the given update, if the mutation is not larger than the maximum
* mutation size, otherwise return null. Or, if we're configured to be strict, throw an exception.
*/
public static Mutation createRepairMutation(PartitionUpdate update, ConsistencyLevel consistency, InetAddressAndPort destination, boolean suppressException)
public static Mutation createRepairMutation(PartitionUpdate update, ConsistencyLevel consistency, InetAddressAndPort destination, boolean suppressException, boolean allowPotentialTransactionConflicts)
{
if (update == null)
return null;
DecoratedKey key = update.partitionKey();
Mutation mutation = new Mutation(update);
Mutation mutation = new Mutation(update, allowPotentialTransactionConflicts);
int messagingVersion = MessagingService.instance().versions.get(destination);
try

View File

@ -100,4 +100,9 @@ public interface ReadRepair<E extends Endpoints<E>, P extends ReplicaPlan.ForRea
* Repairs a partition using the provided read coordinator
*/
void repairPartitionDirectly(ReadCoordinator coordinator, DecoratedKey partitionKey, Map<Replica, Mutation> mutations, ReplicaPlan.ForWrite writePlan);
default boolean coordinatorAllowsPotentialTransactionConflicts()
{
return false;
}
}

View File

@ -390,7 +390,7 @@ public class RowIteratorMergeListener<E extends Endpoints<E>>
else if (repairs[i] != null)
update = repairs[i].build();
Mutation mutation = BlockingReadRepairs.createRepairMutation(update, readPlan.consistencyLevel(), replica.endpoint(), false);
Mutation mutation = BlockingReadRepairs.createRepairMutation(update, readPlan.consistencyLevel(), replica.endpoint(), false, readRepair.coordinatorAllowsPotentialTransactionConflicts());
if (mutation == null)
continue;

View File

@ -38,8 +38,8 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableParams;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationRepairType;
import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationState;
import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration;
import org.apache.cassandra.service.consensus.migration.TableMigrationState;
import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode;
import org.apache.cassandra.tcm.ClusterMetadata;
@ -56,7 +56,8 @@ import static java.lang.String.format;
import static org.apache.cassandra.dht.Range.intersects;
import static org.apache.cassandra.dht.Range.normalize;
import static org.apache.cassandra.exceptions.ExceptionCode.INVALID;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationTarget.accord;
import static org.apache.cassandra.service.consensus.migration.ConsensusMigrationTarget.paxos;
public class MaybeFinishConsensusMigrationForTableAndRange implements Transformation
{
@ -71,7 +72,10 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
public final String cf;
@Nonnull
public final List<Range<Token>> repairedRanges;
public final List<Range<Token>> paxosRepairedRanges;
@Nonnull
public final List<Range<Token>> accordBarrieredRanges;
@Nonnull
public final Epoch minEpoch;
@ -81,21 +85,23 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
public MaybeFinishConsensusMigrationForTableAndRange(@Nonnull String keyspace,
@Nonnull String cf,
@Nonnull List<Range<Token>> repairedRanges,
@Nonnull List<Range<Token>> paxosRepairedRanges,
@Nonnull List<Range<Token>> accordBarrieredRanges,
@Nonnull Epoch minEpoch,
@Nonnull ConsensusMigrationRepairType repairType)
{
checkNotNull(keyspace, "keyspace should not be null");
checkNotNull(cf, "cf should not be null");
checkNotNull(repairedRanges, "repairedRanges should not be null");
checkArgument(!repairedRanges.isEmpty(), "repairedRanges should not be empty");
checkNotNull(paxosRepairedRanges, "paxosRepairedRanges should not be null");
checkNotNull(accordBarrieredRanges, "accordBarrierRanges should not be null");
checkNotNull(minEpoch, "minEpoch should not be null");
checkArgument(minEpoch.isAfter(Epoch.EMPTY), "minEpoch should not be empty");
checkNotNull(repairType, "repairType is null");
checkArgument(repairType != ConsensusMigrationRepairType.ineligible, "Shouldn't attempt to finish migration with ineligible repair");
this.keyspace = keyspace;
this.cf = cf;
this.repairedRanges = repairedRanges;
this.paxosRepairedRanges = paxosRepairedRanges;
this.accordBarrieredRanges = accordBarrieredRanges;
this.minEpoch = minEpoch;
this.repairType = repairType;
}
@ -125,7 +131,7 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
public Result execute(@Nonnull ClusterMetadata metadata)
{
logger.info("Completed repair {} ranges {}", repairType, repairedRanges);
logger.info("Completed repair eligibiliy '{}' paxos repaired ranges {}, accord repaired ranges {}", repairType, paxosRepairedRanges, accordBarrieredRanges);
checkNotNull(metadata, "clusterMetadata should not be null");
String ksAndCF = keyspace + "." + cf;
TableMetadata tbm = Schema.instance.getTableMetadata(keyspace, cf);
@ -138,8 +144,15 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
return new Rejected(INVALID, format("Table %s is not currently performing consensus migration", ksAndCF));
if (!tms.targetProtocol.isMigratedBy(repairType))
return new Rejected(INVALID, format("Table %s is not currently performing consensus migration to %s and the repair was a %s repair", ksAndCF, tms.targetProtocol, repairType));
return new Rejected(INVALID, format("Table %s has a target protocol of %s and is the repair type %s is not eligible/needed to progress the migration", ksAndCF, tms.targetProtocol, repairType));
List<Range<Token>> repairedRanges;
if (tms.targetProtocol == accord)
repairedRanges = paxosRepairedRanges;
else if (tms.targetProtocol == paxos)
repairedRanges = accordBarrieredRanges;
else
throw new IllegalStateException("Unhandled migration target " + tms.targetProtocol);
List<Range<Token>> normalizedRepairedRanges = normalize(repairedRanges);
// Bail out if repair doesn't actually intersect with any migrating ranges
@ -148,6 +161,8 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
Transformer next = metadata.transformer();
ConsensusMigrationState migrationState = metadata.consensusMigrationState.withRangesRepairedAtEpoch(tbm, normalizedRepairedRanges, minEpoch);
logger.debug("Original migration state {}");
logger.debug("New migration state {}");
next = next.with(migrationState);
// reset the migration value on the table if the migration has completed
@ -165,7 +180,8 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
MaybeFinishConsensusMigrationForTableAndRange v = (MaybeFinishConsensusMigrationForTableAndRange)t;
out.writeUTF(v.keyspace);
out.writeUTF(v.cf);
ConsensusTableMigration.rangesSerializer.serialize(v.repairedRanges, out, version);
ConsensusTableMigration.rangesSerializer.serialize(v.paxosRepairedRanges, out, version);
ConsensusTableMigration.rangesSerializer.serialize(v.accordBarrieredRanges, out, version);
Epoch.serializer.serialize(v.minEpoch, out, version);
out.write(v.repairType.value);
}
@ -174,10 +190,11 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
{
String keyspace = in.readUTF();
String cf = in.readUTF();
List<Range<Token>> repairedRanges = ConsensusTableMigration.rangesSerializer.deserialize(in, version);
List<Range<Token>> paxosRepairedRanges = ConsensusTableMigration.rangesSerializer.deserialize(in, version);
List<Range<Token>> accordBarrieredRanges = ConsensusTableMigration.rangesSerializer.deserialize(in, version);
Epoch minEpoch = Epoch.serializer.deserialize(in, version);
ConsensusMigrationRepairType repairType = ConsensusMigrationRepairType.fromValue(in.readByte());
return new MaybeFinishConsensusMigrationForTableAndRange(keyspace, cf, repairedRanges, minEpoch, repairType);
return new MaybeFinishConsensusMigrationForTableAndRange(keyspace, cf, paxosRepairedRanges, accordBarrieredRanges, minEpoch, repairType);
}
public long serializedSize(Transformation t, Version version)
@ -185,7 +202,8 @@ public class MaybeFinishConsensusMigrationForTableAndRange implements Transforma
MaybeFinishConsensusMigrationForTableAndRange v = (MaybeFinishConsensusMigrationForTableAndRange)t;
return TypeSizes.sizeof(v.keyspace)
+ TypeSizes.sizeof(v.cf)
+ ConsensusTableMigration.rangesSerializer.serializedSize(v.repairedRanges, version)
+ ConsensusTableMigration.rangesSerializer.serializedSize(v.paxosRepairedRanges, version)
+ ConsensusTableMigration.rangesSerializer.serializedSize(v.accordBarrieredRanges, version)
+ Epoch.serializer.serializedSize(v.minEpoch)
+ TypeSizes.sizeof(v.repairType.value);
}

View File

@ -151,6 +151,11 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
return new RequestTime(MonotonicClock.Global.preciseTime.now());
}
public RequestTime withStartedAt(long startedAtNanos)
{
return new RequestTime(enqueuedAtNanos, startedAtNanos);
}
public long startedAtNanos()
{
return startedAtNanos;
@ -426,7 +431,7 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
connection.applyStateTransition(request.type, response.type);
return response;
}
/**
* Note: this method may be executed on the netty event loop.
*/

View File

@ -19,13 +19,18 @@
package org.apache.cassandra.triggers;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
@ -34,7 +39,10 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.CounterMutation;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.exceptions.CassandraException;
import org.apache.cassandra.exceptions.InvalidRequestException;
@ -120,7 +128,7 @@ public class TriggerExecutor
* @throws InvalidRequestException if additional mutations were generated, but
* the initial mutations contains counter updates
*/
public Collection<Mutation> execute(Collection<? extends IMutation> mutations) throws InvalidRequestException
public List<Mutation> execute(Collection<? extends IMutation> mutations) throws InvalidRequestException
{
boolean hasCounters = false;
List<Mutation> augmentedMutations = null;
@ -156,7 +164,7 @@ public class TriggerExecutor
return mergeMutations(Iterables.concat(originalMutations, augmentedMutations));
}
private Collection<Mutation> mergeMutations(Iterable<Mutation> mutations)
private List<Mutation> mergeMutations(Iterable<Mutation> mutations)
{
ListMultimap<Pair<String, ByteBuffer>, Mutation> groupedMutations = ArrayListMultimap.create();

View File

@ -21,11 +21,10 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import com.google.common.annotations.VisibleForTesting;
import org.cliffc.high_scale_lib.NonBlockingHashMap;
import org.slf4j.Logger;
import com.google.common.annotations.VisibleForTesting;
import static org.apache.cassandra.utils.Clock.Global;
/**
@ -78,7 +77,7 @@ public class NoSpamLogger
this.minIntervalNanos = minIntervalNanos;
}
private boolean shouldLog(long nowNanos)
public boolean shouldLog(long nowNanos)
{
long expected = get();
return nowNanos >= expected && compareAndSet(expected, nowNanos + minIntervalNanos);

View File

@ -39,6 +39,8 @@ import org.apache.cassandra.io.FSWriteError;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
import static com.google.common.base.Throwables.getStackTraceAsString;
public final class Throwables
{
public enum FileOpType { READ, WRITE }
@ -48,6 +50,23 @@ public final class Throwables
void perform() throws E;
}
public interface ThrowingRunnable
{
void run() throws Exception;
}
public static void runUnchecked(ThrowingRunnable runnable)
{
try
{
runnable.run();
}
catch (Exception e)
{
throwAsUncheckedException(e);
}
}
public static boolean isCausedBy(Throwable t, Predicate<Throwable> cause)
{
return cause.test(t) || (t.getCause() != null && cause.test(t.getCause()));
@ -340,4 +359,16 @@ public final class Throwables
if (Arrays.stream(causeClasses).noneMatch(c -> anyCauseMatches(err, c::isInstance)))
throw new AssertionError("The exception is not caused by any of " + Arrays.toString(causeClasses), err);
}
public static Object getStackTraceAsToString(Throwable t)
{
return new Object()
{
@Override
public String toString()
{
return getStackTraceAsString(t);
}
};
}
}

View File

@ -18,13 +18,13 @@
package org.apache.cassandra.distributed.api;
import org.apache.cassandra.distributed.shared.FutureUtils;
import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import org.apache.cassandra.distributed.shared.FutureUtils;
// The cross-version API requires that a Coordinator can be constructed without any constructor arguments
public interface ICoordinator
{
@ -82,6 +82,7 @@ public interface ICoordinator
}
Future<SimpleQueryResult> asyncExecuteWithTracingWithResult(UUID sessionId, String query, ConsistencyLevel consistencyLevel, Object... boundValues);
Future<SimpleQueryResult> asyncExecuteWithResult(String query, ConsistencyLevel consistencyLevel, Object... boundValues);
default Object[][] executeWithTracing(UUID sessionId, String query, ConsistencyLevel consistencyLevel, Object... boundValues)
{

View File

@ -102,6 +102,12 @@ public class Coordinator implements ICoordinator
}).call();
}
@Override
public Future<SimpleQueryResult> asyncExecuteWithResult(String query, ConsistencyLevel consistencyLevelOrigin, Object... boundValues)
{
return instance.async(() -> unsafeExecuteInternal(query, consistencyLevelOrigin, boundValues)).call();
}
public static org.apache.cassandra.db.ConsistencyLevel toCassandraCL(ConsistencyLevel cl)
{
try

View File

@ -18,10 +18,8 @@
package org.apache.cassandra.distributed.impl;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
import java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,7 +28,9 @@ import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.ClusterMetadataService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.listeners.ChangeListener;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import org.apache.cassandra.utils.concurrent.WaitQueue;
import org.apache.cassandra.utils.concurrent.WaitQueue.Signal;
public class TestChangeListener implements ChangeListener
{
@ -43,77 +43,68 @@ public class TestChangeListener implements ChangeListener
ClusterMetadataService.instance().log().addListener(instance);
}
private final List<Predicate<Epoch>> preCommitPredicates = new ArrayList<>();
private final List<Predicate<Epoch>> postCommitPredicates = new ArrayList<>();
NavigableMap<Epoch, CommitBarrier> preCommitBarriers = new ConcurrentSkipListMap<>();
NavigableMap<Epoch, CommitBarrier> postCommitBarriers = new ConcurrentSkipListMap<>();
private final WaitQueue waiters = WaitQueue.newWaitQueue();
private class CommitBarrier
{
private final CountDownLatch waiting = CountDownLatch.newCountDownLatch(1);
private final Runnable onPaused;
private final String desc;
private CommitBarrier(Runnable onPaused, String desc)
{
this.onPaused = onPaused;
this.desc = desc;
}
private void await()
{
logger.debug("Notifying paused: {}", desc);
Signal s = waiters.register();
waiting.decrement();
onPaused.run();
s.awaitUninterruptibly();
logger.debug("Unpaused: {}", desc);
}
}
@Override
public void notifyPreCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot)
{
Iterator<Predicate<Epoch>> iter = preCommitPredicates.iterator();
while (iter.hasNext())
{
if (iter.next().test(next.epoch))
{
logger.debug("Epoch matches pre-commit predicate, pausing");
pause();
iter.remove();
}
}
CommitBarrier commitBarrier = preCommitBarriers.remove(next.epoch);
if (commitBarrier != null)
commitBarrier.await();
}
@Override
public void notifyPostCommit(ClusterMetadata prev, ClusterMetadata next, boolean fromSnapshot)
{
Iterator<Predicate<Epoch>> iter = postCommitPredicates.iterator();
while (iter.hasNext())
{
if (iter.next().test(next.epoch))
{
logger.debug("Epoch matches post-commit predicate, pausing");
pause();
iter.remove();
}
}
CommitBarrier commitBarrier = postCommitBarriers.remove(next.epoch);
if (commitBarrier != null)
commitBarrier.await();
}
public void pauseBefore(Epoch epoch, Runnable onMatch)
public void pauseBefore(Epoch epoch, Runnable onPaused)
{
logger.debug("Requesting pause before enacting {}", epoch);
preCommitPredicates.add((e) -> {
if (e.is(epoch))
{
onMatch.run();
return true;
}
return false;
});
preCommitBarriers.put(epoch, new CommitBarrier(onPaused, "pre-commit " + epoch));
}
public void pauseAfter(Epoch epoch, Runnable onMatch)
public void pauseAfter(Epoch epoch, Runnable onPaused)
{
logger.debug("Requesting pause after enacting {}", epoch);
postCommitPredicates.add((e) -> {
if (e.is(epoch))
{
onMatch.run();
return true;
}
return false;
});
}
public void pause()
{
WaitQueue.Signal signal = waiters.register();
logger.debug("Log follower is paused, waiting...");
signal.awaitUninterruptibly();
logger.debug("Resumed log follower...");
postCommitBarriers.put(epoch, new CommitBarrier(onPaused, "post-commit " + epoch));
}
public void unpause()
{
logger.debug("Unpausing log follower");
logger.info("Unpausing all precommit and post commit barriers");
waiters.signalAll();
}
public void clearAndUnpause()
{
preCommitBarriers.clear();
postCommitBarriers.clear();
waiters.signalAll();
}
}

View File

@ -600,6 +600,11 @@ public class ClusterUtils
instance.runOnInstance(() -> TestChangeListener.instance.unpause());
}
public static void clearAndUnpause(IInvokableInstance instance)
{
instance.runOnInstance(() -> TestChangeListener.instance.clearAndUnpause());
}
public static boolean isMigrating(IInvokableInstance instance)
{
return instance.callOnInstance(() -> ClusterMetadataService.instance().isMigrating());

View File

@ -72,13 +72,13 @@ public class CASAddTest extends TestBaseImpl
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL), row(1, null, null));
// this section is testing current limitations... if they start to fail due to the limitations going away... update this test to include those cases
Assertions.assertThatThrownBy(() -> cluster.coordinator(1).execute(batch(
Assertions.assertThatThrownBy(() -> cluster.coordinator(1).execute(unloggedBatch(
"INSERT INTO " + KEYSPACE + ".tbl (pk, a, b) VALUES (1, 0, '') IF NOT EXISTS",
"UPDATE " + KEYSPACE + ".tbl SET a = a + 1, b = b + 'success' WHERE pk = 1 IF EXISTS"
), ConsistencyLevel.QUORUM))
.is(AssertionUtils.is(InvalidRequestException.class))
.hasMessage("Cannot mix IF EXISTS and IF NOT EXISTS conditions for the same row");
Assertions.assertThatThrownBy(() -> cluster.coordinator(1).execute(batch(
Assertions.assertThatThrownBy(() -> cluster.coordinator(1).execute(unloggedBatch(
"INSERT INTO " + KEYSPACE + ".tbl (pk, a, b) VALUES (1, 0, '') IF NOT EXISTS",
"UPDATE " + KEYSPACE + ".tbl SET a = a + 1, b = b + 'success' WHERE pk = 1"

View File

@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.test;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
@ -36,7 +37,10 @@ import org.apache.cassandra.distributed.api.IInstanceConfig;
import org.apache.cassandra.service.paxos.PaxosCommit;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.*;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.LOCAL_QUORUM;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.LOCAL_SERIAL;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.SERIAL;
public class CASMultiDCTest
{
@ -75,6 +79,12 @@ public class CASMultiDCTest
}));
}
@AfterClass
public static void afterClass() throws Throwable
{
CLUSTER.close();
}
@Before
public void setUp()
{

View File

@ -275,7 +275,8 @@ public class MessageFiltersTest extends TestBaseImpl
try (Cluster cluster = init(builder().withNodes(3)
.withConfig(config -> config.with(GOSSIP)
.with(NETWORK)
.set("hinted_handoff_enabled", true))
.set("hinted_handoff_enabled", true)
.set("accord.enabled", false))
.start()))
{
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (k int PRIMARY KEY, v int)"));

View File

@ -146,7 +146,6 @@ public class QueriesTableTest extends TestBaseImpl
// Issue a read to unblock the read generated by the original CAS operation:
SESSION.executeAsync("SELECT * FROM " + KEYSPACE + ".cas_tbl WHERE k = 0");
waitForQueriesToFinish();
}
@ -174,7 +173,7 @@ public class QueriesTableTest extends TestBaseImpl
@Test
public void shouldExposeTransaction() throws Throwable
{
SHARED_CLUSTER.schemaChange("CREATE TABLE " + KEYSPACE + ".accord_tbl (k int primary key, v int) WITH transactional_mode='full'");
SHARED_CLUSTER.schemaChange("CREATE TABLE " + KEYSPACE + ".accord_tbl (k int primary key, v int) WITH transactional_mode='mixed_reads'");
// Disable recovery to make sure only one local read occurs:
for (IInvokableInstance instance : SHARED_CLUSTER)

View File

@ -30,44 +30,45 @@ import java.util.stream.Stream;
import com.google.common.util.concurrent.Uninterruptibles;
import org.junit.Assert;
import org.apache.cassandra.Util;
import org.apache.cassandra.concurrent.SEPExecutor;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.locator.ReplicaUtils;
import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.Throwables;
import org.junit.Test;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import org.apache.cassandra.Util;
import org.apache.cassandra.concurrent.SEPExecutor;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.ReadCommand;
import org.apache.cassandra.db.ReadExecutionController;
import org.apache.cassandra.db.SinglePartitionReadCommand;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.distributed.shared.WithProperties;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.StatsComponent;
import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
import org.apache.cassandra.locator.AbstractReplicationStrategy;
import org.apache.cassandra.locator.EndpointsForToken;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaLayout;
import org.apache.cassandra.locator.ReplicaUtils;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.StorageProxy.LocalReadRunnable;
import org.apache.cassandra.service.reads.ReadCoordinator;
import org.apache.cassandra.service.snapshot.SnapshotManager;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.utils.DiagnosticSnapshotService;
import org.apache.cassandra.utils.Throwables;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
@ -440,7 +441,7 @@ public class RepairDigestTrackingTest extends TestBaseImpl
.load(classLoader, ClassLoadingStrategy.Default.INJECTION);
new ByteBuddy().rebase(ReplicaLayout.class)
.method(named("forTokenReadSorted").and(takesArguments(ClusterMetadata.class, Keyspace.class, AbstractReplicationStrategy.class, Token.class)))
.method(named("forTokenReadSorted").and(takesArguments(ClusterMetadata.class, Keyspace.class, AbstractReplicationStrategy.class, TableId.class, Token.class, ReadCoordinator.class)))
.intercept(MethodDelegation.to(BBHelper.class))
.make()
.load(classLoader, ClassLoadingStrategy.Default.INJECTION);
@ -475,7 +476,7 @@ public class RepairDigestTrackingTest extends TestBaseImpl
}
@SuppressWarnings({ "unused" })
public static ReplicaLayout.ForTokenRead forTokenReadSorted(ClusterMetadata metadata, Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, Token token)
public static ReplicaLayout.ForTokenRead forTokenReadSorted(ClusterMetadata metadata, Keyspace keyspace, AbstractReplicationStrategy replicationStrategy, TableId tableId, Token token, org.apache.cassandra.service.reads.ReadCoordinator coordinator)
{
try
{

View File

@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.IntStream;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@ -90,7 +91,7 @@ public class ShortReadProtectionTest extends TestBaseImpl
public static Collection<Object[]> data()
{
List<Object[]> result = new ArrayList<>();
for (TransactionalMode mode : TransactionalMode.values())
for (TransactionalMode mode : ImmutableList.of(TransactionalMode.mixed_reads, TransactionalMode.off))
for (ConsistencyLevel readConsistencyLevel : Arrays.asList(ALL, QUORUM, SERIAL))
for (boolean flush : BOOLEANS)
for (boolean paging : BOOLEANS)

View File

@ -33,6 +33,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import org.junit.After;
import org.junit.BeforeClass;
@ -58,8 +60,11 @@ import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UUIDType;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ICluster;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInstance;
import org.apache.cassandra.distributed.api.IInstanceConfig;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.NodeToolResult;
import org.apache.cassandra.distributed.shared.DistributedTestBase;
import org.apache.cassandra.service.accord.AccordStateCache;
@ -69,6 +74,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.JOIN_RING;
import static org.apache.cassandra.config.CassandraRelevantProperties.RESET_BOOTSTRAP_PROGRESS;
import static org.apache.cassandra.config.CassandraRelevantProperties.SKIP_GC_INSPECTOR;
import static org.apache.cassandra.distributed.action.GossipHelper.withProperty;
import static org.assertj.core.api.Assertions.fail;
// checkstyle: suppress below 'blockSystemPropertyUsage'
public class TestBaseImpl extends DistributedTestBase
@ -140,10 +146,17 @@ public class TestBaseImpl extends DistributedTestBase
return tupleType.pack(bbs, ByteBufferAccessor.instance);
}
public static String batch(String... queries)
public static String unloggedBatch(String... queries)
{
return batch(false, queries);
}
public static String batch(boolean logged, String... queries)
{
StringBuilder sb = new StringBuilder();
sb.append("BEGIN UNLOGGED BATCH\n");
sb.append("BEGIN ");
sb.append(logged ? "" : "UNLOGGED ");
sb.append("BATCH\n");
for (String q : queries)
sb.append(q).append(";\n");
sb.append("APPLY BATCH;");
@ -253,4 +266,31 @@ public class TestBaseImpl extends DistributedTestBase
for (int i = 1; i < cluster.size() + 1; i++)
cluster.get(i).nodetool("disableautocompaction", keyspace, table);
}
public static String nodetool(IInstance instance, String... commandAndArgs)
{
NodeToolResult nodetoolResult = instance.nodetoolResult(commandAndArgs);
if (!nodetoolResult.getStdout().isEmpty())
System.out.println(nodetoolResult.getStdout());
if (!nodetoolResult.getStderr().isEmpty())
System.err.println(nodetoolResult.getStderr());
if (nodetoolResult.getError() != null)
fail("Failed nodetool " + Arrays.asList(commandAndArgs), nodetoolResult.getError());
// TODO why does standard out end up in stderr in nodetool?
return nodetoolResult.getStdout();
}
public static String nodetool(ICoordinator coordinator, String... commandAndArgs)
{
return nodetool(coordinator.instance(), commandAndArgs);
}
public static ListenableFuture<String> nodetoolAsync(ICoordinator coordinator, String... commandAndArgs)
{
ListenableFutureTask<String> task = ListenableFutureTask.create(() -> nodetool(coordinator, commandAndArgs));
Thread asyncThread = new Thread(task, "NodeTool: " + Arrays.asList(commandAndArgs));
asyncThread.setDaemon(true);
asyncThread.start();
return task;
}
}

View File

@ -36,12 +36,12 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.local.cfk.CommandsForKey;
import accord.impl.SimpleProgressLog;
import accord.local.Node;
import accord.local.PreLoadContext;
import accord.local.SafeCommand;
import accord.local.Status;
import accord.local.cfk.CommandsForKey;
import accord.primitives.Seekables;
import accord.primitives.Timestamp;
import accord.primitives.TxnId;
@ -51,6 +51,7 @@ import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.gms.FailureDetector;
@ -147,13 +148,15 @@ public class AccordIncrementalRepairTest extends AccordTestBase
public static void setupClass() throws Throwable
{
CassandraRelevantProperties.ACCORD_AGENT_CLASS.setString(BarrierRecordingAgent.class.getName());
// setupCluster(opt -> opt.withConfig(conf -> conf.with(Feature.NETWORK, Feature.GOSSIP)), 3);
setupCluster(opt -> opt, 3);
setupCluster(opt -> opt.withConfig(conf -> conf.with(Feature.NETWORK, Feature.GOSSIP)), 3);
// setupCluster(opt -> opt, 3);
}
@After
public void tearDown()
{
for (IInvokableInstance instance : SHARED_CLUSTER)
instance.runOnInstance(() -> SimpleProgressLog.PAUSE_FOR_TEST = false);
SHARED_CLUSTER.filters().reset();
}
@ -248,16 +251,16 @@ public class AccordIncrementalRepairTest extends AccordTestBase
@Test
public void txnRepairTest() throws Throwable
{
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='full' AND fast_path={'size':2};", KEYSPACE, tableName));
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='full' AND fast_path={'size':2};", KEYSPACE, accordTableName));
final String keyspace = KEYSPACE;
final String table = tableName;
final String table = accordTableName;
SHARED_CLUSTER.filters().allVerbs().to(3).drop();
awaitEndpointDown(SHARED_CLUSTER.get(1), SHARED_CLUSTER.get(3));
executeWithRetry(SHARED_CLUSTER, format("BEGIN TRANSACTION\n" +
"INSERT INTO %s (k, v) VALUES (1, 1);\n" +
"COMMIT TRANSACTION", qualifiedTableName));
"COMMIT TRANSACTION", qualifiedAccordTableName));
SHARED_CLUSTER.get(1, 2).forEach(instance -> instance.runOnInstance(() -> {
TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
@ -291,7 +294,7 @@ public class AccordIncrementalRepairTest extends AccordTestBase
});
SHARED_CLUSTER.filters().reset();
awaitEndpointUp(SHARED_CLUSTER.get(1), SHARED_CLUSTER.get(3));
SHARED_CLUSTER.get(1).nodetool("repair", KEYSPACE);
nodetool(SHARED_CLUSTER.get(1), "repair", KEYSPACE);
SHARED_CLUSTER.forEach(instance -> {
instance.runOnInstance(() -> {
@ -307,9 +310,9 @@ public class AccordIncrementalRepairTest extends AccordTestBase
private void testSingleNodeWrite(TransactionalMode mode)
{
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='%s';", KEYSPACE, tableName, mode));
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='%s';", KEYSPACE, accordTableName, mode));
final String keyspace = KEYSPACE;
final String table = tableName;
final String table = accordTableName;
SHARED_CLUSTER.get(3).runOnInstance(() -> {
QueryProcessor.executeInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (1, 2);", keyspace, table));
@ -344,7 +347,7 @@ public class AccordIncrementalRepairTest extends AccordTestBase
agent().reset();
}));
SHARED_CLUSTER.get(1).nodetool("repair", KEYSPACE);
nodetool(SHARED_CLUSTER.get(1), "repair", KEYSPACE);
SHARED_CLUSTER.forEach(instance -> instance.runOnInstance(() -> {
Assert.assertFalse( agent().executedBarriers().isEmpty());
ColumnFamilyStore cfs = Keyspace.open(keyspace).getColumnFamilyStore(table);
@ -382,9 +385,9 @@ public class AccordIncrementalRepairTest extends AccordTestBase
@Test
public void onlyAccordTest()
{
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='full' AND fast_path={'size':2};", KEYSPACE, tableName));
SHARED_CLUSTER.schemaChange(format("CREATE TABLE %s.%s (k int primary key, v int) WITH transactional_mode='full' AND fast_path={'size':2};", KEYSPACE, accordTableName));
final String keyspace = KEYSPACE;
final String table = tableName;
final String table = accordTableName;
SHARED_CLUSTER.filters().allVerbs().to(3).drop();
awaitEndpointDown(SHARED_CLUSTER.get(1), SHARED_CLUSTER.get(3));
@ -392,7 +395,7 @@ public class AccordIncrementalRepairTest extends AccordTestBase
executeWithRetry(SHARED_CLUSTER, format("BEGIN TRANSACTION\n" +
"INSERT INTO %s (k, v) VALUES (1, 1);\n" +
"COMMIT TRANSACTION", qualifiedTableName));
"COMMIT TRANSACTION", qualifiedAccordTableName));
SHARED_CLUSTER.get(1, 2).forEach(instance -> instance.runOnInstance(() -> {
TableMetadata metadata = Schema.instance.getTableMetadata(keyspace, table);
@ -403,7 +406,7 @@ public class AccordIncrementalRepairTest extends AccordTestBase
SHARED_CLUSTER.filters().reset();
awaitEndpointUp(SHARED_CLUSTER.get(1), SHARED_CLUSTER.get(3));
SHARED_CLUSTER.get(1).nodetool("repair", "--accord-only", KEYSPACE);
nodetool(SHARED_CLUSTER.get(1), "repair", "--accord-only", KEYSPACE);
SHARED_CLUSTER.forEach(instance -> {
logger().info("checking instance {}", instance.broadcastAddress());

View File

@ -59,10 +59,10 @@ public class AccordIntegrationTest extends AccordTestBase
IMessageFilters.Filter lostCommit = cluster.filters().verbs(Verb.ACCORD_COMMIT_REQ.id).to(2).drop();
String query = "BEGIN TRANSACTION\n" +
" LET row1 = (SELECT v FROM " + qualifiedTableName + " WHERE k=0 AND c=0);\n" +
" LET row1 = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=0 AND c=0);\n" +
" SELECT row1.v;\n" +
" IF row1 IS NULL THEN\n" +
" INSERT INTO " + qualifiedTableName + " (k, c, v) VALUES (0, 0, 1);\n" +
" INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (0, 0, 1);\n" +
" END IF\n" +
"COMMIT TRANSACTION";
// row1.v shouldn't have existed when the txn's SELECT was executed
@ -73,24 +73,24 @@ public class AccordIntegrationTest extends AccordTestBase
// Querying again should trigger recovery...
query = "BEGIN TRANSACTION\n" +
" LET row1 = (SELECT v FROM " + qualifiedTableName + " WHERE k=0 AND c=0);\n" +
" LET row1 = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=0 AND c=0);\n" +
" SELECT row1.v;\n" +
" IF row1.v = 1 THEN\n" +
" UPDATE " + qualifiedTableName + " SET v=2 WHERE k = 0 AND c = 0;\n" +
" UPDATE " + qualifiedAccordTableName + " SET v=2 WHERE k = 0 AND c = 0;\n" +
" END IF\n" +
"COMMIT TRANSACTION";
assertRowEqualsWithPreemptedRetry(cluster, new Object[] { 1 }, query);
String check = "BEGIN TRANSACTION\n" +
" SELECT * FROM " + qualifiedTableName + " WHERE k = ? AND c = ?;\n" +
" SELECT * FROM " + qualifiedAccordTableName + " WHERE k = ? AND c = ?;\n" +
"COMMIT TRANSACTION";
assertRowEqualsWithPreemptedRetry(cluster, new Object[] {0, 0, 2}, check, 0, 0);
query = "BEGIN TRANSACTION\n" +
" LET row1 = (SELECT v FROM " + qualifiedTableName + " WHERE k=0 AND c=0);\n" +
" LET row1 = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=0 AND c=0);\n" +
" SELECT row1.v;\n" +
" IF row1 IS NULL THEN\n" +
" INSERT INTO " + qualifiedTableName + " (k, c, v) VALUES (0, 0, 3);\n" +
" INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (0, 0, 3);\n" +
" END IF\n" +
"COMMIT TRANSACTION";
assertRowEqualsWithPreemptedRetry(cluster, new Object[] { 2 }, query);
@ -113,16 +113,16 @@ public class AccordIntegrationTest extends AccordTestBase
})).drop();
String query = "BEGIN TRANSACTION\n" +
" LET row1 = (SELECT * FROM " + qualifiedTableName + " WHERE k = 0 AND c = 0);\n" +
" LET row1 = (SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 0 AND c = 0);\n" +
" SELECT row1.v;\n" +
" IF row1 IS NULL THEN\n" +
" INSERT INTO " + qualifiedTableName + " (k, c, v) VALUES (0, 0, 1);\n" +
" INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (0, 0, 1);\n" +
" END IF\n" +
"COMMIT TRANSACTION";
assertRowEqualsWithPreemptedRetry(cluster, new Object[] { null }, query);
String check = "BEGIN TRANSACTION\n" +
" SELECT * FROM " + qualifiedTableName + " WHERE k = ? AND c = ?;\n" +
" SELECT * FROM " + qualifiedAccordTableName + " WHERE k = ? AND c = ?;\n" +
"COMMIT TRANSACTION";
assertRowEqualsWithPreemptedRetry(cluster, new Object[] { 0, 0, 1 }, check, 0, 0);
});

View File

@ -48,15 +48,15 @@ public class AccordInteroperabilityTest extends AccordTestBase
@Test
public void testSerialReadDescending() throws Throwable
{
test("CREATE TABLE " + qualifiedTableName + " (k int, c int, v int, PRIMARY KEY(k, c)) WITH transactional_mode='full'",
test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY(k, c)) WITH transactional_mode='full'",
cluster -> {
ICoordinator coordinator = cluster.coordinator(1);
for (int i = 1; i <= 10; i++)
coordinator.execute("INSERT INTO " + qualifiedTableName + " (k, c, v) VALUES (0, ?, ?) USING TIMESTAMP 0;", ConsistencyLevel.ALL, i, i * 10);
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedTableName + " WHERE k=0 ORDER BY c DESC LIMIT 1", AssertUtils.row(10, 100));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedTableName + " WHERE k=0 ORDER BY c DESC LIMIT 2", AssertUtils.row(10, 100), AssertUtils.row(9, 90));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedTableName + " WHERE k=0 ORDER BY c DESC LIMIT 3", AssertUtils.row(10, 100), AssertUtils.row(9, 90), AssertUtils.row(8, 80));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedTableName + " WHERE k=0 ORDER BY c DESC LIMIT 4", AssertUtils.row(10, 100), AssertUtils.row(9, 90), AssertUtils.row(8, 80), AssertUtils.row(7, 70));
coordinator.execute("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (0, ?, ?) USING TIMESTAMP 0;", ConsistencyLevel.ALL, i, i * 10);
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedAccordTableName + " WHERE k=0 ORDER BY c DESC LIMIT 1", AssertUtils.row(10, 100));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedAccordTableName + " WHERE k=0 ORDER BY c DESC LIMIT 2", AssertUtils.row(10, 100), AssertUtils.row(9, 90));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedAccordTableName + " WHERE k=0 ORDER BY c DESC LIMIT 3", AssertUtils.row(10, 100), AssertUtils.row(9, 90), AssertUtils.row(8, 80));
assertRowSerial(cluster, "SELECT c, v FROM " + qualifiedAccordTableName + " WHERE k=0 ORDER BY c DESC LIMIT 4", AssertUtils.row(10, 100), AssertUtils.row(9, 90), AssertUtils.row(8, 80), AssertUtils.row(7, 70));
}
);
}

View File

@ -64,7 +64,7 @@ public class AccordLoadTest extends AccordTestBase
@Test
public void testLoad() throws Exception
{
test("CREATE TABLE " + qualifiedTableName + " (k int, v int, PRIMARY KEY(k)) WITH transactional_mode = 'full'",
test("CREATE TABLE " + qualifiedAccordTableName + " (k int, v int, PRIMARY KEY(k)) WITH transactional_mode = 'full'",
cluster -> {
final ConcurrentHashMap<Verb, AtomicInteger> verbs = new ConcurrentHashMap<>();
@ -86,7 +86,7 @@ public class AccordLoadTest extends AccordTestBase
final float readChance = 0.33f;
long nextRepairAt = repairInterval;
for (int i = 1; i <= keyCount; i++)
coordinator.execute("INSERT INTO " + qualifiedTableName + " (k, v) VALUES (0, 0) USING TIMESTAMP 0;", ConsistencyLevel.ALL, i);
coordinator.execute("INSERT INTO " + qualifiedAccordTableName + " (k, v) VALUES (0, 0) USING TIMESTAMP 0;", ConsistencyLevel.ALL, i);
Random random = new Random();
// CopyOnWriteArrayList<Throwable> exceptions = new CopyOnWriteArrayList<>();
@ -109,7 +109,7 @@ public class AccordLoadTest extends AccordTestBase
inFlight.release();
if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart));
// else exceptions.add(fail);
}, "SELECT * FROM " + qualifiedTableName + " WHERE k = ?;", ConsistencyLevel.SERIAL, random.nextInt(keyCount));
}, "SELECT * FROM " + qualifiedAccordTableName + " WHERE k = ?;", ConsistencyLevel.SERIAL, random.nextInt(keyCount));
}
else
{
@ -117,7 +117,7 @@ public class AccordLoadTest extends AccordTestBase
inFlight.release();
if (fail == null) histogram.add(NANOSECONDS.toMicros(System.nanoTime() - commandStart));
// else exceptions.add(fail);
}, "UPDATE " + qualifiedTableName + " SET v += 1 WHERE k = ? IF EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, random.nextInt(keyCount));
}, "UPDATE " + qualifiedAccordTableName + " SET v += 1 WHERE k = ? IF EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.QUORUM, random.nextInt(keyCount));
}
}
@ -125,7 +125,7 @@ public class AccordLoadTest extends AccordTestBase
{
nextRepairAt += repairInterval;
System.out.println("repairing...");
cluster.coordinator(1).instance().nodetool("repair", qualifiedTableName);
cluster.coordinator(1).instance().nodetool("repair", qualifiedAccordTableName);
}
final Date date = new Date();

View File

@ -73,16 +73,16 @@ public class AccordMetricsTest extends AccordTestBase
String writeCql()
{
return "BEGIN TRANSACTION\n" +
" LET val = (SELECT v FROM " + qualifiedTableName + " WHERE k=? AND c=?);\n" +
" LET val = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=? AND c=?);\n" +
" SELECT val.v;\n" +
" UPDATE " + qualifiedTableName + " SET v = v + 1 WHERE k=? AND c=?;\n" +
" UPDATE " + qualifiedAccordTableName + " SET v = v + 1 WHERE k=? AND c=?;\n" +
"COMMIT TRANSACTION";
}
String readCql()
{
return "BEGIN TRANSACTION\n" +
" LET val = (SELECT v FROM " + qualifiedTableName + " WHERE k=? AND c=?);\n" +
" LET val = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=? AND c=?);\n" +
" SELECT val.v;\n" +
"COMMIT TRANSACTION";
}
@ -93,7 +93,7 @@ public class AccordMetricsTest extends AccordTestBase
public void beforeTest()
{
SHARED_CLUSTER.filters().reset();
SHARED_CLUSTER.schemaChange("CREATE TABLE " + qualifiedTableName + " (k int, c int, v int, PRIMARY KEY (k, c)) WITH " + TransactionalMode.full.asCqlParam());
SHARED_CLUSTER.schemaChange("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY (k, c)) WITH " + TransactionalMode.full.asCqlParam());
}
@Test

View File

@ -0,0 +1,771 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.test.accord;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.RoutingKey;
import accord.messages.PreAccept;
import accord.primitives.PartialKeyRoute;
import accord.primitives.PartialRoute;
import accord.primitives.Routable.Domain;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.Util;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.Config.PaxosVariant;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInstance;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IMessage;
import org.apache.cassandra.distributed.api.IMessageSink;
import org.apache.cassandra.distributed.api.QueryResults;
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.distributed.impl.Instance;
import org.apache.cassandra.distributed.impl.TestChangeListener;
import org.apache.cassandra.distributed.shared.ClusterUtils;
import org.apache.cassandra.exceptions.CoordinatorBehindException;
import org.apache.cassandra.exceptions.RequestFailure;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.hints.HintsService;
import org.apache.cassandra.metrics.AccordClientRequestMetrics;
import org.apache.cassandra.metrics.ClientRequestsMetricsHolder;
import org.apache.cassandra.metrics.HintsServiceMetrics;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Promise;
import org.eclipse.jetty.util.ConcurrentHashSet;
import static java.lang.String.format;
import static org.apache.cassandra.Util.expectException;
import static org.apache.cassandra.Util.spinAssertEquals;
import static org.apache.cassandra.config.CassandraRelevantProperties.HINT_DISPATCH_INTERVAL_MS;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
import static org.apache.cassandra.distributed.shared.ClusterUtils.getNextEpoch;
import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseAfterEnacting;
import static org.apache.cassandra.distributed.shared.ClusterUtils.pauseBeforeEnacting;
import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseEnactment;
import static org.apache.cassandra.distributed.test.accord.AccordMigrationRaceTestBase.Scenario.BATCHLOG_FAILED_ROUTING_THEN_HINT;
import static org.apache.cassandra.distributed.test.accord.AccordMigrationRaceTestBase.Scenario.BATCHLOG_FAILED_TIMEOUT_THEN_HINT;
import static org.apache.cassandra.distributed.test.accord.AccordMigrationRaceTestBase.Scenario.BATCHLOG_SUCCESSFUL_ROUTING;
import static org.apache.cassandra.distributed.test.accord.AccordMigrationRaceTestBase.Scenario.HINT;
import static org.apache.cassandra.distributed.test.accord.AccordMigrationRaceTestBase.Scenario.MUTATION;
import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat;
import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM;
import static org.apache.cassandra.utils.Throwables.runUnchecked;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/*
* Test that non-transactional write operations such as regular mutations, batch log, and hints
* all detect when a migration is in progress, and then retry on the correct system.
*/
public abstract class AccordMigrationRaceTestBase extends AccordTestBase
{
private static final Logger logger = LoggerFactory.getLogger(AccordMigrationRaceTestBase.class);
private static final int CLUSTERING_VALUE = 1;
private static final String TABLE_FMT = "CREATE TABLE %s (id int, c int, v int, PRIMARY KEY ((id), c));";
public static final int PKEY_ACCORD = 3;
public static final int PKEY_NORMAL = 0;
private static IPartitioner partitioner;
private static Token minToken;
private static Token maxToken;
private static Token midToken;
private static Token upperMidToken;
private static Token lowerMidToken;
private static ICoordinator coordinator;
private final static TestMessageSink messageSink = new TestMessageSink();
private static class TestMessageSink implements IMessageSink
{
private final Queue<Pair<InetSocketAddress,IMessage>> messages = new ConcurrentLinkedQueue<>();
private final Set<InetSocketAddress> blackholed = new ConcurrentHashSet<>();
public void reset()
{
messages.clear();
blackholed.clear();
}
@Override
public void accept(InetSocketAddress to, IMessage message) {
messages.offer(Pair.create(to,message));
IInstance i = SHARED_CLUSTER.get(to);
if (blackholed.contains(to) || blackholed.contains(message.from()))
return;
if (i != null)
i.receiveMessage(message);
}
}
enum Scenario
{
// Apply the mutation from the coordinator directly without going through hinting
MUTATION(false, false, false, false, false),
// Hint from the initial mutation coordination
HINT(true, false, true, false, true),
// Apply the mutation from the batchlog directly
BATCHLOG_SUCCESSFUL_ROUTING(false, true, true, true, false),
// Have the batchlog use hints to apply the mutation after failing to route, migrating back from Accord this is a timeout because you can't get Accord to fail at routing
// it either executes correctly in the old epoch or times out waiting for the new one to arrive
BATCHLOG_FAILED_ROUTING_THEN_HINT(false, true, true, true, true),
// Have the batchlog use hints to apply the mutation after a timeout
BATCHLOG_FAILED_TIMEOUT_THEN_HINT(false, true, true, true, true),
;
final boolean initiallyEnableHints;
final boolean initiallyEnableBatchlogReplay;
final boolean initiallyBlockTestKeyspaceMutations;
final boolean passesThroughBatchlog;
final boolean deliversViaHint;
Scenario(boolean initiallyEnableHints, boolean initiallyEnableBatchlogReplay, boolean initiallyBlockTestKeyspaceMutations, boolean passesThroughBatchlog, boolean deliversViaHint)
{
this.initiallyEnableHints = initiallyEnableHints;
this.initiallyEnableBatchlogReplay = initiallyEnableBatchlogReplay;
this.initiallyBlockTestKeyspaceMutations = initiallyBlockTestKeyspaceMutations;
this.passesThroughBatchlog = passesThroughBatchlog;
this.deliversViaHint = deliversViaHint;
}
}
private final boolean migrateAwayFromAccord;
protected AccordMigrationRaceTestBase()
{
this.migrateAwayFromAccord = migratingAwayFromAccord();
}
protected abstract boolean migratingAwayFromAccord();
@Override
protected Logger logger()
{
return logger;
}
@BeforeClass
public static void setupClass() throws IOException
{
HINT_DISPATCH_INTERVAL_MS.setLong(100);
ServerTestUtils.daemonInitialization();
// Otherwise repair complains if you don't specify a keyspace
CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.setInt(3);
AccordTestBase.setupCluster(builder -> builder.appendConfig(config -> config.set("paxos_variant", PaxosVariant.v2.name())
.set("write_request_timeout", "2s")
.set("accord.range_migration", "explicit")), 3);
partitioner = FBUtilities.newPartitioner(SHARED_CLUSTER.get(1).callsOnInstance(() -> DatabaseDescriptor.getPartitioner().getClass().getSimpleName()).call());
StorageService.instance.setPartitionerUnsafe(partitioner);
ServerTestUtils.prepareServerNoRegister();
minToken = partitioner.getMinimumToken();
maxToken = partitioner.getMaximumTokenForSplitting();
midToken = partitioner.midpoint(minToken, maxToken);
upperMidToken = partitioner.midpoint(midToken, maxToken);
lowerMidToken = partitioner.midpoint(minToken, midToken);
coordinator = SHARED_CLUSTER.coordinator(1);
SHARED_CLUSTER.setMessageSink(messageSink);
}
@AfterClass
public static void tearDownClass()
{
StorageService.instance.resetPartitionerUnsafe();
}
@After
public void tearDown() throws Exception
{
messageSink.reset();
forEach(() -> {
BatchlogManager.instance.resumeReplay();
HintsService.instance.deleteAllHintsUnsafe();
HintsService.instance.resumeDispatch();
});
SHARED_CLUSTER.forEach(ClusterUtils::clearAndUnpause);
super.tearDown();
// Reset migration state
forEach(() -> {
ConsensusRequestRouter.resetInstance();
ConsensusKeyMigrationState.reset();
});
truncateSystemTables();
}
private ListenableFuture<Void> alterTableTransactionalModeAsync(TransactionalMode mode)
{
ListenableFutureTask<Void> task = ListenableFutureTask.create(() -> {
coordinator.execute(format("ALTER TABLE %s WITH %s", qualifiedAccordTableName, mode.asCqlParam()), ALL);
}, null);
Thread asyncThread = new Thread(task, "Alter table transaction mode " + mode);
asyncThread.setDaemon(true);
asyncThread.start();
return task;
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchTwoTablesOnePkey() throws Throwable
{
testSplitAndRetryMutationCoordination(twoTableBatchInsert(false, PKEY_ACCORD, PKEY_ACCORD, 1), validateTwoTable(PKEY_ACCORD));
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchTwoTablesOnePkeyHinting() throws Throwable
{
// Accord doesn't hint if a write times out
if (!migrateAwayFromAccord)
testSplitAndRetryHintDelivery(twoTableBatchInsert(false, PKEY_ACCORD, PKEY_ACCORD, 1), validateTwoTable(PKEY_ACCORD));
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchTwoTablesTwoPkey() throws Throwable
{
testSplitAndRetryMutationCoordination(twoTableBatchInsert(false, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchTwoTablesHinting() throws Throwable
{
// Accord doesn't hint if a write times out
if (!migrateAwayFromAccord)
testSplitAndRetryHintDelivery(twoTableBatchInsert(false, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchSingleTable() throws Throwable
{
testSplitAndRetryMutationCoordination(singleTableBatchInsert(false, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
@Test
public void testSplitAndRetryNonSerialUnloggedBatchSingleTableHinting() throws Throwable
{
// Accord doesn't hint if a write times out
if (!migrateAwayFromAccord)
testSplitAndRetryHintDelivery(singleTableBatchInsert(false, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
/*
* This doesn't really test much since on top of testSplitAndRetryNonSerialUnloggedBatchTwoTablesOnePkey since it is
* a single table & key and will be converted to an unlogged batch
*/
@Test
public void testSplitAndRetryNonSerialLoggedBatchTwoTablesOnePkey() throws Throwable
{
testSplitAndRetryMutationCoordination(twoTableBatchInsert(true, PKEY_ACCORD, PKEY_ACCORD, 1), validateTwoTable(PKEY_ACCORD));
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchTwoTablesTwoPkey() throws Throwable
{
testSplitAndRetryMutationCoordination(twoTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchTwoTablesTwoPkeyDeliverViaBatchLog() throws Throwable
{
testSplitAndRetryBatchlogDelivery(twoTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchTwoTablesTwoPkeyHintedViaBatchLogTimeout() throws Throwable
{
testSplitAndRetryHintDeliveryAfterBatchlogTimeout(twoTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchTwoTablesTwoPkeyHintedViaBatchLogRoutingFailure() throws Throwable
{
testSplitAndRetryHintDeliveryAfterBatchlogRoutingFailure(twoTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), validateTwoTable(PKEY_NORMAL));
}
/*
* Test that a logged batch writing to a migrating table and a non-migrating table can
*/
@Test
public void testSplitAndRetryNonSerialLoggedBatchSingleTable() throws Throwable
{
testSplitAndRetryBatchlogDelivery(singleTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchSingleTableDeliverViaBatchLog() throws Throwable
{
testSplitAndRetryBatchlogDelivery(singleTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchSingleTableHintedViaBatchLogTimeout() throws Throwable
{
testSplitAndRetryHintDeliveryAfterBatchlogTimeout(singleTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
@Test
public void testSplitAndRetryNonSerialLoggedBatchSingleTableHintedViaBatchLogRoutingFailure() throws Throwable
{
testSplitAndRetryHintDeliveryAfterBatchlogRoutingFailure(singleTableBatchInsert(true, PKEY_ACCORD, PKEY_NORMAL, 1), this::validateSingleTable);
}
private void testSplitAndRetryMutationCoordination(String batchCQL, Consumer<Cluster> validation) throws Throwable
{
testSplitAndRetry(batchCQL, validation, MUTATION);
}
private void testSplitAndRetryBatchlogDelivery(String batchCQL, Consumer<Cluster> validation) throws Throwable
{
testSplitAndRetry(batchCQL, validation, BATCHLOG_SUCCESSFUL_ROUTING);
}
private void testSplitAndRetryHintDeliveryAfterBatchlogTimeout(String batchCQL, Consumer<Cluster> validation) throws Throwable
{
testSplitAndRetry(batchCQL, validation, BATCHLOG_FAILED_TIMEOUT_THEN_HINT);
}
private void testSplitAndRetryHintDeliveryAfterBatchlogRoutingFailure(String batchCQL, Consumer<Cluster> validation) throws Throwable
{
testSplitAndRetry(batchCQL, validation, BATCHLOG_FAILED_ROUTING_THEN_HINT);
}
private void testSplitAndRetryHintDelivery(String batchCQL, Consumer<Cluster> validation) throws Throwable
{
testSplitAndRetry(batchCQL, validation, HINT);
}
private void validateSingleTable(Cluster cluster)
{
SimpleQueryResult expected = QueryResults.builder()
.columns("id", "c", "v")
.row(PKEY_NORMAL, 1, 1)
.row(PKEY_ACCORD, 1, 1)
.build();
cluster.forEach(instance -> {
assertThat(instance.executeInternalWithResult("SELECT * FROM " + qualifiedAccordTableName)).isEqualTo(expected);
});
}
private Consumer<Cluster> validateTwoTable(int secondPkey)
{
return cluster -> {
SimpleQueryResult expectedAccord = QueryResults.builder()
.columns("id", "c", "v")
.row(PKEY_ACCORD, 1, 1)
.build();
cluster.forEach(instance -> assertThat(instance.executeInternalWithResult("SELECT * FROM " + qualifiedAccordTableName)).isEqualTo(expectedAccord));
SimpleQueryResult expectedNormal = QueryResults.builder()
.columns("id", "c", "v")
.row(secondPkey, 1, 1)
.build();
cluster.forEach(instance -> assertThat(instance.executeInternalWithResult("SELECT * FROM " + qualifiedRegularTableName)).isEqualTo(expectedNormal));
};
}
/*
* Test if the coordinator is behind that the request can be re-split and routed to the correct systems
* without surfacing an error
*/
private void testSplitAndRetry(String batchCQL, Consumer<Cluster> validation, Scenario scenario) throws Throwable
{
test(createTables(TABLE_FMT, qualifiedRegularTableName, qualifiedAccordTableName),
cluster -> {
// Only enable these when testing it works from a specific instance
forEach(() -> BatchlogManager.instance.pauseReplay());
forEach(() -> HintsService.instance.pauseDispatch());
// Node 3 is always the out of sync node
IInvokableInstance outOfSyncInstance = setUpOutOfSyncNode(cluster);
// Force the batchlog Accord txn to run after this write txn in the new epoch where it
// will trigger RetryDifferentSystem
if (scenario == BATCHLOG_FAILED_ROUTING_THEN_HINT && migrateAwayFromAccord)
writeAccordRowViaAccord();
// Need to be able to block writing to the test keyspace forcing batchlog replay
// without also failing writes to the batch log
if (scenario.initiallyBlockTestKeyspaceMutations)
cluster.filters().outbound().messagesMatching((from, to, message) -> {
if (message.verb() == Verb.MUTATION_REQ.id)
{
String keyspace = cluster.get(to).callsOnInstance(() -> ((Message<Mutation>)Instance.deserializeMessage(message)).payload.getKeyspaceName()).call();
if (keyspace.equals(KEYSPACE))
return true;
}
if (message.verb() == Verb.ACCORD_PRE_ACCEPT_REQ.id && !migrateAwayFromAccord)
{
boolean drop = cluster.get(to).callsOnInstance(() -> {
PreAccept preAccept = (PreAccept)Instance.deserializeMessage(message).payload;
PartialRoute<?> route = preAccept.scope;
if (route.domain() == Domain.Key)
for (RoutingKey key : (PartialKeyRoute)route)
{
AccordRoutingKey routingKey = (AccordRoutingKey)key;
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(routingKey.table());
if (cfs.getKeyspaceName().equals(KEYSPACE))
return true;
}
return false;
}).call();
if (drop)
return true;
}
return false;
}).drop();
forEach(() -> BatchlogManager.instance.pauseReplay());
// If testing batch log delivery the coordinator needs to be a node other than the node that is behind on
// topology updates so that the batch log writes (and thus replay) can be done on the node that is out of sync
int coordinatorIndex = scenario.initiallyEnableBatchlogReplay ? 2 : 3;
IInvokableInstance instance = cluster.get(coordinatorIndex);
ICoordinator coordinator = instance.coordinator();
int startRetryCount = getRetryOnDifferentSystemCount(coordinatorIndex);
// If testing routing at mutation coordination then Node 1 and 2 will both rejected the mutation because it is in a migrating range
int startRejectedCount = getMutationsRejectedOnWrongSystemCount();
logger.info("Executing batch insert");
Future<SimpleQueryResult> resultFuture = coordinator.asyncExecuteWithResult(batchCQL, ALL);
// Testing either batch log delivery or hint delivery via batchlog
if (scenario.initiallyBlockTestKeyspaceMutations)
{
// Expect initial write failure
expectException(() -> {
try
{
return resultFuture.get();
}
catch (ExecutionException e)
{
throw (Exception) e.getCause();
}
}, WriteTimeoutException.class);
}
if (scenario.passesThroughBatchlog)
{
// At this stage we want the batch log to fail because it misrouted the queries to the wrong system
// not because it timed out not getting a response. We only did that with mutations as a quick
// way to populate the batch log. Could almost as easily have constructed the mutation and put it
// in the batch log directly
if (scenario == Scenario.BATCHLOG_FAILED_ROUTING_THEN_HINT || scenario == BATCHLOG_SUCCESSFUL_ROUTING)
cluster.filters().reset();
// We only want the batch log to have access to the correct topology if we are testing its
// ability to handle misrouted things
if (scenario == Scenario.BATCHLOG_SUCCESSFUL_ROUTING)
unpauseEnactment(outOfSyncInstance);
// Unfortunately the batch won't be replayed until some time has passed because the starting time
// for replay is the current time - timeout
// Don't wait here for the batchlog if we need to spin on the creation of the Accord transaction
// and then unpause to test Accord routing failure
boolean unpauseAfterBatchLogCreatesTransaction = migrateAwayFromAccord && scenario == BATCHLOG_FAILED_ROUTING_THEN_HINT;
if (!unpauseAfterBatchLogCreatesTransaction)
Thread.sleep(BatchlogManager.BATCHLOG_REPLAY_TIMEOUT + DatabaseDescriptor.getWriteRpcTimeout(TimeUnit.MILLISECONDS));
messageSink.reset();
// Force batch log delivery (or hint delivery) on the node that was out of sync, but should be in sync once we unpause
// This demonstrates it can split the mutation correctly or forward it to hinting if it fails
outOfSyncInstance.runOnInstance(() -> runUnchecked(() -> {
// We don't want hints for any reason that might apply the mutation and make the test look like it succeeded
assertTrue(HintsService.instance.isDispatchPaused());
// The failed write will have written hints
HintsService.instance.deleteAllHintsUnsafe();
assertFalse(hasPendingHints());
BatchlogManager.instance.resumeReplay();
// Unpausing needs to be done async because it waits for the batch log replay
Promise<Void> unpaused = new AsyncPromise<>();
if (unpauseAfterBatchLogCreatesTransaction)
{
logger.info("Creating thread to unpause after batchlog creates Accord transaction");
new Thread(() ->
{
try
{
// Unpause so it can route incorrectly instead of timing out waiting to fetch the epoch, need the transaction to be created first
// otherwise it will just be routed straight to non-Accord.
logger.info("Spinning waiting on a transaction");
Util.spinUntilTrue(() -> !((AccordService)AccordService.instance()).node().coordinating().isEmpty(), 20);
logger.info("Foudn transaction, unpausing");
TestChangeListener.instance.unpause();
unpaused.trySuccess(null);
}
catch (Throwable t)
{
unpaused.tryFailure(t);
}
}).start();
}
else
{
// Force replay so mosts tests don't have to wait
BatchlogManager.instance.forceBatchlogReplay();
unpaused.trySuccess(null);
}
// Fetch errors
unpaused.get();
// Ensure the batch log did or didn't create pending hints depending on the test scenario
spinAssertEquals(scenario == BATCHLOG_FAILED_TIMEOUT_THEN_HINT || scenario == BATCHLOG_FAILED_ROUTING_THEN_HINT, () -> hasPendingHints(), 20);
}));
}
// Mutation successfully applied from the coordinator after retrying scenario
if (scenario == MUTATION)
{
// Don't want to mistakenly have hints applying the mutation
forEach(() -> assertTrue(HintsService.instance.isDispatchPaused()));
// Check for the error differently depending on what system should be seeing an error
if (migrateAwayFromAccord)
{
// Accord will block until we unpause enactment so to test the routing we wait until the transaction
// has started so the epoch it is created in is the old one
Util.spinUntilTrue(() -> outOfSyncInstance.callOnInstance(() -> !((AccordService)AccordService.instance()).node().coordinating().isEmpty()), 20);
logger.info("Accord node is now coordinating something");
try
{
validation.accept(cluster);
throw new AssertionError("Expected validation to fail");
}
catch (AssertionError e)
{
//ignored
}
}
else
{
Stopwatch sw = Stopwatch.createStarted();
spinAssertEquals(startRejectedCount + 2, 10, () -> getMutationsRejectedOnWrongSystemCount() - startRejectedCount);
logger.info("Took {}ms to get mutations rejected on wrong system", sw.elapsed(TimeUnit.MILLISECONDS));
}
logger.info("Unpausing out of sync instance");
// Testing regular mutation coordination retry loop let coordinator get up to date and retry
unpauseEnactment(outOfSyncInstance);
try
{
resultFuture.get();
}
catch (ExecutionException e)
{
// This is expected when inverting the migration
if (migrateAwayFromAccord && e.getCause() instanceof CoordinatorBehindException)
throw e;
throw e;
}
if (!migrateAwayFromAccord)
{
int endRetryCount = getRetryOnDifferentSystemCount(coordinatorIndex);
int endRejectedCount = getMutationsRejectedOnWrongSystemCount();
assertEquals(1, endRetryCount - startRetryCount);
// Expect only two nodes to reject since they enacted the new epoch
assertEquals(2, endRejectedCount - startRejectedCount);
}
}
// Anything related to making sure hints are delivered goes here
if (scenario.deliversViaHint)
{
// Don't want to mistakenly have hints applying the mutation before we enable it on just one instance
forEach(() -> assertTrue(HintsService.instance.isDispatchPaused()));
// The filters wouldn't have been reset yet if they were needed to make the batchlog or original mutation time out
// Need to reset so Hints can use Accord txns
cluster.filters().reset();
long startingAccordTimeouts = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.timeouts.getCount());
long startingAccordPreempted = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.preempted.getCount());
long startingAccordMigrationRejects = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.accordMigrationRejects.getCount());
long startingHintTimeouts = outOfSyncInstance.callOnInstance(() -> HintsServiceMetrics.hintsTimedOut.getCount());
outOfSyncInstance.runOnInstance(() -> HintsService.instance.resumeDispatch());
// The initial hinting attempt should fail, unless it's a batchlog routing failure in which
// case the coordinator has already caught up so the hint will succeed on the first try
// Can only really have this case for BATCHLOG_FAILED_TIMEOUT_THEN_HINT becuase Accord timeouts don't
// write hints so there is nothing to test
if (migrateAwayFromAccord && scenario == BATCHLOG_FAILED_TIMEOUT_THEN_HINT)
{
Callable<Boolean> test = () -> outOfSyncInstance.callOnInstance(() -> {
logger.info("startingAccordTimeouts {}, startingAccordPreempts {}, startingAccordMigrationRejects {}, startingHintTimeouts {}, accord timeouts {}, accordPreempts {}, accordMigrationRejects {}, hint timeouts {}", startingAccordTimeouts, startingAccordPreempted, startingAccordMigrationRejects, startingHintTimeouts, ClientRequestsMetricsHolder.accordWriteMetrics.timeouts.getCount(), ClientRequestsMetricsHolder.accordWriteMetrics.preempted.getCount(), ClientRequestsMetricsHolder.accordWriteMetrics.accordMigrationRejects.getCount(), HintsServiceMetrics.hintsTimedOut.getCount());
AccordClientRequestMetrics accordMetrics = ClientRequestsMetricsHolder.accordWriteMetrics;
return accordMetrics.timeouts.getCount() >= (startingAccordTimeouts + 1) && HintsServiceMetrics.hintsTimedOut.getCount() >= (startingHintTimeouts + 1);
});
Util.spinUntilTrue(test, 40);
}
else if (!migrateAwayFromAccord)
{
// Expect two retry on different system responses when migrating from Paxos to Accord, one from each
// node that knows it is on the wrong system
Util.spinUntilTrue(() -> messageSink.messages.stream().filter(p -> {
if (p.right.verb() != Verb.FAILURE_RSP.id)
return false;
if (!p.left.equals(outOfSyncInstance.broadcastAddress()))
return false;
RequestFailureReason reason = ((RequestFailure) Instance.deserializeMessage(p.right).payload).reason;
if (reason == RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM)
return true;
return false;
}).count() == 2, 20);
}
// After this hints should deliver and the final validation should succeed
// if we don't unpause enactment
unpauseEnactment(outOfSyncInstance);
}
// Accord commit is async and might take a while, but the data should end up as expected
Util.spinUntilSuccess(() -> validation.accept(cluster));
});
}
/*
* Set up 3 to be behind and unaware of the migration while 1 and 2 are aware
*/
private IInvokableInstance setUpOutOfSyncNode(Cluster cluster) throws Throwable
{
IInvokableInstance i1 = cluster.get(1);
IInvokableInstance i2 = cluster.get(2);
IInvokableInstance i3 = cluster.get(3);
alterTableTransactionalMode(TransactionalMode.full);
Epoch nextEpoch = getNextEpoch(i1);
// Node 3 will coordinate the query and not be aware that the migration has begun
Callable<?> pausedBeforeEnacting = pauseBeforeEnacting(i3, nextEpoch);
// In batch log delivery cases i2 will be the coordinator and we need to be sure that it has enacted the latest epoch
Callable<?> i2PausedAfterEnacting = pauseAfterEnacting(i2, nextEpoch);
ListenableFuture<?> result = nodetoolAsync(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), "-tp", "accord", KEYSPACE, accordTableName);
if (migrateAwayFromAccord)
{
pausedBeforeEnacting.call();
i2PausedAfterEnacting.call();
unpauseEnactment(i2);
unpauseEnactment(i3);
result.get();
long migratingEpoch = nextEpoch.getEpoch();
Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(migratingEpoch)))), 10);
nextEpoch = getNextEpoch(i1);
pausedBeforeEnacting = pauseBeforeEnacting(i3, nextEpoch);
i2PausedAfterEnacting = pauseAfterEnacting(i2, nextEpoch);
// In the reverse direction doing the alter automatically reverses the migratin without a need to call begin migration on any ranges
result = alterTableTransactionalModeAsync(TransactionalMode.off);
}
// Wait for everyone to get to where they are supposed to be
try
{
pausedBeforeEnacting.call();
}
catch (Throwable t)
{
if (result.isDone())
{
try
{
result.get();
}
catch (ExecutionException e)
{
t.addSuppressed(e);
throw t;
}
}
throw t;
}
i2PausedAfterEnacting.call();
// Unpause on 1 and 2 where we want them aware of the migration
unpauseEnactment(i1);
unpauseEnactment(i2);
// nodetool should be able to complete now
result.get();
return i3;
}
private String twoTableBatchInsert(boolean logged, int pkey1, int pkey2, int value)
{
return batch(logged,
insertCQL(qualifiedAccordTableName, pkey1, value),
insertCQL(qualifiedRegularTableName, pkey2, value));
}
private String singleTableBatchInsert(boolean logged, int pkey1, int pkey2, int value)
{
return batch(logged,
insertCQL(qualifiedAccordTableName, pkey1, value),
insertCQL(qualifiedAccordTableName, pkey2, value));
}
private static String insertCQL(String qualifiedTableName, int pkey, int value)
{
return format("INSERT INTO %s ( id, c, v ) VALUES ( %d, %d, %d )", qualifiedTableName, pkey, CLUSTERING_VALUE, value);
}
// Prevents the creation of transactions in an older epoch because later writes need to order after earlier
private void writeAccordRowViaAccord()
{
logger.info("Initiating Accord row write");
SHARED_CLUSTER.coordinator(1).execute(insertCQL(qualifiedAccordTableName, PKEY_ACCORD, 99), ConsistencyLevel.QUORUM);
logger.info("Finished Accord row write");
}
}

View File

@ -21,7 +21,6 @@ package org.apache.cassandra.distributed.test.accord;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@ -52,14 +51,16 @@ import org.apache.cassandra.db.BufferDecoratedKey;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.Mutation.SimpleBuilder;
import org.apache.cassandra.db.SimpleBuilders.PartitionUpdateBuilder;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.NodeToolResult;
import org.apache.cassandra.distributed.api.Row;
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.gms.EndpointState;
@ -97,8 +98,8 @@ import org.yaml.snakeyaml.Yaml;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static org.apache.cassandra.Util.dk;
import static org.apache.cassandra.Util.spinUntilSuccess;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
import static org.apache.cassandra.db.SystemKeyspace.CONSENSUS_MIGRATION_STATE;
import static org.apache.cassandra.db.SystemKeyspace.PAXOS;
import static org.apache.cassandra.dht.Range.normalize;
@ -160,7 +161,7 @@ public class AccordMigrationTest extends AccordTestBase
ServerTestUtils.daemonInitialization();
// Otherwise repair complains if you don't specify a keyspace
CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.setInt(3);
AccordTestBase.setupCluster(builder -> builder.appendConfig(config -> config.set("paxos_variant", PaxosVariant.v2.name())
AccordTestBase.setupCluster(builder -> builder.appendConfig(config -> config.with(Feature.NETWORK).set("paxos_variant", PaxosVariant.v2.name())
.set("accord.range_migration", "explicit")), 3);
partitioner = FBUtilities.newPartitioner(SHARED_CLUSTER.get(1).callsOnInstance(() -> DatabaseDescriptor.getPartitioner().getClass().getSimpleName()).call());
StorageService.instance.setPartitionerUnsafe(partitioner);
@ -192,19 +193,6 @@ public class AccordMigrationTest extends AccordTestBase
SHARED_CLUSTER.coordinators().forEach(coordinator -> coordinator.execute(format("TRUNCATE TABLE %s.%s", SYSTEM_KEYSPACE_NAME, PAXOS), ALL));
}
private static String nodetool(ICoordinator coordinator, String... commandAndArgs)
{
NodeToolResult nodetoolResult = coordinator.instance().nodetoolResult(commandAndArgs);
if (!nodetoolResult.getStdout().isEmpty())
System.out.println(nodetoolResult.getStdout());
if (!nodetoolResult.getStderr().isEmpty())
System.err.println(nodetoolResult.getStderr());
if (nodetoolResult.getError() != null)
fail("Failed nodetool " + Arrays.asList(commandAndArgs), nodetoolResult.getError());
// TODO why does standard out end up in stderr in nodetool?
return nodetoolResult.getStdout();
}
private static int getKeyBetweenTokens(Token left, Token right)
{
return getKeysBetweenTokens(left, right).next();
@ -398,10 +386,10 @@ public class AccordMigrationTest extends AccordTestBase
@Test
public void testPaxosToAccordCAS() throws Exception
{
test(format(TABLE_FMT, qualifiedTableName),
test(format(TABLE_FMT, qualifiedAccordTableName),
cluster -> {
List<Pair<ByteBuffer, UUID>> expectedKeyMigrations = new ArrayList<>();
String table = tableName;
String table = accordTableName;
UUID tableUUID = cluster.get(1).callOnInstance(() -> ColumnFamilyStore.getIfExists(KEYSPACE, table).getTableId().asUUID());
cluster.forEach(node -> node.runOnInstance(() -> {
TableMetadata tbl = Schema.instance.getTableMetadata(KEYSPACE, table);
@ -409,7 +397,7 @@ public class AccordMigrationTest extends AccordTestBase
Assert.assertEquals(TransactionalMigrationFromMode.none, tbl.params.transactionalMigrationFrom);
}));
cluster.schemaChange(format("ALTER TABLE %s.%s WITH transactional_mode='%s'", KEYSPACE, tableName, TransactionalMode.full));
cluster.schemaChange(format("ALTER TABLE %s.%s WITH transactional_mode='%s'", KEYSPACE, accordTableName, TransactionalMode.full));
cluster.forEach(node -> node.runOnInstance(() -> {
TableMetadata tbl = Schema.instance.getTableMetadata(KEYSPACE, table);
@ -417,11 +405,11 @@ public class AccordMigrationTest extends AccordTestBase
Assert.assertEquals(TransactionalMigrationFromMode.off, tbl.params.transactionalMigrationFrom);
}));
String casCQL = format(CAS_FMT, qualifiedTableName, CLUSTERING_VALUE);
String casCQL = format(CAS_FMT, qualifiedAccordTableName, CLUSTERING_VALUE);
Consumer<Integer> runCasNoApply = key -> assertRowEquals(cluster, new Object[]{false}, casCQL, key);
Consumer<Integer> runCasApplies = key -> assertRowEquals(cluster, new Object[]{true}, casCQL, key);
Consumer<Integer> runCasOnSecondNode = key -> assertEquals( "[applied]", cluster.coordinator(2).executeWithResult(casCQL, ANY, key).names().get(0));
String tableName = qualifiedTableName.split("\\.")[1];
String tableName = qualifiedAccordTableName.split("\\.")[1];
int migratingKey = getKeyBetweenTokens(midToken, maxToken);
int notMigratingKey = getKeyBetweenTokens(minToken, midToken);
Range<Token> migratingRange = new Range(midToken, maxToken);
@ -493,8 +481,15 @@ public class AccordMigrationTest extends AccordTestBase
// Update inserted row so the condition can apply, if the condition check doesn't apply
// then it won't get to propose/accept
migratingKey = testingKeys.next();
String query = "UPDATE " + qualifiedTableName + " SET v = 42 WHERE id = ? AND c = ?";
Consumer<Integer> makeCASApply = key -> cluster.forEach(instance -> instance.runOnInstance(() -> executeInternal(query, key, CLUSTERING_VALUE)));
String keyspace = KEYSPACE;
Integer clusteringValue = CLUSTERING_VALUE;
String mutationTableName = accordTableName;
Consumer<Integer> makeCASApply = key -> cluster.forEach(instance -> instance.runOnInstance(() -> {
SimpleBuilder mutationBuilder = Mutation.simpleBuilder(keyspace, dk(key)).allowPotentialTransactionConflicts();
mutationBuilder.update(mutationTableName).row(clusteringValue).add("v", 42);
Mutation m = mutationBuilder.build();
m.applyUnsafe();
}));
makeCASApply.accept(migratingKey);
// This will force the request to run on Paxos up to Accept
@ -552,13 +547,13 @@ public class AccordMigrationTest extends AccordTestBase
@Test
public void testPaxosToAccordSerialRead() throws Exception
{
test(format(TABLE_FMT, qualifiedTableName),
test(format(TABLE_FMT, qualifiedAccordTableName),
cluster -> {
String table = tableName;
String table = accordTableName;
UUID tableUUID = cluster.get(1).callOnInstance(() -> ColumnFamilyStore.getIfExists(KEYSPACE, table).getTableId().asUUID());
List<Pair<ByteBuffer, UUID>> expectedKeyMigrations = new ArrayList<>();
cluster.schemaChange(format("ALTER TABLE %s.%s WITH transactional_mode='%s'", KEYSPACE, tableName, TransactionalMode.full));
String readCQL = format("SELECT * FROM %s WHERE id = ? and c = %s", qualifiedTableName, CLUSTERING_VALUE);
cluster.schemaChange(format("ALTER TABLE %s.%s WITH transactional_mode='%s'", KEYSPACE, accordTableName, TransactionalMode.full));
String readCQL = format("SELECT * FROM %s WHERE id = ? and c = %s", qualifiedAccordTableName, CLUSTERING_VALUE);
Function<Integer, Object[][]> runRead = key -> cluster.coordinator(1).execute(readCQL, SERIAL, key);
Range<Token> migratingRange = new Range<>(new LongToken(Long.MIN_VALUE + 1), new LongToken(Long.MIN_VALUE));
List<Range<Token>> migratingRanges = ImmutableList.of(migratingRange);
@ -566,15 +561,15 @@ public class AccordMigrationTest extends AccordTestBase
assertTargetAccordRead(runRead, 1, key, expectedKeyMigrations, 0, 1, 0, 0, 0);
// Mark wrap around range as migrating
nodetool(coordinator, "consensus_admin", "begin-migration", "-st", String.valueOf(Long.MIN_VALUE + 1), "-et", String.valueOf(Long.MIN_VALUE), "-tp", "accord", KEYSPACE, tableName);
assertMigrationState(tableName, ConsensusMigrationTarget.accord, emptyList(), migratingRanges, 1);
nodetool(coordinator, "consensus_admin", "begin-migration", "-st", String.valueOf(Long.MIN_VALUE + 1), "-et", String.valueOf(Long.MIN_VALUE), "-tp", "accord", KEYSPACE, accordTableName);
assertMigrationState(accordTableName, ConsensusMigrationTarget.accord, emptyList(), migratingRanges, 1);
// Should run directly on accord, migrate the key, and perform a quorum read from Accord, Paxos repair will run prepare once
addExpectedMigratedKey(expectedKeyMigrations, key, tableUUID);
assertTargetAccordRead(runRead, 1, key, expectedKeyMigrations, 1, 1, 1, 0, 0);
key++;
// Should run up to accept with both nodes refusing to accept
savePromisedAndCommittedPaxosProposal(tableName, key);
savePromisedAndCommittedPaxosProposal(accordTableName, key);
cluster.get(1).runOnInstance(() -> ConsensusRequestRouter.setInstance(new PaxosToAccordMigrationNotHappeningUpToBegin()));
addExpectedMigratedKey(expectedKeyMigrations, key, tableUUID);
assertTargetAccordRead(runRead, 1, key, expectedKeyMigrations, 1, 2, 1, 0, 1);
@ -582,11 +577,6 @@ public class AccordMigrationTest extends AccordTestBase
});
}
private void alterTableTransactionalMode(TransactionalMode mode)
{
SHARED_CLUSTER.schemaChange(format("ALTER TABLE %s WITH %s", qualifiedTableName, mode.asCqlParam()));
}
private void assertTransactionalModes(String keyspace, String table, TransactionalMode mode, TransactionalMigrationFromMode migration)
{
forEach(() -> {
@ -598,17 +588,17 @@ public class AccordMigrationTest extends AccordTestBase
private void assertTransactionalModes(TransactionalMode mode, TransactionalMigrationFromMode migration)
{
assertTransactionalModes(KEYSPACE, tableName, mode, migration);
assertTransactionalModes(KEYSPACE, accordTableName, mode, migration);
}
@Test
public void testAccordToPaxos() throws Exception
{
test(format(TABLE_FMT, qualifiedTableName),
test(format(TABLE_FMT, qualifiedAccordTableName),
cluster -> {
String casCQL = format(CAS_FMT, qualifiedTableName, CLUSTERING_VALUE);
String casCQL = format(CAS_FMT, qualifiedAccordTableName, CLUSTERING_VALUE);
Consumer<Integer> runCasNoApply = key -> assertRowEquals(cluster, new Object[]{false}, casCQL, key);
String tableName = qualifiedTableName.split("\\.")[1];
String tableName = qualifiedAccordTableName.split("\\.")[1];
UUID tableUUID = cluster.get(1).callOnInstance(() -> ColumnFamilyStore.getIfExists(KEYSPACE, tableName).getTableId().asUUID());
alterTableTransactionalMode(TransactionalMode.mixed_reads);

View File

@ -24,17 +24,17 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import accord.coordinate.Invalidated;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
@ -42,15 +42,25 @@ import org.junit.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.RoutingKey;
import accord.coordinate.Invalidated;
import accord.impl.SimpleProgressLog;
import accord.messages.PreAccept;
import accord.primitives.PartialKeyRoute;
import accord.primitives.PartialRoute;
import accord.primitives.Routable.Domain;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import org.apache.cassandra.batchlog.BatchlogManager;
import org.apache.cassandra.cql3.statements.ModificationStatement;
import org.apache.cassandra.cql3.statements.TransactionStatement;
import org.apache.cassandra.cql3.transactions.ReferenceValue;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.SystemKeyspace;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.Cluster.Builder;
@ -60,24 +70,37 @@ import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableRunnable;
import org.apache.cassandra.distributed.api.QueryResults;
import org.apache.cassandra.distributed.api.SimpleQueryResult;
import org.apache.cassandra.distributed.impl.Instance;
import org.apache.cassandra.distributed.shared.AssertUtils;
import org.apache.cassandra.distributed.shared.Metrics;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.distributed.util.QueryResultUtil;
import org.apache.cassandra.hints.HintsService;
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.net.Message;
import org.apache.cassandra.net.Verb;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.AccordTestUtils;
import org.apache.cassandra.service.accord.api.AccordRoutingKey;
import org.apache.cassandra.service.accord.exceptions.ReadPreemptedException;
import org.apache.cassandra.service.accord.exceptions.WritePreemptedException;
import org.apache.cassandra.service.consensus.TransactionalMode;
import org.apache.cassandra.service.consensus.migration.ConsensusMigrationState;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.serialization.Version;
import org.apache.cassandra.utils.AssertionUtils;
import org.apache.cassandra.utils.FailingConsumer;
import static java.lang.String.format;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static org.apache.cassandra.db.SystemKeyspace.CONSENSUS_MIGRATION_STATE;
import static org.apache.cassandra.db.SystemKeyspace.PAXOS;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
import static org.apache.cassandra.schema.SchemaConstants.SYSTEM_KEYSPACE_NAME;
import static org.junit.Assert.assertArrayEquals;
public abstract class AccordTestBase extends TestBaseImpl
@ -89,8 +112,10 @@ public abstract class AccordTestBase extends TestBaseImpl
protected static Cluster SHARED_CLUSTER;
protected String tableName;
protected String qualifiedTableName;
protected String accordTableName;
protected String qualifiedAccordTableName;
protected String regularTableName;
protected String qualifiedRegularTableName;
public static void setupCluster(Function<Builder, Builder> options, int nodes) throws IOException
{
@ -107,8 +132,10 @@ public abstract class AccordTestBase extends TestBaseImpl
@Before
public void setup()
{
tableName = "tbl" + COUNTER.getAndIncrement();
qualifiedTableName = KEYSPACE + '.' + tableName;
accordTableName = "accordtbl" + COUNTER.getAndIncrement();
qualifiedAccordTableName = KEYSPACE + '.' + accordTableName;
regularTableName = "regulartbl" + COUNTER.getAndIncrement();
qualifiedRegularTableName = KEYSPACE + '.' + regularTableName;
}
@After
@ -135,6 +162,14 @@ public abstract class AccordTestBase extends TestBaseImpl
test(Collections.singletonList(tableDDL), fn);
}
protected List<String> createTables(String tableFormat, String... qualifiedTables)
{
ImmutableList.Builder<String> builder = ImmutableList.builder();
for (String qualifiedTable : qualifiedTables)
builder.add(format(tableFormat, qualifiedTable));
return builder.build();
}
public static void ensureTableIsAccordManaged(Cluster cluster, String ksname, String tableName)
{
cluster.get(1).runOnInstance(() -> {
@ -165,7 +200,7 @@ public abstract class AccordTestBase extends TestBaseImpl
protected void test(FailingConsumer<Cluster> fn) throws Exception
{
test("CREATE TABLE " + qualifiedTableName + " (k int, c int, v int, primary key (k, c)) WITH transactional_mode='full'", fn);
test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, primary key (k, c)) WITH transactional_mode='full'", fn);
}
protected static ConsensusMigrationState getMigrationStateSnapshot(IInvokableInstance instance) throws IOException
@ -198,6 +233,19 @@ public abstract class AccordTestBase extends TestBaseImpl
return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.Latency.CASWrite"));
}
protected static int getRetryOnDifferentSystemCount(int coordinatorIndex)
{
return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.RetryDifferentSystem.Write"));
}
protected int getMutationsRejectedOnWrongSystemCount()
{
long sum = 0;
for (IInvokableInstance instance : SHARED_CLUSTER)
sum += instance.metrics().getCounter("org.apache.cassandra.metrics.Table.MutationsRejectedOnWrongSystem." + qualifiedAccordTableName);
return Ints.checkedCast(sum);
}
protected static int getCasPrepareCount(int coordinatorIndex)
{
return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.keyspace.CasPrepareLatency.distributed_test_keyspace"));
@ -279,7 +327,7 @@ public abstract class AccordTestBase extends TestBaseImpl
// disable vnode for now, but should enable before trunk
Cluster.Builder builder = Cluster.build(nodes)
.withoutVNodes()
.withConfig(c -> c.with(Feature.NETWORK, Feature.GOSSIP).set("write_request_timeout", "10s")
.withConfig(c -> c.with(Feature.GOSSIP).set("write_request_timeout", "10s")
.set("transaction_timeout", "15s")
.set("transaction_timeout", "15s"))
.withInstanceInitializer(EnforceUpdateDoesNotPerformRead::install);
@ -464,4 +512,86 @@ public abstract class AccordTestBase extends TestBaseImpl
}
protected abstract Logger logger();
protected void alterTableTransactionalMode(TransactionalMode mode)
{
SHARED_CLUSTER.schemaChange(format("ALTER TABLE %s WITH %s", qualifiedAccordTableName, mode.asCqlParam()));
}
protected static void pauseHints()
{
forEach(() -> HintsService.instance.pauseDispatch());
}
protected static void deleteAllHints()
{
forEach(() -> HintsService.instance.deleteAllHintsUnsafe());
}
protected static void pauseBatchlog()
{
forEach(() -> BatchlogManager.instance.pauseReplay());
}
protected static void unpauseHints()
{
forEach(() -> HintsService.instance.resumeDispatch());
}
protected static void unpauseBatchlog()
{
forEach(() -> BatchlogManager.instance.resumeReplay());
}
protected static void blockMutationAndPreAccept(Cluster cluster)
{
cluster.filters().outbound().messagesMatching((from, to, message) -> {
if (message.verb() == Verb.MUTATION_REQ.id)
{
String keyspace = cluster.get(to).callsOnInstance(() -> ((Message<Mutation>) Instance.deserializeMessage(message)).payload.getKeyspaceName()).call();
if (keyspace.equals(KEYSPACE))
return true;
}
if (message.verb() == Verb.ACCORD_PRE_ACCEPT_REQ.id)
{
boolean drop = cluster.get(to).callsOnInstance(() -> {
PreAccept preAccept = (PreAccept)Instance.deserializeMessage(message).payload;
PartialRoute<?> route = preAccept.scope;
if (route.domain() == Domain.Key)
for (RoutingKey key : (PartialKeyRoute)route)
{
AccordRoutingKey routingKey = (AccordRoutingKey)key;
ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(routingKey.table());
if (cfs.getKeyspaceName().equals(KEYSPACE))
return true;
}
return false;
}).call();
if (drop)
return true;
}
return false;
}).drop();
}
protected static void truncateSystemTables()
{
SHARED_CLUSTER.coordinator(1).execute("TRUNCATE " + SYSTEM_KEYSPACE_NAME + "." + SystemKeyspace.BATCHES, ALL);
SHARED_CLUSTER.coordinator(1).execute(format("TRUNCATE TABLE %s.%s", SYSTEM_KEYSPACE_NAME, CONSENSUS_MIGRATION_STATE), ALL);
SHARED_CLUSTER.coordinator(1).execute(format("TRUNCATE TABLE %s.%s", SYSTEM_KEYSPACE_NAME, PAXOS), ALL);
}
protected static Stream<UUID> hostIds()
{
return Stream.concat(ClusterMetadata.current().directory.peerIds()
.stream()
.map(ClusterMetadata.current().directory::hostId),
Stream.of(HintsService.RETRY_ON_DIFFERENT_SYSTEM_UUID));
}
protected static boolean hasPendingHints()
{
return hostIds().map(HintsService.instance::getTotalHintsSize)
.anyMatch(size -> size > 0);
}
}

View File

@ -0,0 +1,209 @@
/*
* 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.accord;
import java.io.IOException;
import java.nio.ByteBuffer;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.SimpleBuilders.MutationBuilder;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.distributed.api.ICoordinator;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.exceptions.WriteTimeoutException;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaPlan;
import org.apache.cassandra.locator.ReplicaPlans;
import org.apache.cassandra.service.StorageProxy;
import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState;
import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter;
import static java.lang.String.format;
import static org.apache.cassandra.Util.spinAssertEquals;
import static org.apache.cassandra.config.CassandraRelevantProperties.HINT_DISPATCH_INTERVAL_MS;
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/*
* Test that non-transactional updates have their timestamps preserved when written through Accord so that
* `USING TIMESTAMP` continues to work and so that hints and batch log retry attempts are inserted with their
* original timestamp and not a later Accord timestamp which could cause data resurrection.
*/
public class AccordTimestampPreservationTest extends AccordTestBase
{
private static final Logger logger = LoggerFactory.getLogger(AccordTimestampPreservationTest.class);
private static final int CLUSTERING_VALUE = 1;
private static final String NORMAL_TABLE_FMT = "CREATE TABLE %s (id int, c int, v int, PRIMARY KEY ((id), c))";
private static final String ACCORD_TABLE_FMT = NORMAL_TABLE_FMT + " WITH transactional_mode='full'";
private static ICoordinator coordinator;
private static final String expectedResult = "[[42]]";
private static final int PKEY1 = 77;
private static final int PKEY2 = 78;
private static final int VALUE = 66;
private static final long TIMESTAMP = 42;
@Override
protected Logger logger()
{
return logger;
}
@BeforeClass
public static void setupClass() throws IOException
{
HINT_DISPATCH_INTERVAL_MS.setLong(100);
ServerTestUtils.daemonInitialization();
// Otherwise repair complains if you don't specify a keyspace
CassandraRelevantProperties.SYSTEM_TRACES_DEFAULT_RF.setInt(3);
AccordTestBase.setupCluster(builder -> builder.appendConfig(config -> config.set("write_request_timeout", "2s")), 3);
ServerTestUtils.prepareServerNoRegister();
coordinator = SHARED_CLUSTER.coordinator(1);
}
@After
public void tearDown() throws Exception
{
unpauseBatchlog();
deleteAllHints();
unpauseHints();
super.tearDown();
// Reset migration state
forEach(() -> {
ConsensusRequestRouter.resetInstance();
ConsensusKeyMigrationState.reset();
});
truncateSystemTables();
}
@Test
public void testMutationPreservesTimestamp() throws Exception
{
test(createTables(ACCORD_TABLE_FMT, qualifiedAccordTableName), cluster -> {
long startCount = getAccordCoordinateCount();
coordinator.executeWithResult(insertCQL(qualifiedAccordTableName, PKEY1, VALUE), ALL);
assertEquals(startCount + 1, getAccordWriteCount());
int id = 1;
for (IInvokableInstance instance : cluster)
{
logger.info("Checking instance " + id);
id++;
spinAssertEquals(expectedResult, () -> instance.executeInternalWithResult(checkCQL()).toString(), 20);
}
});
}
@Test
public void testBatchlogPreservesTimestamp() throws Exception
{
test(ImmutableList.of(format(NORMAL_TABLE_FMT, qualifiedRegularTableName), format(ACCORD_TABLE_FMT, qualifiedAccordTableName)), cluster -> {
pauseHints();
blockMutationAndPreAccept(cluster);
try
{
// Insert must span both Accord and non-Accord ranges or tables otherwise it bypasses the batchlog entirely
coordinator.executeWithResult(batchInsert(true, PKEY1, PKEY2, VALUE), ALL);
fail("Should have thrown WTE");
}
catch (Throwable t)
{
assertEquals(t.getClass().getName(), WriteTimeoutException.class.getName());
}
cluster.filters().reset();
int id = 1;
for (IInvokableInstance instance : cluster)
{
logger.info("Checking instance " + id);
id++;
spinAssertEquals(expectedResult, () -> instance.executeInternalWithResult(checkCQL()).toString(), 20);
}
});
}
@Test
public void testHintsPreservesTimestamp() throws Exception
{
test(createTables(ACCORD_TABLE_FMT, qualifiedAccordTableName), cluster -> {
String keyspace = KEYSPACE;
int pkey1 = PKEY1;
long timestamp = TIMESTAMP;
int clustering = CLUSTERING_VALUE;
String tableName = accordTableName;
cluster.get(1).runOnInstance(() -> {
ByteBuffer keyBuf = Int32Type.instance.fromString(Integer.toString(pkey1));
DecoratedKey dk = DatabaseDescriptor.getPartitioner().decorateKey(keyBuf);
MutationBuilder mutationBuilder = new MutationBuilder(KEYSPACE, dk);
mutationBuilder.timestamp(timestamp);
mutationBuilder.update(tableName).row(clustering).add("v", VALUE);
Mutation m = mutationBuilder.build();
ReplicaPlan.ForWrite plan = ReplicaPlans.forWrite(Keyspace.open(keyspace), ConsistencyLevel.ALL, dk.getToken(), ReplicaPlans.writeAll);
for (Replica replica : plan.live().withoutSelf())
StorageProxy.submitHint(m, replica, null);
});
for (int i = 2; i <= 3; i++)
{
int instance = i;
spinAssertEquals(expectedResult, () -> cluster.get(instance).executeInternalWithResult(checkCQL()).toString(), 20);
}
});
}
private String batchInsert(boolean logged, int pkey1, int pkey2, int value)
{
return batch(logged,
insertCQL(qualifiedAccordTableName, pkey1, value),
insertCQL(qualifiedRegularTableName, pkey2, value));
}
private String insertCQL(String table, int pkey, int value)
{
return insertCQL(table, pkey, value, false);
}
private String insertCQL(String table, int pkey, int value, boolean cas)
{
return format("INSERT INTO %s ( id, c, v ) VALUES ( %d, %d, %d )%s USING TIMESTAMP %d", table, pkey, CLUSTERING_VALUE, value, cas ? " IF NOT EXISTS" : "", TIMESTAMP);
}
private String checkCQL()
{
return format("SELECT WRITETIME(v) from %s WHERE id = %d", qualifiedAccordTableName, PKEY1);
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.accord;
public class MigrationFromAccordRaceTest extends AccordMigrationRaceTestBase
{
protected boolean migratingAwayFromAccord()
{
return true;
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.accord;
public class MigrationToAccordRaceTest extends AccordMigrationRaceTestBase
{
protected boolean migratingAwayFromAccord()
{
return false;
}
}

View File

@ -142,6 +142,7 @@ public class HintsServiceMetricsTest extends TestBaseImpl
assertThat(countHintsSucceeded(node)).isEqualTo(0);
assertThat(countHintsFailed(node)).isEqualTo(0);
assertThat(countHintsTimedOut(node)).isEqualTo(0);
assertThat(countHintsRetryDifferentSystem(node)).isEqualTo(0);
assertThat(countGlobalDelays(node)).isEqualTo(0);
cluster.forEach(target -> assertThat(countEndpointDelays(node, target)).isEqualTo(0));
}
@ -180,6 +181,12 @@ public class HintsServiceMetricsTest extends TestBaseImpl
return node.callOnInstance(() -> HintsServiceMetrics.hintsTimedOut.getCount());
}
@SuppressWarnings("Convert2MethodRef")
private static Long countHintsRetryDifferentSystem(IInvokableInstance node)
{
return node.callOnInstance(() -> HintsServiceMetrics.hintsRetryDifferentSystem.getCount());
}
private static Long countGlobalDelays(IInvokableInstance node)
{
return getHistogramCount(node, "org.apache.cassandra.metrics.HintsService.Hint_delays");

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