mirror of https://github.com/apache/cassandra
Merge commit '080280dc0177da6176dd4ba970e5a35aa7e2a729' into trunk
This commit is contained in:
commit
9a3ca008ba
|
|
@ -15,6 +15,7 @@
|
|||
Merged from 3.11:
|
||||
* SASI's `max_compaction_flush_memory_in_mb` settings over 100GB revert to default of 1GB (CASSANDRA-16071)
|
||||
Merged from 3.0:
|
||||
* Fix serial read/non-applying CAS linearizability (CASSANDRA-12126)
|
||||
* Avoid potential NPE in JVMStabilityInspector (CASSANDRA-16294)
|
||||
* Improved check of num_tokens against the length of initial_token (CASSANDRA-14477)
|
||||
* Fix a race condition on ColumnFamilyStore and TableMetrics (CASSANDRA-16228)
|
||||
|
|
|
|||
8
NEWS.txt
8
NEWS.txt
|
|
@ -283,6 +283,14 @@ Materialized Views
|
|||
|
||||
Upgrading
|
||||
---------
|
||||
- This release fix a correctness issue with SERIAL reads, and LWT writes that do not apply.
|
||||
Unfortunately, this fix has a performance impact on read performance at the SERIAL or
|
||||
LOCAL_SERIAL consistency levels. For heavy users of such SERIAL reads, the performance
|
||||
impact may be noticeable and may also result in an increased of timeouts. For that
|
||||
reason, a opt-in system property has been added to disable the fix:
|
||||
-Dcassandra.unsafe.disable-serial-reads-linearizability=true
|
||||
Use this flag at your own risk as it revert SERIAL reads to the incorrect behavior of
|
||||
previous versions. See CASSANDRA-12126 for details.
|
||||
- SASI's `max_compaction_flush_memory_in_mb` setting was previously getting interpreted in bytes. From 3.11.8
|
||||
it is correctly interpreted in megabytes, but prior to 3.11.10 previous configurations of this setting will
|
||||
lead to nodes OOM during compaction. From 3.11.10 previous configurations will be detected as incorrect,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
package org.apache.cassandra.service;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
|
@ -41,6 +40,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
|
|
@ -202,6 +202,10 @@ public class StorageProxy implements StorageProxyMBean
|
|||
*/
|
||||
private static final int MAX_CONCURRENT_RANGE_REQUESTS = Math.max(1, Integer.getInteger("cassandra.max_concurrent_range_requests", FBUtilities.getAvailableProcessors() * 10));
|
||||
|
||||
private static final String DISABLE_SERIAL_READ_LINEARIZABILITY_KEY = "cassandra.unsafe.disable-serial-reads-linearizability";
|
||||
private static final boolean disableSerialReadLinearizability =
|
||||
Boolean.parseBoolean(System.getProperty(DISABLE_SERIAL_READ_LINEARIZABILITY_KEY, "false"));
|
||||
|
||||
private StorageProxy()
|
||||
{
|
||||
}
|
||||
|
|
@ -245,6 +249,16 @@ public class StorageProxy implements StorageProxyMBean
|
|||
}
|
||||
|
||||
ReadRepairMetrics.init();
|
||||
|
||||
if (disableSerialReadLinearizability)
|
||||
{
|
||||
logger.warn("This node was started with -D{}. SERIAL (and LOCAL_SERIAL) reads coordinated by this node " +
|
||||
"will not offer linearizability (see CASSANDRA-12126 for details on what this mean) with " +
|
||||
"respect to other SERIAL operations. Please note that, with this flag, SERIAL reads will be " +
|
||||
"slower than QUORUM reads, yet offer no more guarantee. This flag should only be used in " +
|
||||
"the restricted case of upgrading from a pre-CASSANDRA-12126 version, and only if you " +
|
||||
"understand the tradeoff.", DISABLE_SERIAL_READ_LINEARIZABILITY_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -300,23 +314,12 @@ public class StorageProxy implements StorageProxyMBean
|
|||
throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException, CasWriteUnknownResultException
|
||||
{
|
||||
final long startTimeForMetrics = System.nanoTime();
|
||||
TableMetadata metadata = Schema.instance.getTableMetadata(keyspaceName, cfName);
|
||||
int contentions = 0;
|
||||
try
|
||||
{
|
||||
consistencyForPaxos.validateForCas();
|
||||
consistencyForCommit.validateForCasCommit(keyspaceName);
|
||||
TableMetadata metadata = Schema.instance.validateTable(keyspaceName, cfName);
|
||||
|
||||
long timeoutNanos = DatabaseDescriptor.getCasContentionTimeout(NANOSECONDS);
|
||||
while (System.nanoTime() - queryStartNanoTime < timeoutNanos)
|
||||
Supplier<Pair<PartitionUpdate, RowIterator>> updateProposer = () ->
|
||||
{
|
||||
// for simplicity, we'll do a single liveness check at the start of each attempt
|
||||
ReplicaPlan.ForPaxosWrite replicaPlan = ReplicaPlans.forPaxos(Keyspace.open(keyspaceName), key, consistencyForPaxos);
|
||||
|
||||
final PaxosBallotAndContention pair = beginAndRepairPaxos(queryStartNanoTime, key, metadata, replicaPlan, consistencyForPaxos, consistencyForCommit, true, state);
|
||||
final UUID ballot = pair.ballot;
|
||||
contentions += pair.contentions;
|
||||
|
||||
// read the current values and check they validate the conditions
|
||||
Tracing.trace("Reading existing values for CAS precondition");
|
||||
SinglePartitionReadCommand readCommand = (SinglePartitionReadCommand) request.readCommand(nowInSeconds);
|
||||
|
|
@ -332,11 +335,10 @@ public class StorageProxy implements StorageProxyMBean
|
|||
{
|
||||
Tracing.trace("CAS precondition does not match current values {}", current);
|
||||
casWriteMetrics.conditionNotMet.inc();
|
||||
return current.rowIterator();
|
||||
return Pair.create(PartitionUpdate.emptyUpdate(metadata, key), current.rowIterator());
|
||||
}
|
||||
|
||||
// finish the paxos round w/ the desired updates
|
||||
// TODO turn null updates into delete?
|
||||
// Create the desired updates
|
||||
PartitionUpdate updates = request.makeUpdates(current);
|
||||
|
||||
long size = updates.dataSize();
|
||||
|
|
@ -352,34 +354,30 @@ public class StorageProxy implements StorageProxyMBean
|
|||
// InvalidRequestException) any which aren't.
|
||||
updates = TriggerExecutor.instance.execute(updates);
|
||||
|
||||
return Pair.create(updates, null);
|
||||
};
|
||||
|
||||
Commit proposal = Commit.newProposal(ballot, updates);
|
||||
Tracing.trace("CAS precondition is met; proposing client-requested updates for {}", ballot);
|
||||
if (proposePaxos(proposal, replicaPlan, true, queryStartNanoTime))
|
||||
{
|
||||
commitPaxos(proposal, consistencyForCommit, true, queryStartNanoTime);
|
||||
Tracing.trace("CAS successful");
|
||||
return null;
|
||||
}
|
||||
return doPaxos(metadata,
|
||||
key,
|
||||
consistencyForPaxos,
|
||||
consistencyForCommit,
|
||||
consistencyForCommit,
|
||||
state,
|
||||
queryStartNanoTime,
|
||||
casWriteMetrics,
|
||||
updateProposer);
|
||||
|
||||
Tracing.trace("Paxos proposal not accepted (pre-empted by a higher ballot)");
|
||||
contentions++;
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS);
|
||||
// continue to retry
|
||||
}
|
||||
|
||||
throw new WriteTimeoutException(WriteType.CAS, consistencyForPaxos, 0, consistencyForPaxos.blockFor(Keyspace.open(keyspaceName)));
|
||||
}
|
||||
catch (CasWriteUnknownResultException e)
|
||||
{
|
||||
casWriteMetrics.unknownResult.mark();
|
||||
throw e;
|
||||
}
|
||||
catch (WriteTimeoutException wte)
|
||||
catch (CasWriteTimeoutException wte)
|
||||
{
|
||||
casWriteMetrics.timeouts.mark();
|
||||
writeMetricsMap.get(consistencyForPaxos).timeouts.mark();
|
||||
throw new CasWriteTimeoutException(wte.writeType, wte.consistency, wte.received, wte.blockFor, contentions);
|
||||
throw new CasWriteTimeoutException(wte.writeType, wte.consistency, wte.received, wte.blockFor, wte.contentions);
|
||||
}
|
||||
catch (ReadTimeoutException e)
|
||||
{
|
||||
|
|
@ -387,7 +385,7 @@ public class StorageProxy implements StorageProxyMBean
|
|||
writeMetricsMap.get(consistencyForPaxos).timeouts.mark();
|
||||
throw e;
|
||||
}
|
||||
catch (WriteFailureException|ReadFailureException e)
|
||||
catch (WriteFailureException | ReadFailureException e)
|
||||
{
|
||||
casWriteMetrics.failures.mark();
|
||||
writeMetricsMap.get(consistencyForPaxos).failures.mark();
|
||||
|
|
@ -401,18 +399,137 @@ public class StorageProxy implements StorageProxyMBean
|
|||
}
|
||||
finally
|
||||
{
|
||||
recordCasContention(contentions);
|
||||
Keyspace.open(keyspaceName).getColumnFamilyStore(cfName).metric.topCasPartitionContention.addSample(key.getKey(), contentions);
|
||||
final long latency = System.nanoTime() - startTimeForMetrics;
|
||||
casWriteMetrics.addNano(latency);
|
||||
writeMetricsMap.get(consistencyForPaxos).addNano(latency);
|
||||
}
|
||||
}
|
||||
|
||||
private static void recordCasContention(int contentions)
|
||||
private static void recordCasContention(TableMetadata table,
|
||||
DecoratedKey key,
|
||||
CASClientRequestMetrics casMetrics,
|
||||
int contentions)
|
||||
{
|
||||
if(contentions > 0)
|
||||
casWriteMetrics.contention.update(contentions);
|
||||
if (contentions == 0)
|
||||
return;
|
||||
|
||||
casMetrics.contention.update(contentions);
|
||||
Keyspace.open(table.keyspace)
|
||||
.getColumnFamilyStore(table.name)
|
||||
.metric
|
||||
.topCasPartitionContention
|
||||
.addSample(key.getKey(), contentions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the Paxos rounds for a given proposal, retrying when preempted until the timeout.
|
||||
*
|
||||
* <p>The main 'configurable' of this method is the {@code createUpdateProposal} method: it is called by the method
|
||||
* once a ballot has been successfully 'prepared' to generate the update to 'propose' (and commit if the proposal is
|
||||
* successful). That method also generates the result that the whole method will return. Note that due to retrying,
|
||||
* this method may be called multiple times and does not have to return the same results.
|
||||
*
|
||||
* @param metadata the table to update with Paxos.
|
||||
* @param key the partition updated.
|
||||
* @param consistencyForPaxos the serial consistency of the operation (either {@link ConsistencyLevel#SERIAL} or
|
||||
* {@link ConsistencyLevel#LOCAL_SERIAL}).
|
||||
* @param consistencyForReplayCommits the consistency for the commit phase of "replayed" in-progress operations.
|
||||
* @param consistencyForCommit the consistency for the commit phase of _this_ operation update.
|
||||
* @param state the client state.
|
||||
* @param queryStartNanoTime the nano time for the start of the query this is part of. This is the base time for
|
||||
* timeouts.
|
||||
* @param casMetrics the metrics to update for this operation.
|
||||
* @param createUpdateProposal method called after a successful 'prepare' phase to obtain 1) the actual update of
|
||||
* this operation and 2) the result that the whole method should return. This can return {@code null} in the
|
||||
* special where, after having "prepared" (and thus potentially replayed in-progress upgdates), we don't want
|
||||
* to propose anything (the whole method then return {@code null}).
|
||||
* @return the second element of the pair returned by {@code createUpdateProposal} (for the last call of that method
|
||||
* if that method is called multiple times due to retries).
|
||||
*/
|
||||
private static RowIterator doPaxos(TableMetadata metadata,
|
||||
DecoratedKey key,
|
||||
ConsistencyLevel consistencyForPaxos,
|
||||
ConsistencyLevel consistencyForReplayCommits,
|
||||
ConsistencyLevel consistencyForCommit,
|
||||
ClientState state,
|
||||
long queryStartNanoTime,
|
||||
CASClientRequestMetrics casMetrics,
|
||||
Supplier<Pair<PartitionUpdate, RowIterator>> createUpdateProposal)
|
||||
throws UnavailableException, IsBootstrappingException, RequestFailureException, RequestTimeoutException, InvalidRequestException
|
||||
{
|
||||
int contentions = 0;
|
||||
Keyspace keyspace = Keyspace.open(metadata.keyspace);
|
||||
try
|
||||
{
|
||||
consistencyForPaxos.validateForCas();
|
||||
consistencyForReplayCommits.validateForCasCommit(metadata.keyspace);
|
||||
consistencyForCommit.validateForCasCommit(metadata.keyspace);
|
||||
|
||||
long timeoutNanos = DatabaseDescriptor.getCasContentionTimeout(NANOSECONDS);
|
||||
while (System.nanoTime() - queryStartNanoTime < timeoutNanos)
|
||||
{
|
||||
// for simplicity, we'll do a single liveness check at the start of each attempt
|
||||
ReplicaPlan.ForPaxosWrite replicaPlan = ReplicaPlans.forPaxos(keyspace, key, consistencyForPaxos);
|
||||
PaxosBallotAndContention pair = beginAndRepairPaxos(queryStartNanoTime,
|
||||
key,
|
||||
metadata,
|
||||
replicaPlan,
|
||||
consistencyForPaxos,
|
||||
consistencyForReplayCommits,
|
||||
casMetrics,
|
||||
state);
|
||||
|
||||
final UUID ballot = pair.ballot;
|
||||
contentions += pair.contentions;
|
||||
|
||||
Pair<PartitionUpdate, RowIterator> proposalPair = createUpdateProposal.get();
|
||||
// See method javadoc: null here is code for "stop here and return null".
|
||||
if (proposalPair == null)
|
||||
return null;
|
||||
|
||||
Commit proposal = Commit.newProposal(ballot, proposalPair.left);
|
||||
Tracing.trace("CAS precondition is met; proposing client-requested updates for {}", ballot);
|
||||
if (proposePaxos(proposal, replicaPlan, true, queryStartNanoTime))
|
||||
{
|
||||
// We skip committing accepted updates when they are empty. This is an optimization which works
|
||||
// because we also skip replaying those same empty update in beginAndRepairPaxos (see the longer
|
||||
// comment there). As empty update are somewhat common (serial reads and non-applying CAS propose
|
||||
// them), this is worth bothering.
|
||||
if (!proposal.update.isEmpty())
|
||||
commitPaxos(proposal, consistencyForCommit, true, queryStartNanoTime);
|
||||
RowIterator result = proposalPair.right;
|
||||
if (result != null)
|
||||
Tracing.trace("CAS did not apply");
|
||||
else
|
||||
Tracing.trace("CAS applied successfully");
|
||||
return result;
|
||||
}
|
||||
|
||||
Tracing.trace("Paxos proposal not accepted (pre-empted by a higher ballot)");
|
||||
contentions++;
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), TimeUnit.MILLISECONDS);
|
||||
// continue to retry
|
||||
}
|
||||
}
|
||||
catch (CasWriteTimeoutException e)
|
||||
{
|
||||
// Might be thrown by beginRepairAndPaxos. In that case, any contention that happened within the method and
|
||||
// led up to the timeout was not accounted in our local 'contentions' variable and we add it now so it the
|
||||
// contention recorded in the finally is correct.
|
||||
contentions += e.contentions;
|
||||
throw e;
|
||||
}
|
||||
catch (WriteTimeoutException e)
|
||||
{
|
||||
// Might be thrown by proposePaxos or commitPaxos
|
||||
throw new CasWriteTimeoutException(e.writeType, e.consistency, e.received, e.blockFor, contentions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
recordCasContention(metadata, key, casMetrics, contentions);
|
||||
}
|
||||
|
||||
throw new CasWriteTimeoutException(WriteType.CAS, consistencyForPaxos, 0, consistencyForPaxos.blockFor(keyspace), contentions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -427,7 +544,7 @@ public class StorageProxy implements StorageProxyMBean
|
|||
ReplicaPlan.ForPaxosWrite paxosPlan,
|
||||
ConsistencyLevel consistencyForPaxos,
|
||||
ConsistencyLevel consistencyForCommit,
|
||||
final boolean isWrite,
|
||||
CASClientRequestMetrics casMetrics,
|
||||
ClientState state)
|
||||
throws WriteTimeoutException, WriteFailureException
|
||||
{
|
||||
|
|
@ -448,76 +565,87 @@ public class StorageProxy implements StorageProxyMBean
|
|||
UUID ballot = UUIDGen.getRandomTimeUUIDFromMicros(ballotMicros);
|
||||
|
||||
// prepare
|
||||
Tracing.trace("Preparing {}", ballot);
|
||||
Commit toPrepare = Commit.newPrepare(key, metadata, ballot);
|
||||
summary = preparePaxos(toPrepare, paxosPlan, queryStartNanoTime);
|
||||
if (!summary.promised)
|
||||
try
|
||||
{
|
||||
Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting");
|
||||
contentions++;
|
||||
// sleep a random amount to give the other proposer a chance to finish
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS);
|
||||
continue;
|
||||
}
|
||||
|
||||
Commit inProgress = summary.mostRecentInProgressCommitWithUpdate;
|
||||
Commit mostRecent = summary.mostRecentCommit;
|
||||
|
||||
// If we have an in-progress ballot greater than the MRC we know, then it's an in-progress round that
|
||||
// needs to be completed, so do it.
|
||||
if (!inProgress.update.isEmpty() && inProgress.isAfter(mostRecent))
|
||||
{
|
||||
Tracing.trace("Finishing incomplete paxos round {}", inProgress);
|
||||
if(isWrite)
|
||||
casWriteMetrics.unfinishedCommit.inc();
|
||||
else
|
||||
casReadMetrics.unfinishedCommit.inc();
|
||||
Commit refreshedInProgress = Commit.newProposal(ballot, inProgress.update);
|
||||
if (proposePaxos(refreshedInProgress, paxosPlan, false, queryStartNanoTime))
|
||||
Tracing.trace("Preparing {}", ballot);
|
||||
Commit toPrepare = Commit.newPrepare(key, metadata, ballot);
|
||||
summary = preparePaxos(toPrepare, paxosPlan, queryStartNanoTime);
|
||||
if (!summary.promised)
|
||||
{
|
||||
try
|
||||
Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting");
|
||||
contentions++;
|
||||
// sleep a random amount to give the other proposer a chance to finish
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS);
|
||||
continue;
|
||||
}
|
||||
|
||||
Commit inProgress = summary.mostRecentInProgressCommit;
|
||||
Commit mostRecent = summary.mostRecentCommit;
|
||||
|
||||
// If we have an in-progress ballot greater than the MRC we know, then it's an in-progress round that
|
||||
// needs to be completed, so do it.
|
||||
// One special case we make is for update that are empty (which are proposed by serial reads and
|
||||
// non-applying CAS). While we could handle those as any other updates, we can optimize this somewhat by
|
||||
// neither committing those empty updates, nor replaying in-progress ones. The reasoning is this: as the
|
||||
// update is empty, we have nothing to apply to storage in the commit phase, so the only reason to commit
|
||||
// would be to update the MRC. However, if we skip replaying those empty updates, then we don't need to
|
||||
// update the MRC for following updates to make progress (that is, if we didn't had the empty update skip
|
||||
// below _but_ skipped updating the MRC on empty updates, then we'd be stuck always proposing that same
|
||||
// empty update). And the reason skipping that replay is safe is that when an operation tries to propose
|
||||
// an empty value, there can be only 2 cases:
|
||||
// 1) the propose succeed, meaning a quorum of nodes accept it, in which case we are guaranteed no earlier
|
||||
// pending operation can ever be replayed (which is what we want to guarantee with the empty update).
|
||||
// 2) the propose does not succeed. But then the operation proposing the empty update will not succeed
|
||||
// either (it will retry or ultimately timeout), and we're actually ok if earlier pending operation gets
|
||||
// replayed in that case.
|
||||
// Tl;dr, it is safe to skip committing empty updates _as long as_ we also skip replying them below. And
|
||||
// doing is more efficient, so we do so.
|
||||
if (!inProgress.update.isEmpty() && inProgress.isAfter(mostRecent))
|
||||
{
|
||||
Tracing.trace("Finishing incomplete paxos round {}", inProgress);
|
||||
casMetrics.unfinishedCommit.inc();
|
||||
Commit refreshedInProgress = Commit.newProposal(ballot, inProgress.update);
|
||||
if (proposePaxos(refreshedInProgress, paxosPlan, false, queryStartNanoTime))
|
||||
{
|
||||
commitPaxos(refreshedInProgress, consistencyForCommit, false, queryStartNanoTime);
|
||||
}
|
||||
catch (WriteTimeoutException e)
|
||||
else
|
||||
{
|
||||
recordCasContention(contentions);
|
||||
// We're still doing preparation for the paxos rounds, so we want to use the CAS (see CASSANDRA-8672)
|
||||
throw new WriteTimeoutException(WriteType.CAS, e.consistency, e.received, e.blockFor);
|
||||
Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting");
|
||||
// sleep a random amount to give the other proposer a chance to finish
|
||||
contentions++;
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
||||
// To be able to propose our value on a new round, we need a quorum of replica to have learn the previous one. Why is explained at:
|
||||
// https://issues.apache.org/jira/browse/CASSANDRA-5062?focusedCommentId=13619810&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13619810)
|
||||
// Since we waited for quorum nodes, if some of them haven't seen the last commit (which may just be a timing issue, but may also
|
||||
// mean we lost messages), we pro-actively "repair" those nodes, and retry.
|
||||
int nowInSec = Ints.checkedCast(TimeUnit.MICROSECONDS.toSeconds(ballotMicros));
|
||||
Iterable<InetAddressAndPort> missingMRC = summary.replicasMissingMostRecentCommit(metadata, nowInSec);
|
||||
if (Iterables.size(missingMRC) > 0)
|
||||
{
|
||||
Tracing.trace("Some replicas have already promised a higher ballot than ours; aborting");
|
||||
// sleep a random amount to give the other proposer a chance to finish
|
||||
contentions++;
|
||||
Uninterruptibles.sleepUninterruptibly(ThreadLocalRandom.current().nextInt(100), MILLISECONDS);
|
||||
Tracing.trace("Repairing replicas that missed the most recent commit");
|
||||
sendCommit(mostRecent, missingMRC);
|
||||
// TODO: provided commits don't invalid the prepare we just did above (which they don't), we could just wait
|
||||
// for all the missingMRC to acknowledge this commit and then move on with proposing our value. But that means
|
||||
// adding the ability to have commitPaxos block, which is exactly CASSANDRA-5442 will do. So once we have that
|
||||
// latter ticket, we can pass CL.ALL to the commit above and remove the 'continue'.
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// To be able to propose our value on a new round, we need a quorum of replica to have learn the previous one. Why is explained at:
|
||||
// https://issues.apache.org/jira/browse/CASSANDRA-5062?focusedCommentId=13619810&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13619810)
|
||||
// Since we waited for quorum nodes, if some of them haven't seen the last commit (which may just be a timing issue, but may also
|
||||
// mean we lost messages), we pro-actively "repair" those nodes, and retry.
|
||||
int nowInSec = Ints.checkedCast(TimeUnit.MICROSECONDS.toSeconds(ballotMicros));
|
||||
Iterable<InetAddressAndPort> missingMRC = summary.replicasMissingMostRecentCommit(metadata, nowInSec);
|
||||
if (Iterables.size(missingMRC) > 0)
|
||||
return new PaxosBallotAndContention(ballot, contentions);
|
||||
}
|
||||
catch (WriteTimeoutException e)
|
||||
{
|
||||
Tracing.trace("Repairing replicas that missed the most recent commit");
|
||||
sendCommit(mostRecent, missingMRC);
|
||||
// TODO: provided commits don't invalid the prepare we just did above (which they don't), we could just wait
|
||||
// for all the missingMRC to acknowledge this commit and then move on with proposing our value. But that means
|
||||
// adding the ability to have commitPaxos block, which is exactly CASSANDRA-5442 will do. So once we have that
|
||||
// latter ticket, we can pass CL.ALL to the commit above and remove the 'continue'.
|
||||
continue;
|
||||
// We're still doing preparation for the paxos rounds, so we want to use the CAS (see CASSANDRA-8672)
|
||||
throw new CasWriteTimeoutException(WriteType.CAS, e.consistency, e.received, e.blockFor, contentions);
|
||||
}
|
||||
|
||||
return new PaxosBallotAndContention(ballot, contentions);
|
||||
}
|
||||
|
||||
recordCasContention(contentions);
|
||||
throw new WriteTimeoutException(WriteType.CAS, consistencyForPaxos, 0, consistencyForPaxos.blockFor(Keyspace.open(metadata.keyspace)));
|
||||
throw new CasWriteTimeoutException(WriteType.CAS, consistencyForPaxos, 0, consistencyForPaxos.blockFor(Keyspace.open(metadata.keyspace)), contentions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1643,19 +1771,31 @@ public class StorageProxy implements StorageProxyMBean
|
|||
PartitionIterator result = null;
|
||||
try
|
||||
{
|
||||
// make sure any in-progress paxos writes are done (i.e., committed to a majority of replicas), before performing a quorum read
|
||||
ReplicaPlan.ForPaxosWrite replicaPlan = ReplicaPlans.forPaxos(Keyspace.open(metadata.keyspace), key, consistencyLevel);
|
||||
|
||||
// does the work of applying in-progress writes; throws UAE or timeout if it can't
|
||||
final ConsistencyLevel consistencyForCommitOrFetch = consistencyLevel == ConsistencyLevel.LOCAL_SERIAL
|
||||
? ConsistencyLevel.LOCAL_QUORUM
|
||||
: ConsistencyLevel.QUORUM;
|
||||
final ConsistencyLevel consistencyForReplayCommitsOrFetch = consistencyLevel == ConsistencyLevel.LOCAL_SERIAL
|
||||
? ConsistencyLevel.LOCAL_QUORUM
|
||||
: ConsistencyLevel.QUORUM;
|
||||
|
||||
try
|
||||
{
|
||||
final PaxosBallotAndContention pair = beginAndRepairPaxos(start, key, metadata, replicaPlan, consistencyLevel, consistencyForCommitOrFetch, false, state);
|
||||
if (pair.contentions > 0)
|
||||
casReadMetrics.contention.update(pair.contentions);
|
||||
// Commit an empty update to make sure all in-progress updates that should be finished first is, _and_
|
||||
// that no other in-progress can get resurrected.
|
||||
Supplier<Pair<PartitionUpdate, RowIterator>> updateProposer =
|
||||
disableSerialReadLinearizability
|
||||
? () -> null
|
||||
: () -> Pair.create(PartitionUpdate.emptyUpdate(metadata, key), null);
|
||||
// When replaying, we commit at quorum/local quorum, as we want to be sure the following read (done at
|
||||
// quorum/local_quorum) sees any replayed updates. Our own update is however empty, and those don't even
|
||||
// get committed due to an optimiation described in doPaxos/beingRepairAndPaxos, so the commit
|
||||
// consistency is irrelevant (we use ANY just to emphasis that we don't wait on our commit).
|
||||
doPaxos(metadata,
|
||||
key,
|
||||
consistencyLevel,
|
||||
consistencyForReplayCommitsOrFetch,
|
||||
ConsistencyLevel.ANY,
|
||||
state,
|
||||
start,
|
||||
casReadMetrics,
|
||||
updateProposer);
|
||||
}
|
||||
catch (WriteTimeoutException e)
|
||||
{
|
||||
|
|
@ -1666,7 +1806,7 @@ public class StorageProxy implements StorageProxyMBean
|
|||
throw new ReadFailureException(consistencyLevel, e.received, e.blockFor, false, e.failureReasonByEndpoint);
|
||||
}
|
||||
|
||||
result = fetchRows(group.queries, consistencyForCommitOrFetch, queryStartNanoTime);
|
||||
result = fetchRows(group.queries, consistencyForReplayCommitsOrFetch, queryStartNanoTime);
|
||||
}
|
||||
catch (UnavailableException e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ public class Commit
|
|||
return this.ballot.equals(ballot);
|
||||
}
|
||||
|
||||
/** Whether this is an empty commit, that is one with no updates. */
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return update.isEmpty();
|
||||
}
|
||||
|
||||
public Mutation makeMutation()
|
||||
{
|
||||
return new Mutation(update);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ public class PrepareCallback extends AbstractPaxosCallback<PrepareResponse>
|
|||
public boolean promised = true;
|
||||
public Commit mostRecentCommit;
|
||||
public Commit mostRecentInProgressCommit;
|
||||
public Commit mostRecentInProgressCommitWithUpdate;
|
||||
|
||||
private final Map<InetAddressAndPort, Commit> commitsByReplica = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
@ -56,7 +55,6 @@ public class PrepareCallback extends AbstractPaxosCallback<PrepareResponse>
|
|||
// need to inject the right key in the empty commit so comparing with empty commits in the response works as expected
|
||||
mostRecentCommit = Commit.emptyCommit(key, metadata);
|
||||
mostRecentInProgressCommit = Commit.emptyCommit(key, metadata);
|
||||
mostRecentInProgressCommitWithUpdate = Commit.emptyCommit(key, metadata);
|
||||
}
|
||||
|
||||
public synchronized void onResponse(Message<PrepareResponse> message)
|
||||
|
|
@ -64,9 +62,8 @@ public class PrepareCallback extends AbstractPaxosCallback<PrepareResponse>
|
|||
PrepareResponse response = message.payload;
|
||||
logger.trace("Prepare response {} from {}", response, message.from());
|
||||
|
||||
// In case of clock skew, another node could be proposing with ballot that are quite a bit
|
||||
// older than our own. In that case, we record the more recent commit we've received to make
|
||||
// sure we re-prepare on an older ballot.
|
||||
// We set the mostRecentInProgressCommit even if we're not promised as, in that case, the ballot of that commit
|
||||
// will be used to avoid generating a ballot that has not chance to win on retry (think clock skew).
|
||||
if (response.inProgressCommit.isAfter(mostRecentInProgressCommit))
|
||||
mostRecentInProgressCommit = response.inProgressCommit;
|
||||
|
||||
|
|
@ -82,11 +79,6 @@ public class PrepareCallback extends AbstractPaxosCallback<PrepareResponse>
|
|||
if (response.mostRecentCommit.isAfter(mostRecentCommit))
|
||||
mostRecentCommit = response.mostRecentCommit;
|
||||
|
||||
// If some response has an update, then we should replay the update with the highest ballot. So find
|
||||
// the the highest commit that actually have an update
|
||||
if (response.inProgressCommit.isAfter(mostRecentInProgressCommitWithUpdate) && !response.inProgressCommit.update.isEmpty())
|
||||
mostRecentInProgressCommitWithUpdate = response.inProgressCommit;
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ import org.apache.cassandra.db.SystemKeyspace;
|
|||
import org.apache.cassandra.db.SystemKeyspaceMigrator40;
|
||||
import org.apache.cassandra.db.commitlog.CommitLog;
|
||||
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||
import org.apache.cassandra.dht.IPartitioner;
|
||||
import org.apache.cassandra.dht.Token;
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.distributed.Constants;
|
||||
import org.apache.cassandra.distributed.action.GossipHelper;
|
||||
|
|
@ -79,7 +81,9 @@ import org.apache.cassandra.distributed.api.NodeToolResult;
|
|||
import org.apache.cassandra.distributed.api.SimpleQueryResult;
|
||||
import org.apache.cassandra.distributed.mock.nodetool.InternalNodeProbe;
|
||||
import org.apache.cassandra.distributed.mock.nodetool.InternalNodeProbeFactory;
|
||||
import org.apache.cassandra.gms.ApplicationState;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.VersionedValue;
|
||||
import org.apache.cassandra.hints.HintsService;
|
||||
import org.apache.cassandra.index.SecondaryIndexManager;
|
||||
import org.apache.cassandra.io.IVersionedAsymmetricSerializer;
|
||||
|
|
@ -538,6 +542,85 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
|
|||
return YamlConfigurationLoader.fromMap(params, check, Config.class);
|
||||
}
|
||||
|
||||
public static void addToRing(boolean bootstrapping, IInstance peer)
|
||||
{
|
||||
try
|
||||
{
|
||||
IInstanceConfig config = peer.config();
|
||||
IPartitioner partitioner = FBUtilities.newPartitioner(config.getString("partitioner"));
|
||||
Token token = partitioner.getTokenFactory().fromString(config.getString("initial_token"));
|
||||
InetAddressAndPort addressAndPort = toCassandraInetAddressAndPort(peer.broadcastAddress());
|
||||
|
||||
UUID hostId = config.hostId();
|
||||
Gossiper.runInGossipStageBlocking(() -> {
|
||||
Gossiper.instance.initializeNodeUnsafe(addressAndPort, hostId, 1);
|
||||
Gossiper.instance.injectApplicationState(addressAndPort,
|
||||
ApplicationState.TOKENS,
|
||||
new VersionedValue.VersionedValueFactory(partitioner).tokens(Collections.singleton(token)));
|
||||
StorageService.instance.onChange(addressAndPort,
|
||||
ApplicationState.STATUS,
|
||||
bootstrapping
|
||||
? new VersionedValue.VersionedValueFactory(partitioner).bootstrapping(Collections.singleton(token))
|
||||
: new VersionedValue.VersionedValueFactory(partitioner).normal(Collections.singleton(token)));
|
||||
Gossiper.instance.realMarkAlive(addressAndPort, Gossiper.instance.getEndpointStateForEndpoint(addressAndPort));
|
||||
});
|
||||
int messagingVersion = peer.isShutdown()
|
||||
? MessagingService.current_version
|
||||
: Math.min(MessagingService.current_version, peer.getMessagingVersion());
|
||||
MessagingService.instance().versions.set(addressAndPort, messagingVersion);
|
||||
|
||||
assert bootstrapping || StorageService.instance.getTokenMetadata().isMember(addressAndPort);
|
||||
PendingRangeCalculatorService.instance.blockUntilFinished();
|
||||
}
|
||||
catch (Throwable e) // UnknownHostException
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeFromRing(IInstance peer)
|
||||
{
|
||||
try
|
||||
{
|
||||
IInstanceConfig config = peer.config();
|
||||
IPartitioner partitioner = FBUtilities.newPartitioner(config.getString("partitioner"));
|
||||
Token token = partitioner.getTokenFactory().fromString(config.getString("initial_token"));
|
||||
InetAddressAndPort addressAndPort = toCassandraInetAddressAndPort(peer.broadcastAddress());
|
||||
|
||||
Gossiper.runInGossipStageBlocking(() -> {
|
||||
StorageService.instance.onChange(addressAndPort,
|
||||
ApplicationState.STATUS,
|
||||
new VersionedValue.VersionedValueFactory(partitioner).left(Collections.singleton(token), 0L));
|
||||
Gossiper.instance.removeEndpoint(addressAndPort);
|
||||
});
|
||||
PendingRangeCalculatorService.instance.blockUntilFinished();
|
||||
}
|
||||
catch (Throwable e) // UnknownHostException
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addToRingNormal(IInstance peer)
|
||||
{
|
||||
addToRing(false, peer);
|
||||
assert StorageService.instance.getTokenMetadata().isMember(toCassandraInetAddressAndPort(peer.broadcastAddress()));
|
||||
}
|
||||
|
||||
public static void addToRingBootstrapping(IInstance peer)
|
||||
{
|
||||
addToRing(true, peer);
|
||||
}
|
||||
|
||||
private static void initializeRing(ICluster cluster)
|
||||
{
|
||||
for (int i = 1 ; i <= cluster.size() ; ++i)
|
||||
addToRing(false, cluster.get(i));
|
||||
|
||||
for (int i = 1; i <= cluster.size(); ++i)
|
||||
assert StorageService.instance.getTokenMetadata().isMember(toCassandraInetAddressAndPort(cluster.get(i).broadcastAddress()));
|
||||
}
|
||||
|
||||
public Future<Void> shutdown()
|
||||
{
|
||||
return shutdown(true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,688 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.marshal.Int32Type;
|
||||
import org.apache.cassandra.dht.Murmur3Partitioner;
|
||||
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.IMessageFilters;
|
||||
import org.apache.cassandra.distributed.impl.Instance;
|
||||
import org.apache.cassandra.schema.TableId;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.UUIDGen;
|
||||
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.assertRows;
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.fail;
|
||||
import static org.apache.cassandra.distributed.shared.AssertUtils.row;
|
||||
import static org.apache.cassandra.net.Verb.PAXOS_COMMIT_REQ;
|
||||
import static org.apache.cassandra.net.Verb.PAXOS_PREPARE_REQ;
|
||||
import static org.apache.cassandra.net.Verb.PAXOS_PROPOSE_REQ;
|
||||
import static org.apache.cassandra.net.Verb.READ_REQ;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class CASTest extends TestBaseImpl
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CASTest.class);
|
||||
|
||||
@Test
|
||||
public void simpleUpdate() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = init(Cluster.create(3)))
|
||||
{
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (1, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL),
|
||||
row(1, 1, 1));
|
||||
cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET v = 3 WHERE pk = 1 and ck = 1 IF v = 2", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL),
|
||||
row(1, 1, 1));
|
||||
cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET v = 2 WHERE pk = 1 and ck = 1 IF v = 1", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL),
|
||||
row(1, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompletePrepare() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", 200L).set("cas_contention_timeout_in_ms", 200L))))
|
||||
{
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
IMessageFilters.Filter drop = cluster.filters().verbs(PAXOS_PREPARE_REQ.id).from(1).to(2, 3).drop();
|
||||
try
|
||||
{
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (1, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertEquals("CAS operation timed out - encountered contentions: 0", e.getMessage());
|
||||
}
|
||||
drop.off();
|
||||
cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET v = 2 WHERE pk = 1 and ck = 1 IF v = 1", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompletePropose() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", 200L).set("cas_contention_timeout_in_ms", 200L))))
|
||||
{
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
IMessageFilters.Filter drop1 = cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(2, 3).drop();
|
||||
try
|
||||
{
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (1, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertEquals("CAS operation timed out - encountered contentions: 0", e.getMessage());
|
||||
}
|
||||
drop1.off();
|
||||
// make sure we encounter one of the in-progress proposals so we complete it
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id).from(1).to(2).drop();
|
||||
cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET v = 2 WHERE pk = 1 and ck = 1 IF v = 1", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL),
|
||||
row(1, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void incompleteCommit() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", 200L).set("cas_contention_timeout_in_ms", 200L))))
|
||||
{
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
IMessageFilters.Filter drop1 = cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
try
|
||||
{
|
||||
cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (1, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM);
|
||||
Assert.fail();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Assert.assertEquals("CAS operation timed out - encountered contentions: 0", e.getMessage());
|
||||
}
|
||||
drop1.off();
|
||||
// make sure we see one of the successful commits
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(2).drop();
|
||||
cluster.coordinator(1).execute("UPDATE " + KEYSPACE + ".tbl SET v = 2 WHERE pk = 1 and ck = 1 IF v = 1", ConsistencyLevel.QUORUM);
|
||||
assertRows(cluster.coordinator(1).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = 1", ConsistencyLevel.SERIAL),
|
||||
row(1, 1, 2));
|
||||
}
|
||||
}
|
||||
|
||||
private int[] paxosAndReadVerbs() {
|
||||
return new int[] { PAXOS_PREPARE_REQ.id, PAXOS_PROPOSE_REQ.id, PAXOS_COMMIT_REQ.id, READ_REQ.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Base test to ensure that if a write times out but with a proposal accepted by some nodes (less then quorum), and
|
||||
* a following SERIAL operation does not observe that write (the node having accepted it do not participate in that
|
||||
* following operation), then that write is never applied, even when the nodes having accepted the original proposal
|
||||
* participate.
|
||||
*
|
||||
* <p>In other words, if an operation timeout, it may or may not be applied, but that "fate" is persistently decided
|
||||
* by the very SERIAL operation that "succeed" (in the sense of 'not timing out or throwing some other exception').
|
||||
*
|
||||
* @param postTimeoutOperation1 a SERIAL operation executed after an initial write that inserts the row [0, 0] times
|
||||
* out. It is executed with a QUORUM of nodes that have _not_ see the timed out
|
||||
* proposal, and so that operation should expect that the [0, 0] write has not taken
|
||||
* place.
|
||||
* @param postTimeoutOperation2 a 2nd SERIAL operation executed _after_ {@code postTimeoutOperation1}, with no
|
||||
* write executed between the 2 operation. Contrarily to the 1st operation, the QORUM
|
||||
* for this operation _will_ include the node that got the proposal for the [0, 0]
|
||||
* insert but didn't participated to {@code postTimeoutOperation1}}. That operation
|
||||
* should also no witness that [0, 0] write (since {@code postTimeoutOperation1}
|
||||
* didn't).
|
||||
* @param loseCommitOfOperation1 if {@code true}, the test will also drop the "commits" messages for
|
||||
* {@code postTimeoutOperation1}. In general, the test should behave the same with or
|
||||
* without that flag since a value is decided as soon as it has been "accepted by
|
||||
* quorum" and the commits should always be properly replayed.
|
||||
*/
|
||||
private void consistencyAfterWriteTimeoutTest(BiConsumer<String, ICoordinator> postTimeoutOperation1,
|
||||
BiConsumer<String, ICoordinator> postTimeoutOperation2,
|
||||
boolean loseCommitOfOperation1) throws IOException
|
||||
{
|
||||
// It's unclear why (haven't dug), but in some of the instance of this test method, there is a consistent 2+
|
||||
// seconds pauses between the prepare and propose phases during the execution of 'postTimeoutOperation2'. This
|
||||
// does not happen on 3.0 and there is no report of such long pauses otherwise, so an hypothesis is that this
|
||||
// is due to the in-jvm dtest framework. This is is why we use a 4 seconds timeout here. Given this test is
|
||||
// not about performance, this is probably ok, even if we ideally should dug into the underlying reason.
|
||||
try (Cluster cluster = init(Cluster.create(3, config -> config.set("write_request_timeout_in_ms", 4000L)
|
||||
.set("cas_contention_timeout_in_ms", 200L))))
|
||||
{
|
||||
String table = KEYSPACE + ".t";
|
||||
cluster.schemaChange("CREATE TABLE " + table + " (k int PRIMARY KEY, v int)");
|
||||
|
||||
// We do a CAS insertion, but have with the PROPOSE message dropped on node 1 and 2. The CAS will not get
|
||||
// through and should timeout. Importantly, node 3 does receive and answer the PROPOSE.
|
||||
IMessageFilters.Filter dropProposeFilter = cluster.filters()
|
||||
.inbound()
|
||||
.verbs(PAXOS_PROPOSE_REQ.id)
|
||||
.from(3)
|
||||
.to(1, 2)
|
||||
.drop();
|
||||
try
|
||||
{
|
||||
// NOTE: the consistency below is the "commit" one, so it doesn't matter at all here.
|
||||
// NOTE 2: we use node 3 as coordinator because message filters don't currently work for locally
|
||||
// delivered messages and as we want to drop messages to 1 and 2, we can't use them.
|
||||
cluster.coordinator(3)
|
||||
.execute("INSERT INTO " + table + "(k, v) VALUES (0, 0) IF NOT EXISTS", ConsistencyLevel.ONE);
|
||||
fail("The insertion should have timed-out");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// We expect a write timeout. If we get one, the test can continue, otherwise, we rethrow. Note that we
|
||||
// look at the root cause because the dtest framework effectively wrap the exception in a RuntimeException
|
||||
// (we could just look at the immediate cause, but this feel a bit more resilient this way).
|
||||
// TODO: we can't use an instanceof below because the WriteTimeoutException we get is from a different class
|
||||
// loader than the one the test run under, and that's our poor-man work-around. This kind of things should
|
||||
// be improved at the dtest API level.
|
||||
if (!e.getClass().getSimpleName().equals("CasWriteTimeoutException"))
|
||||
throw e;
|
||||
}
|
||||
finally
|
||||
{
|
||||
dropProposeFilter.off();
|
||||
}
|
||||
|
||||
// Isolates node 3 and executes the SERIAL operation. As neither node 1 or 2 got the initial insert proposal,
|
||||
// there is nothing to "replay" and the operation should assert the table is still empty.
|
||||
IMessageFilters.Filter ignoreNode3Filter = cluster.filters().verbs(paxosAndReadVerbs()).to(3).drop();
|
||||
IMessageFilters.Filter dropCommitFilter = null;
|
||||
if (loseCommitOfOperation1)
|
||||
{
|
||||
dropCommitFilter = cluster.filters().verbs(PAXOS_COMMIT_REQ.id).to(1, 2).drop();
|
||||
}
|
||||
try
|
||||
{
|
||||
postTimeoutOperation1.accept(table, cluster.coordinator(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ignoreNode3Filter.off();
|
||||
if (dropCommitFilter != null)
|
||||
dropCommitFilter.off();
|
||||
}
|
||||
|
||||
// Node 3 is now back and we isolate node 2 to ensure the next read hits node 1 and 3.
|
||||
// What we want to ensure is that despite node 3 having the initial insert in its paxos state in a position of
|
||||
// being replayed, that insert is _not_ replayed (it would contradict serializability since the previous
|
||||
// operation asserted nothing was inserted). It is this execution that failed before CASSANDRA-12126.
|
||||
IMessageFilters.Filter ignoreNode2Filter = cluster.filters().verbs(paxosAndReadVerbs()).to(2).drop();
|
||||
try
|
||||
{
|
||||
postTimeoutOperation2.accept(table, cluster.coordinator(1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ignoreNode2Filter.off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that if a write timeouts and a following serial read does not see that write, then no following reads sees
|
||||
* it, even if some nodes still have the write in their paxos state.
|
||||
*
|
||||
* <p>This specifically test for the inconsistency described/fixed by CASSANDRA-12126.
|
||||
*/
|
||||
@Test
|
||||
public void readConsistencyAfterWriteTimeoutTest() throws IOException
|
||||
{
|
||||
BiConsumer<String, ICoordinator> operation =
|
||||
(table, coordinator) -> assertRows(coordinator.execute("SELECT * FROM " + table + " WHERE k=0",
|
||||
ConsistencyLevel.SERIAL));
|
||||
|
||||
consistencyAfterWriteTimeoutTest(operation, operation, false);
|
||||
consistencyAfterWriteTimeoutTest(operation, operation, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that if a write timeouts, then a following CAS succeed but does not apply in a way that indicate the write
|
||||
* has not applied, then no following CAS can see that initial insert , even if some nodes still have the write in
|
||||
* their paxos state.
|
||||
*
|
||||
* <p>This specifically test for the inconsistency described/fixed by CASSANDRA-12126.
|
||||
*/
|
||||
@Test
|
||||
public void nonApplyingCasConsistencyAfterWriteTimeout() throws IOException
|
||||
{
|
||||
// Note: we use CL.ANY so that the operation don't timeout in the case where we "lost" the operation1 commits.
|
||||
// The commit CL shouldn't have impact on this test anyway, so this doesn't diminishes the test.
|
||||
BiConsumer<String, ICoordinator> operation =
|
||||
(table, coordinator) -> assertCasNotApplied(coordinator.execute("UPDATE " + table + " SET v = 1 WHERE k = 0 IF v = 0",
|
||||
ConsistencyLevel.ANY));
|
||||
consistencyAfterWriteTimeoutTest(operation, operation, false);
|
||||
consistencyAfterWriteTimeoutTest(operation, operation, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that if a write timeouts and a following serial read does not see that write, then no following CAS see
|
||||
* that initial insert, even if some nodes still have the write in their paxos state.
|
||||
*
|
||||
* <p>This specifically test for the inconsistency described/fixed by CASSANDRA-12126.
|
||||
*/
|
||||
@Test
|
||||
public void mixedReadAndNonApplyingCasConsistencyAfterWriteTimeout() throws IOException
|
||||
{
|
||||
BiConsumer<String, ICoordinator> operation1 =
|
||||
(table, coordinator) -> assertRows(coordinator.execute("SELECT * FROM " + table + " WHERE k=0",
|
||||
ConsistencyLevel.SERIAL));
|
||||
BiConsumer<String, ICoordinator> operation2 =
|
||||
(table, coordinator) -> assertCasNotApplied(coordinator.execute("UPDATE " + table + " SET v = 1 WHERE k = 0 IF v = 0",
|
||||
ConsistencyLevel.QUORUM));
|
||||
consistencyAfterWriteTimeoutTest(operation1, operation2, false);
|
||||
consistencyAfterWriteTimeoutTest(operation1, operation2, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that if a write timeouts and a following CAS succeed but does not apply in a way that indicate the write
|
||||
* has not applied, then following serial reads do no see that write, even if some nodes still have the write in
|
||||
* their paxos state.
|
||||
*
|
||||
* <p>This specifically test for the inconsistency described/fixed by CASSANDRA-12126.
|
||||
*/
|
||||
@Test
|
||||
public void mixedNonApplyingCasAndReadConsistencyAfterWriteTimeout() throws IOException
|
||||
{
|
||||
// Note: we use CL.ANY so that the operation don't timeout in the case where we "lost" the operation1 commits.
|
||||
// The commit CL shouldn't have impact on this test anyway, so this doesn't diminishes the test.
|
||||
BiConsumer<String, ICoordinator> operation1 =
|
||||
(table, coordinator) -> assertCasNotApplied(coordinator.execute("UPDATE " + table + " SET v = 1 WHERE k = 0 IF v = 0",
|
||||
ConsistencyLevel.ANY));
|
||||
BiConsumer<String, ICoordinator> operation2 =
|
||||
(table, coordinator) -> assertRows(coordinator.execute("SELECT * FROM " + table + " WHERE k=0",
|
||||
ConsistencyLevel.SERIAL));
|
||||
consistencyAfterWriteTimeoutTest(operation1, operation2, false);
|
||||
consistencyAfterWriteTimeoutTest(operation1, operation2, true);
|
||||
}
|
||||
|
||||
// TODO: this shoud probably be moved into the dtest API.
|
||||
private void assertCasNotApplied(Object[][] resultSet)
|
||||
{
|
||||
assertFalse("Expected a CAS resultSet (with at least application result) but got an empty one.",
|
||||
resultSet.length == 0);
|
||||
assertFalse("Invalid empty first row in CAS resultSet.", resultSet[0].length == 0);
|
||||
Object wasApplied = resultSet[0][0];
|
||||
assertTrue("Expected 1st column of CAS resultSet to be a boolean, but got a " + wasApplied.getClass(),
|
||||
wasApplied instanceof Boolean);
|
||||
assertFalse("Expected CAS to not be applied, but was applied.", (Boolean)wasApplied);
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed write (by node that did not yet witness a range movement via gossip) is witnessed later as successful
|
||||
* conflicting with another successful write performed by a node that did witness the range movement
|
||||
* Prepare, Propose and Commit A to {1, 2}
|
||||
* Range moves to {2, 3, 4}
|
||||
* Prepare and Propose B (=> !A) to {3, 4}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testSuccessfulWriteBeforeRangeMovement() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {1} is unaware (yet) that {4} is an owner of the token
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
|
||||
int pk = pk(cluster, 1, 2);
|
||||
|
||||
// {1} promises and accepts on !{3} => {1, 2}; commits on !{2,3} => {1}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
for (int i = 1 ; i <= 3 ; ++i)
|
||||
cluster.get(i).acceptsOnInstance(Instance::addToRingNormal).accept(cluster.get(4));
|
||||
|
||||
// {4} reads from !{2} => {3, 4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(4).to(2).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(4).to(2).drop();
|
||||
assertRows(cluster.coordinator(4).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(false, pk, 1, 1, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Failed write (by node that did not yet witness a range movement via gossip) is witnessed later as successful
|
||||
* conflicting with another successful write performed by a node that did witness the range movement
|
||||
* - Range moves from {1, 2, 3} to {2, 3, 4}, witnessed by X (not by !X)
|
||||
* - X: Prepare, Propose and Commit A to {3, 4}
|
||||
* - !X: Prepare and Propose B (=>!A) to {1, 2}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testConflictingWritesWithStaleRingInformation() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {1} is unaware (yet) that {4} is an owner of the token
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
|
||||
// {4} promises, accepts and commits on !{2} => {3, 4}
|
||||
int pk = pk(cluster, 1, 2);
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(4).to(2).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(4).to(2).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(4).to(2).drop();
|
||||
assertRows(cluster.coordinator(4).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
// {1} promises, accepts and commmits on !{3} => {1, 2}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(false, pk, 1, 1, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Successful write during range movement, not witnessed by read after range movement.
|
||||
* Very similar to {@link #testConflictingWritesWithStaleRingInformation}.
|
||||
*
|
||||
* - Range moves from {1, 2, 3} to {2, 3, 4}; witnessed by X (not by !X)
|
||||
* - !X: Prepare and Propose to {1, 2}
|
||||
* - Range movement witnessed by !X
|
||||
* - Any: Prepare and Read from {3, 4}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testSucccessfulWriteDuringRangeMovementFollowedByRead() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {4} is bootstrapping, and this has not propagated to other nodes yet
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
cluster.get(4).acceptsOnInstance(Instance::addToRingBootstrapping).accept(cluster.get(4));
|
||||
|
||||
int pk = pk(cluster, 1, 2);
|
||||
|
||||
// {1} promises and accepts on !{3} => {1, 2}; commmits on !{2, 3} => {1}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
// finish topology change
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(i).acceptsOnInstance(Instance::addToRingNormal).accept(cluster.get(4));
|
||||
|
||||
// {3} reads from !{2} => {3, 4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(3).to(2).drop();
|
||||
assertRows(cluster.coordinator(3).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = ?", ConsistencyLevel.SERIAL, pk),
|
||||
row(pk, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Successful write during range movement not witnessed by write after range movement
|
||||
*
|
||||
* - Range moves from {1, 2, 3} to {2, 3, 4}; witnessed by X (not by !X)
|
||||
* - !X: Prepare and Propose to {1, 2}
|
||||
* - Range movement witnessed by !X
|
||||
* - Any: Prepare and Propose to {3, 4}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testSuccessfulWriteDuringRangeMovementFollowedByConflicting() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {4} is bootstrapping, and this has not propagated to other nodes yet
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
cluster.get(4).acceptsOnInstance(Instance::addToRingBootstrapping).accept(cluster.get(4));
|
||||
|
||||
int pk = pk(cluster, 1, 2);
|
||||
|
||||
// {1} promises and accepts on !{3} => {1, 2}; commits on !{2, 3} => {1}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
// finish topology change
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(i).acceptsOnInstance(Instance::addToRingNormal).accept(cluster.get(4));
|
||||
|
||||
// {3} reads from !{2} => {3, 4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(3).to(2).drop();
|
||||
assertRows(cluster.coordinator(3).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(false, pk, 1, 1, null));
|
||||
|
||||
// TODO: repair and verify base table state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* During a range movement, a CAS may fail leaving side effects that are not witnessed by another operation
|
||||
* being performed with stale ring information.
|
||||
* This is a particular special case of stale ring information sequencing, which probably would be resolved
|
||||
* by fixing each of the more isolated cases (but is unique, so deserving of its own test case).
|
||||
* See CASSANDRA-15745
|
||||
*
|
||||
* - Range moves from {1, 2, 3} to {2, 3, 4}; witnessed by X (not by !X)
|
||||
* - X: Prepare to {2, 3, 4}
|
||||
* - X: Propose to {4}
|
||||
* - !X: Prepare and Propose to {1, 2}
|
||||
* - Range move visible by !X
|
||||
* - Any: Prepare and Read from {3, 4}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testIncompleteWriteFollowedBySuccessfulWriteWithStaleRingDuringRangeMovementFollowedByRead() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {4} is bootstrapping, and this has not propagated to other nodes yet
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
cluster.get(4).acceptsOnInstance(Instance::addToRingBootstrapping).accept(cluster.get(4));
|
||||
|
||||
int pk = pk(cluster, 1, 2);
|
||||
|
||||
// {4} promises and accepts on !{1} => {2, 3, 4}; commits on !{1, 2, 3} => {4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(4).to(1).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(4).to(1, 2, 3).drop();
|
||||
try
|
||||
{
|
||||
cluster.coordinator(4).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM, pk);
|
||||
Assert.assertTrue(false);
|
||||
}
|
||||
catch (RuntimeException wrapped)
|
||||
{
|
||||
Assert.assertEquals("Operation timed out - received only 1 responses.", wrapped.getCause().getMessage());
|
||||
}
|
||||
|
||||
// {1} promises and accepts on !{3} => {1, 2}; commits on !{2, 3} => {1}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
// finish topology change
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(i).acceptsOnInstance(Instance::addToRingNormal).accept(cluster.get(4));
|
||||
|
||||
// {3} reads from !{2} => {3, 4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(3).to(2).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(3).to(2).drop();
|
||||
assertRows(cluster.coordinator(3).execute("SELECT * FROM " + KEYSPACE + ".tbl WHERE pk = ?", ConsistencyLevel.SERIAL, pk),
|
||||
row(pk, 1, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* During a range movement, a CAS may fail leaving side effects that are not witnessed by another operation
|
||||
* being performed with stale ring information.
|
||||
* This is a particular special case of stale ring information sequencing, which probably would be resolved
|
||||
* by fixing each of the more isolated cases (but is unique, so deserving of its own test case).
|
||||
* See CASSANDRA-15745
|
||||
*
|
||||
* - Range moves from {1, 2, 3} to {2, 3, 4}; witnessed by X (not by !X)
|
||||
* - X: Prepare to {2, 3, 4}
|
||||
* - X: Propose to {4}
|
||||
* - !X: Prepare and Propose to {1, 2}
|
||||
* - Range move visible by !X
|
||||
* - Any: Prepare and Propose to {3, 4}
|
||||
*/
|
||||
@Ignore
|
||||
@Test
|
||||
public void testIncompleteWriteFollowedBySuccessfulWriteWithStaleRingDuringRangeMovementFollowedByWrite() throws Throwable
|
||||
{
|
||||
try (Cluster cluster = Cluster.create(4, config -> config
|
||||
.set("write_request_timeout_in_ms", 200L)
|
||||
.set("cas_contention_timeout_in_ms", 200L)))
|
||||
{
|
||||
cluster.schemaChange("CREATE KEYSPACE " + KEYSPACE + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int, ck int, v1 int, v2 int, PRIMARY KEY (pk, ck))");
|
||||
|
||||
// make it so {4} is bootstrapping, and this has not propagated to other nodes yet
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(1).acceptsOnInstance(Instance::removeFromRing).accept(cluster.get(4));
|
||||
cluster.get(4).acceptsOnInstance(Instance::addToRingBootstrapping).accept(cluster.get(4));
|
||||
|
||||
int pk = pk(cluster, 1, 2);
|
||||
|
||||
// {4} promises and accepts on !{1} => {2, 3, 4}; commits on !{1, 2, 3} => {4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(4).to(1).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(4).to(1, 2, 3).drop();
|
||||
try
|
||||
{
|
||||
cluster.coordinator(4).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v1) VALUES (?, 1, 1) IF NOT EXISTS", ConsistencyLevel.QUORUM, pk);
|
||||
Assert.assertTrue(false);
|
||||
}
|
||||
catch (RuntimeException wrapped)
|
||||
{
|
||||
Assert.assertEquals("Operation timed out - received only 1 responses.", wrapped.getCause().getMessage());
|
||||
}
|
||||
|
||||
// {1} promises and accepts on !{3} => {1, 2}; commits on !{2, 3} => {1}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(1).to(3).drop();
|
||||
cluster.filters().verbs(PAXOS_COMMIT_REQ.id).from(1).to(2, 3).drop();
|
||||
assertRows(cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(true));
|
||||
|
||||
// finish topology change
|
||||
for (int i = 1 ; i <= 4 ; ++i)
|
||||
cluster.get(i).acceptsOnInstance(Instance::addToRingNormal).accept(cluster.get(4));
|
||||
|
||||
// {3} reads from !{2} => {3, 4}
|
||||
cluster.filters().verbs(PAXOS_PREPARE_REQ.id, READ_REQ.id).from(3).to(2).drop();
|
||||
cluster.filters().verbs(PAXOS_PROPOSE_REQ.id).from(3).to(2).drop();
|
||||
assertRows(cluster.coordinator(3).execute("INSERT INTO " + KEYSPACE + ".tbl (pk, ck, v2) VALUES (?, 1, 2) IF NOT EXISTS", ConsistencyLevel.ONE, pk),
|
||||
row(false, 5, 1, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
private static int pk(Cluster cluster, int lb, int ub)
|
||||
{
|
||||
return pk(cluster.get(lb), cluster.get(ub));
|
||||
}
|
||||
|
||||
private static int pk(IInstance lb, IInstance ub)
|
||||
{
|
||||
return pk(Murmur3Partitioner.instance.getTokenFactory().fromString(lb.config().getString("initial_token")),
|
||||
Murmur3Partitioner.instance.getTokenFactory().fromString(ub.config().getString("initial_token")));
|
||||
}
|
||||
|
||||
private static int pk(Token lb, Token ub)
|
||||
{
|
||||
int pk = 0;
|
||||
Token pkt;
|
||||
while (lb.compareTo(pkt = Murmur3Partitioner.instance.getToken(Int32Type.instance.decompose(pk))) >= 0 || ub.compareTo(pkt) < 0)
|
||||
++pk;
|
||||
return pk;
|
||||
}
|
||||
|
||||
private static void debugOwnership(Cluster cluster, int pk)
|
||||
{
|
||||
for (int i = 1 ; i <= cluster.size() ; ++i)
|
||||
System.out.println(i + ": " + cluster.get(i).appliesOnInstance((Integer v) -> StorageService.instance.getNaturalEndpointsWithPort(KEYSPACE, Int32Type.instance.decompose(v)))
|
||||
.apply(pk));
|
||||
}
|
||||
|
||||
private static void debugPaxosState(Cluster cluster, int pk)
|
||||
{
|
||||
TableId tableId = cluster.get(1).callOnInstance(() -> Keyspace.open(KEYSPACE).getColumnFamilyStore("tbl").metadata.id);
|
||||
for (int i = 1 ; i <= cluster.size() ; ++i)
|
||||
for (Object[] row : cluster.get(i).executeInternal("select in_progress_ballot, proposal_ballot, most_recent_commit_at from system.paxos where row_key = ? and cf_id = ?", Int32Type.instance.decompose(pk), tableId))
|
||||
System.out.println(i + ": " + (row[0] == null ? 0L : UUIDGen.microsTimestamp((UUID)row[0])) + ", " + (row[1] == null ? 0L : UUIDGen.microsTimestamp((UUID)row[1])) + ", " + (row[2] == null ? 0L : UUIDGen.microsTimestamp((UUID)row[2])));
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue