From 6522d52b0b0793a5ccbb1bbdd98ba69f5f199d99 Mon Sep 17 00:00:00 2001 From: Ariel Weisberg Date: Wed, 2 Oct 2024 18:15:39 -0400 Subject: [PATCH] Live migration for non-serial reads Patch by Ariel Weisberg; Reviewed by David Capwell for CASSANDRA-19439 --- build.xml | 1 + ide/idea/workspace.xml | 1 + .../cassandra/batchlog/BatchlogManager.java | 5 +- .../config/CassandraRelevantProperties.java | 1 + .../cql3/statements/BatchStatement.java | 3 +- .../statements/BatchUpdatesCollector.java | 17 +- .../cql3/statements/CQL3CasRequest.java | 9 +- .../statements/ModificationStatement.java | 5 +- .../cql3/statements/SelectStatement.java | 33 +- ...eTableSinglePartitionUpdatesCollector.java | 9 +- .../SingleTableUpdatesCollector.java | 11 +- .../cql3/statements/TransactionStatement.java | 126 +++- .../cql3/statements/UpdatesCollector.java | 3 +- .../schema/AlterTableStatement.java | 2 +- .../statements/schema/CopyTableStatement.java | 2 +- .../schema/CreateTableStatement.java | 48 +- .../schema/CreateViewStatement.java | 26 +- .../statements/schema/TableAttributes.java | 25 +- .../apache/cassandra/db/CounterMutation.java | 5 +- .../org/apache/cassandra/db/IMutation.java | 5 +- .../cassandra/db/MutableDeletionInfo.java | 5 +- .../org/apache/cassandra/db/Mutation.java | 63 +- .../cassandra/db/MutationVerbHandler.java | 5 +- .../db/PartitionRangeReadCommand.java | 88 ++- .../cassandra/db/PartitionRangeReadQuery.java | 6 +- .../cassandra/db/RangeTombstoneList.java | 15 +- .../org/apache/cassandra/db/ReadCommand.java | 67 +- .../cassandra/db/ReadCommandVerbHandler.java | 11 +- .../org/apache/cassandra/db/ReadResponse.java | 42 +- .../apache/cassandra/db/SimpleBuilders.java | 11 +- .../db/SinglePartitionReadCommand.java | 103 +-- .../db/SinglePartitionReadQuery.java | 7 +- .../db/partitions/PartitionIterators.java | 10 +- .../db/partitions/PartitionUpdate.java | 6 +- .../UnfilteredPartitionIterators.java | 16 +- .../cassandra/db/rows/AbstractCell.java | 4 +- .../apache/cassandra/db/rows/BTreeRow.java | 2 +- .../apache/cassandra/db/rows/ColumnData.java | 4 +- .../cassandra/db/rows/ComplexColumnData.java | 4 +- .../org/apache/cassandra/db/rows/Row.java | 2 +- .../cassandra/db/virtual/VirtualMutation.java | 5 +- .../cassandra/dht/Murmur3Partitioner.java | 1 + .../cassandra/dht/NormalizedRanges.java | 8 +- src/java/org/apache/cassandra/dht/Range.java | 87 +++ .../cassandra/hints/HintsDispatcher.java | 22 +- .../composites/CompositesSearcher.java | 20 +- .../index/internal/keys/KeysSearcher.java | 16 +- .../index/sai/utils/IndexTermType.java | 2 +- .../cassandra/metrics/TableMetrics.java | 2 +- .../apache/cassandra/schema/TableParams.java | 4 + .../service/AbstractWriteResponseHandler.java | 3 +- .../cassandra/service/StorageProxy.java | 224 ++++-- .../cassandra/service/StorageService.java | 6 +- .../service/StorageServiceMBean.java | 3 +- .../service/accord/AccordCallback.java | 79 --- .../accord/AccordConfigurationService.java | 70 +- .../service/accord/AccordObjectSizes.java | 11 + .../accord/AccordResponseVerbHandler.java | 1 + .../service/accord/AccordService.java | 75 +- .../service/accord/AccordTopology.java | 22 +- .../service/accord/AccordVerbHandler.java | 36 +- .../service/accord/IAccordService.java | 20 +- .../cassandra/service/accord/TokenRange.java | 19 +- .../service/accord/api/AccordAgent.java | 4 +- .../service/accord/api/AccordRoutingKey.java | 4 +- .../accord/interop/AccordInteropAdapter.java | 2 +- .../interop/AccordInteropExecution.java | 185 ++--- .../accord/interop/AccordInteropRead.java | 143 +++- .../interop/AccordInteropReadCallback.java | 3 + .../interop/AccordInteropStableThenRead.java | 16 +- .../accord/serializers/KeySerializers.java | 46 ++ .../service/accord/txn/AbstractKeySorted.java | 145 +++- .../txn/RetryWithNewProtocolResult.java | 12 +- .../service/accord/txn/TxnKeyRead.java | 254 ------- .../service/accord/txn/TxnNamedRead.java | 241 ++++++- .../service/accord/txn/TxnQuery.java | 78 ++- .../service/accord/txn/TxnRangeRead.java | 387 ----------- .../cassandra/service/accord/txn/TxnRead.java | 328 +++++++-- .../service/accord/txn/TxnUpdate.java | 4 + .../service/accord/txn/TxnWrite.java | 30 +- .../service/consensus/TransactionalMode.java | 97 +-- .../migration/ConsensusKeyMigrationState.java | 38 +- .../ConsensusMigrationMutationHelper.java | 45 +- .../migration/ConsensusMigrationState.java | 13 +- .../migration/ConsensusRequestRouter.java | 613 ++++++++++++++++- .../migration/ConsensusTableMigration.java | 48 +- .../migration/TableMigrationState.java | 20 +- .../TransactionalMigrationFromMode.java | 8 +- .../cassandra/service/paxos/Commit.java | 3 +- .../service/reads/AbstractReadExecutor.java | 2 +- .../cassandra/service/reads/ReadCallback.java | 33 + .../service/reads/ReadCoordinator.java | 8 +- .../reads/ReplicaFilteringProtection.java | 3 +- .../reads/ShortReadPartitionsProtection.java | 2 +- .../reads/range/AccordRangeResponse.java | 10 +- .../reads/range/RangeCommandIterator.java | 235 +++++-- .../range/ScanAllRangesCommandIterator.java | 2 +- ...Response.java => SingleRangeResponse.java} | 12 +- .../reads/repair/AbstractReadRepair.java | 2 +- .../reads/repair/BlockingPartitionRepair.java | 6 +- .../reads/repair/BlockingReadRepair.java | 9 +- .../reads/repair/BlockingReadRepairs.java | 5 +- .../service/reads/repair/ReadRepair.java | 7 +- .../repair/RowIteratorMergeListener.java | 2 +- .../apache/cassandra/tcm/log/LogState.java | 1 - ...ginConsensusMigrationForTableAndRange.java | 17 +- .../nodetool/ConsensusMigrationAdmin.java | 5 +- .../apache/cassandra/utils/Collectors3.java | 15 + .../distributed/impl/InstanceConfig.java | 2 +- .../distributed/impl/MessageImpl.java | 3 +- .../distributed/shared/ClusterUtils.java | 15 +- .../distributed/test/QueriesTableTest.java | 28 +- .../distributed/test/ReadRepairTest.java | 16 +- .../test/ShortReadProtectionTest.java | 11 +- .../distributed/test/TestBaseImpl.java | 5 + .../test/accord/AccordCQLTestBase.java | 112 ++- .../test/accord/AccordDropKeyspaceTest.java | 7 +- .../test/accord/AccordDropTableTest.java | 7 +- .../accord/AccordIncrementalRepairTest.java | 2 +- .../test/accord/AccordInteropReadTest.java | 3 +- .../accord/AccordInteroperabilityTest.java | 83 --- .../test/accord/AccordMetricsTest.java | 90 +-- .../AccordMigrationReadRaceTestBase.java | 645 ++++++++++++++++++ .../test/accord/AccordMigrationTest.java | 16 +- ... => AccordMigrationWriteRaceTestBase.java} | 163 +++-- .../AccordReadInteroperabilityTest.java | 180 +++++ .../test/accord/AccordTestBase.java | 48 +- .../AccordWriteInteroperabilityTest.java | 225 ++++++ .../test/accord/InteropTokenRangeTest.java | 42 +- .../MigrationFromAccordReadRaceTest.java | 28 + ... => MigrationFromAccordWriteRaceTest.java} | 2 +- .../accord/MigrationToAccordReadRaceTest.java | 28 + ...va => MigrationToAccordWriteRaceTest.java} | 2 +- .../CoordinatorReadLatencyMetricTest.java | 3 +- .../distributed/util/QueryResultUtil.java | 14 +- .../topology/AccordTopologyMixupTest.java | 3 +- .../cassandra/simulator/SimulationRunner.java | 8 +- .../OnInstanceStartConsensusMigration.java | 2 +- .../paxos/PairOfSequencesPaxosSimulation.java | 6 +- .../paxos/PaxosClusterSimulation.java | 2 +- .../paxos/PaxosSimulationRunner.java | 4 +- .../test/ShortPaxosSimulationTest.java | 2 +- test/unit/org/apache/cassandra/Util.java | 5 + .../cql3/ColumnSpecificationTest.java | 3 + .../cql3/PreparedStatementsTest.java | 19 +- .../ReadCommandVerbHandlerOutOfRangeTest.java | 7 +- .../db/ReadCommandVerbHandlerTest.java | 2 +- .../apache/cassandra/db/ReadResponseTest.java | 2 +- .../org/apache/cassandra/dht/RangeTest.java | 305 ++++++++- .../cassandra/hints/HintsServiceTest.java | 2 +- .../index/accord/RouteIndexTest.java | 18 +- .../service/accord/AccordReadRepairTest.java | 10 +- .../service/accord/AccordTestUtils.java | 8 +- .../accord/txn/AbstractKeySortedTest.java | 14 +- .../service/reads/ReadExecutorTest.java | 4 +- .../repair/RepairedDataVerifierTest.java | 2 +- 156 files changed, 4986 insertions(+), 1909 deletions(-) delete mode 100644 src/java/org/apache/cassandra/service/accord/AccordCallback.java delete mode 100644 src/java/org/apache/cassandra/service/accord/txn/TxnKeyRead.java delete mode 100644 src/java/org/apache/cassandra/service/accord/txn/TxnRangeRead.java rename src/java/org/apache/cassandra/service/reads/range/{CassandraRangeResponse.java => SingleRangeResponse.java} (81%) create mode 100644 test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java rename test/distributed/org/apache/cassandra/distributed/test/accord/{AccordMigrationRaceTestBase.java => AccordMigrationWriteRaceTestBase.java} (83%) create mode 100644 test/distributed/org/apache/cassandra/distributed/test/accord/AccordReadInteroperabilityTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/accord/AccordWriteInteroperabilityTest.java create mode 100644 test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordReadRaceTest.java rename test/distributed/org/apache/cassandra/distributed/test/accord/{MigrationFromAccordRaceTest.java => MigrationFromAccordWriteRaceTest.java} (91%) create mode 100644 test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordReadRaceTest.java rename test/distributed/org/apache/cassandra/distributed/test/accord/{MigrationToAccordRaceTest.java => MigrationToAccordWriteRaceTest.java} (91%) diff --git a/build.xml b/build.xml index 3fe92490d4..d209b9af14 100644 --- a/build.xml +++ b/build.xml @@ -1239,6 +1239,7 @@ + diff --git a/ide/idea/workspace.xml b/ide/idea/workspace.xml index 08a8a73ae4..13018f4052 100644 --- a/ide/idea/workspace.xml +++ b/ide/idea/workspace.xml @@ -190,6 +190,7 @@ -Dcassandra.ring_delay_ms=10000 -Dcassandra.skip_sync=true -Dcassandra.storagedir=$PROJECT_DIR$/data + -Dcassandra.test.accord.allow_test_modes=true -Dcassandra.strict.runtime.checks=true -Dcassandra.test.flush_local_schema_changes=false -Dcassandra.test.messagingService.nonGracefulShutdown=true diff --git a/src/java/org/apache/cassandra/batchlog/BatchlogManager.java b/src/java/org/apache/cassandra/batchlog/BatchlogManager.java index 1844f84249..ff7f2d2cb3 100644 --- a/src/java/org/apache/cassandra/batchlog/BatchlogManager.java +++ b/src/java/org/apache/cassandra/batchlog/BatchlogManager.java @@ -31,7 +31,6 @@ 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; @@ -454,12 +453,12 @@ public class BatchlogManager implements BatchlogManagerMBean if (accordResult != null) { IAccordService accord = AccordService.instance(); - TxnResult.Kind kind = accord.getTxnResult(accordResult, true, ConsistencyLevel.QUORUM, accordTxnStart).kind(); + TxnResult.Kind kind = accord.getTxnResult(accordResult).kind(); if (kind == retry_new_protocol) throw new RetryOnDifferentSystemException(); } } - catch (WriteTimeoutException|WriteFailureException|RetryOnDifferentSystemException 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()); diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index bb7dc7c551..7459fecb8c 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -39,6 +39,7 @@ import static org.apache.cassandra.utils.LocalizeString.toUpperCaseLocalized; public enum CassandraRelevantProperties { ACCORD_AGENT_CLASS("cassandra.test.accord.agent"), + ACCORD_ALLOW_TEST_MODES("cassandra.test.accord.allow_test_modes", "false"), ACCORD_KEY_PARANOIA_COSTFACTOR(Invariants.KEY_PARANOIA_COSTFACTOR), ACCORD_KEY_PARANOIA_CPU(Invariants.KEY_PARANOIA_CPU), ACCORD_KEY_PARANOIA_MEMORY(Invariants.KEY_PARANOIA_MEMORY), diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java index 45c433a530..d12f9e8922 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchStatement.java @@ -53,6 +53,7 @@ import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.Slice; import org.apache.cassandra.db.Slices; @@ -356,7 +357,7 @@ public class BatchStatement implements CQLStatement.CompositeCQLStatement } // 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); + return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW); } /** diff --git a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java index 49b5a404dd..4bc0d909d2 100644 --- a/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/BatchUpdatesCollector.java @@ -32,6 +32,7 @@ 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.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -137,14 +138,14 @@ final class BatchUpdatesCollector implements UpdatesCollector * @return a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, boolean allowPotentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) { List ms = new ArrayList<>(); for (Map ksMap : mutationBuilders.values()) { for (IMutationBuilder builder : ksMap.values()) { - IMutation mutation = builder.build(allowPotentialTxnConflicts); + IMutation mutation = builder.build(potentialTxnConflicts); mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); ms.add(mutation); @@ -182,7 +183,7 @@ final class BatchUpdatesCollector implements UpdatesCollector /** * Build the immutable mutation */ - IMutation build(boolean allowPotentialTxnConflicts); + IMutation build(PotentialTxnConflicts potentialTxnConflicts); /** * Get the builder for the given tableId @@ -215,7 +216,7 @@ final class BatchUpdatesCollector implements UpdatesCollector return this; } - public Mutation build(boolean allowPotentialTxnConflicts) + public Mutation build(PotentialTxnConflicts potentialTxnConflicts) { ImmutableMap.Builder updates = new ImmutableMap.Builder<>(); for (Map.Entry updateEntry : modifications.entrySet()) @@ -223,7 +224,7 @@ final class BatchUpdatesCollector implements UpdatesCollector PartitionUpdate update = updateEntry.getValue().build(); updates.put(updateEntry.getKey(), update); } - return new Mutation(keyspaceName, key, updates.build(), createdAt, allowPotentialTxnConflicts); + return new Mutation(keyspaceName, key, updates.build(), createdAt, potentialTxnConflicts); } public PartitionUpdate.Builder get(TableId tableId) @@ -263,9 +264,9 @@ final class BatchUpdatesCollector implements UpdatesCollector return mutationBuilder.add(builder); } - public IMutation build(boolean allowPotentialTxnConflicts) + public IMutation build(PotentialTxnConflicts potentialTxnConflicts) { - return new CounterMutation(mutationBuilder.build(allowPotentialTxnConflicts), cl); + return new CounterMutation(mutationBuilder.build(potentialTxnConflicts), cl); } public PartitionUpdate.Builder get(TableId id) @@ -297,7 +298,7 @@ final class BatchUpdatesCollector implements UpdatesCollector } @Override - public VirtualMutation build(boolean allowPotentialTxnConflicts) + public VirtualMutation build(PotentialTxnConflicts potentialTxnConflicts) { ImmutableMap.Builder updates = new ImmutableMap.Builder<>(); modifications.forEach((tableId, updateBuilder) -> updates.put(tableId, updateBuilder.build())); diff --git a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java index c315bd04a8..03a9876efa 100644 --- a/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java +++ b/src/java/org/apache/cassandra/cql3/statements/CQL3CasRequest.java @@ -63,8 +63,8 @@ import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.accord.txn.TxnCondition; import org.apache.cassandra.service.accord.txn.TxnData; import org.apache.cassandra.service.accord.txn.TxnDataKeyValue; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; import org.apache.cassandra.service.accord.txn.TxnQuery; +import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.service.accord.txn.TxnReference; import org.apache.cassandra.service.accord.txn.TxnResult; import org.apache.cassandra.service.accord.txn.TxnUpdate; @@ -83,7 +83,6 @@ import static org.apache.cassandra.service.accord.txn.TxnData.txnDataName; import static org.apache.cassandra.service.accord.txn.TxnResult.Kind.retry_new_protocol; import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.getTableMetadata; - /** * Processed CAS conditions and update on potentially multiple rows of the same partition. */ @@ -504,8 +503,8 @@ public class CQL3CasRequest implements CASRequest // If the write strategy is sending all writes through Accord there is no need to use the supplied consistency // level since Accord will manage reading safely TableParams tableParams = getTableMetadata(cm, metadata.id).params; - consistencyLevel = tableParams.transactionalMode.readCLForStrategy(tableParams.transactionalMigrationFrom, consistencyLevel, cm, metadata.id, readCommand.partitionKey().getToken()); - TxnKeyRead read = TxnKeyRead.createCasRead(readCommand, consistencyLevel); + consistencyLevel = tableParams.transactionalMode.readCLForMode(tableParams.transactionalMigrationFrom, consistencyLevel, cm, metadata.id, readCommand.partitionKey().getToken()); + TxnRead read = TxnRead.createCasRead(readCommand, consistencyLevel); // In a CAS requesting only one key is supported and writes // can't be dependent on any data that is read (only conditions) // so the only relevant keys are the read key @@ -518,7 +517,7 @@ public class CQL3CasRequest implements CASRequest // since it is safe to match what non-SERIAL writes do TableMetadata tableMetadata = getTableMetadata(cm, metadata.id); TableParams tableParams = tableMetadata.params; - commitConsistencyLevel = tableParams.transactionalMode.commitCLForStrategy(tableParams.transactionalMigrationFrom, commitConsistencyLevel, cm, metadata.id, key.getToken()); + commitConsistencyLevel = tableParams.transactionalMode.commitCLForMode(tableParams.transactionalMigrationFrom, commitConsistencyLevel, cm, metadata.id, key.getToken()); // CAS requires using the new txn timestamp to correctly linearize some kinds of updates return new TxnUpdate(createWriteFragments(clientState), createCondition(), commitConsistencyLevel, false); } diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index a02cd27b4e..cecf839e8b 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -73,6 +73,7 @@ import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; import org.apache.cassandra.db.Keyspace; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.SinglePartitionReadCommand; @@ -870,7 +871,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa SingleTableSinglePartitionUpdatesCollector collector = new SingleTableSinglePartitionUpdatesCollector(metadata, updatedColumns); addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime, constructingAccordBaseUpdate); // local means this is test or internal things that are bypassing distributed system modification/checks - return collector.toMutations(state, local); + return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW); } else { @@ -878,7 +879,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa SingleTableUpdatesCollector collector = new SingleTableUpdatesCollector(metadata, updatedColumns, perPartitionKeyCounts); addUpdates(collector, keys, state, options, local, timestamp, nowInSeconds, requestTime, constructingAccordBaseUpdate); // local means this is test or internal things that are bypassing distributed system modification/checks - return collector.toMutations(state, local); + return collector.toMutations(state, local ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index 45f2499af7..7f025cf02f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -79,6 +79,7 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.db.PartitionRangeReadQuery; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.ReadQuery; import org.apache.cassandra.db.SinglePartitionReadCommand; @@ -386,7 +387,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, } } - ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), nowInSec, limit); + ReadQuery query = getQuery(options, state.getClientState(), selectors.getColumnFilter(), nowInSec, limit, PotentialTxnConflicts.DISALLOW); if (options.isReadThresholdsEnabled()) query.trackWarnings(); @@ -437,7 +438,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, getLimit(options), getPerPartitionLimit(options), options.getPageSize(), - getAggregationSpec(options)); + getAggregationSpec(options), + PotentialTxnConflicts.DISALLOW); } public ReadQuery getQuery(QueryOptions options, @@ -447,18 +449,20 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, int userLimit, int perPartitionLimit, int pageSize, - AggregationSpecification aggregationSpec) + AggregationSpecification aggregationSpec, + PotentialTxnConflicts potentialTxnConflicts) { DataLimits limit = getDataLimits(userLimit, perPartitionLimit, pageSize, aggregationSpec); - return getQuery(options, state, columnFilter, nowInSec, limit); + return getQuery(options, state, columnFilter, nowInSec, limit, potentialTxnConflicts); } public ReadQuery getQuery(QueryOptions options, ClientState state, ColumnFilter columnFilter, long nowInSec, - DataLimits limit) + DataLimits limit, + PotentialTxnConflicts potentialTxnConflicts) { RowFilter rowFilter = getRowFilter(options, state); @@ -467,13 +471,13 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, if (restrictions.usesSecondaryIndexing() && !SchemaConstants.isLocalSystemKeyspace(table.keyspace)) Guardrails.nonPartitionRestrictedIndexQueryEnabled.ensureEnabled(state); - return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec); + return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec, potentialTxnConflicts); } if (restrictions.usesSecondaryIndexing() && !rowFilter.isStrict()) - return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec); + return getRangeCommand(options, state, columnFilter, rowFilter, limit, nowInSec, potentialTxnConflicts); - return getSliceCommands(options, state, columnFilter, rowFilter, limit, nowInSec); + return getSliceCommands(options, state, columnFilter, rowFilter, limit, nowInSec, potentialTxnConflicts); } private ResultMessage.Rows execute(ReadQuery query, @@ -657,7 +661,8 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, userLimit, userPerPartitionLimit, pageSize, - aggregationSpec); + aggregationSpec, + PotentialTxnConflicts.ALLOW); try (ReadExecutionController executionController = query.executionController()) { @@ -704,7 +709,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, throw new IllegalStateException(); Selectors selectors = selection.newSelectors(options); - ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, Integer.MAX_VALUE, null); + ReadQuery query = getQuery(options, state, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, Integer.MAX_VALUE, null, PotentialTxnConflicts.ALLOW); Map> result = Collections.emptyMap(); try (ReadExecutionController executionController = query.executionController()) @@ -778,7 +783,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, } private ReadQuery getSliceCommands(QueryOptions options, ClientState state, ColumnFilter columnFilter, - RowFilter rowFilter, DataLimits limit, long nowInSec) + RowFilter rowFilter, DataLimits limit, long nowInSec, PotentialTxnConflicts potentialTxnConflicts) { Collection keys = restrictions.getPartitionKeys(options, state); if (keys.isEmpty()) @@ -801,7 +806,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, } SinglePartitionReadQuery.Group group = - SinglePartitionReadQuery.createGroup(table, nowInSec, columnFilter, rowFilter, limit, decoratedKeys, filter); + SinglePartitionReadQuery.createGroup(table, nowInSec, columnFilter, rowFilter, limit, decoratedKeys, filter, potentialTxnConflicts); // If there's a secondary index that the commands can use, have it validate the request parameters. group.maybeValidateIndex(); @@ -855,7 +860,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, } private ReadQuery getRangeCommand(QueryOptions options, ClientState state, ColumnFilter columnFilter, - RowFilter rowFilter, DataLimits limit, long nowInSec) + RowFilter rowFilter, DataLimits limit, long nowInSec, PotentialTxnConflicts potentialTxnConflicts) { ClusteringIndexFilter clusteringIndexFilter = makeClusteringIndexFilter(options, state, columnFilter); if (clusteringIndexFilter == null) @@ -868,7 +873,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement, return ReadQuery.empty(table); ReadQuery command = - PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter)); + PartitionRangeReadQuery.create(table, nowInSec, columnFilter, rowFilter, limit, new DataRange(keyBounds, clusteringIndexFilter), potentialTxnConflicts); // If there's a secondary index that the command can use, have it validate the request parameters. command.maybeValidateIndex(); diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java index 6664c09e2c..eba1c27bfa 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleTableSinglePartitionUpdatesCollector.java @@ -25,6 +25,7 @@ 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.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -78,16 +79,16 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect * Returns a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, boolean allowPotentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) { // 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, allowPotentialTxnConflicts)); + return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts)); } - private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, boolean allowPotentialTxnConflicts) + private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts) { IMutation mutation; @@ -96,7 +97,7 @@ final class SingleTableSinglePartitionUpdatesCollector implements UpdatesCollect else if (metadata.isCounter()) mutation = new CounterMutation(new Mutation(builder.build()), counterConsistencyLevel); else - mutation = new Mutation(builder.build(), allowPotentialTxnConflicts); + mutation = new Mutation(builder.build(), potentialTxnConflicts); mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java index c2497360a7..b2570e9d70 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleTableUpdatesCollector.java @@ -31,6 +31,7 @@ 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.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.commitlog.CommitLogSegment; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -95,24 +96,24 @@ final class SingleTableUpdatesCollector implements UpdatesCollector * @return a collection containing all the mutations. */ @Override - public List toMutations(ClientState state, boolean allowPotentialTxnConflicts) + public List toMutations(ClientState state, PotentialTxnConflicts potentialTxnConflicts) { if (puBuilders.size() == 1) { PartitionUpdate.Builder builder = puBuilders.values().iterator().next(); - return Collections.singletonList(createMutation(state, builder, allowPotentialTxnConflicts)); + return Collections.singletonList(createMutation(state, builder, potentialTxnConflicts)); } List ms = new ArrayList<>(puBuilders.size()); for (PartitionUpdate.Builder builder : puBuilders.values()) { - IMutation mutation = createMutation(state, builder, allowPotentialTxnConflicts); + IMutation mutation = createMutation(state, builder, potentialTxnConflicts); ms.add(mutation); } return ms; } - private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, boolean allowPotentialTxnConflicts) + private IMutation createMutation(ClientState state, PartitionUpdate.Builder builder, PotentialTxnConflicts potentialTxnConflicts) { IMutation mutation; @@ -121,7 +122,7 @@ final class SingleTableUpdatesCollector implements UpdatesCollector else if (metadata.isCounter()) mutation = new CounterMutation(new Mutation(builder.build()), counterConsistencyLevel); else - mutation = new Mutation(builder.build(), allowPotentialTxnConflicts); + mutation = new Mutation(builder.build(), potentialTxnConflicts); mutation.validateIndexedColumns(state); mutation.validateSize(MessagingService.current_version, CommitLogSegment.ENTRY_OVERHEAD_SIZE); diff --git a/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java b/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java index b95c1faa4a..f61c394954 100644 --- a/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/TransactionStatement.java @@ -40,6 +40,7 @@ import com.google.common.collect.Iterables; import accord.api.Key; import accord.primitives.Keys; +import accord.primitives.Routable.Domain; import accord.primitives.Txn; import org.agrona.collections.Int2ObjectHashMap; import org.apache.cassandra.audit.AuditLogContext; @@ -56,34 +57,43 @@ import org.apache.cassandra.cql3.transactions.ConditionStatement; import org.apache.cassandra.cql3.transactions.ReferenceOperation; import org.apache.cassandra.cql3.transactions.RowDataReference; import org.apache.cassandra.cql3.transactions.SelectReferenceSource; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.SinglePartitionReadQuery; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.schema.ColumnMetadata; import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; import org.apache.cassandra.service.accord.AccordService; import org.apache.cassandra.service.accord.api.AccordRoutableKey; +import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.txn.AccordUpdate; import org.apache.cassandra.service.accord.txn.TxnCondition; import org.apache.cassandra.service.accord.txn.TxnData; import org.apache.cassandra.service.accord.txn.TxnDataKeyValue; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; import org.apache.cassandra.service.accord.txn.TxnNamedRead; import org.apache.cassandra.service.accord.txn.TxnQuery; +import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.service.accord.txn.TxnReference; 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.TransactionalMigrationFromMode; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.transport.messages.ResultMessage; import org.apache.cassandra.utils.FBUtilities; import static accord.primitives.Txn.Kind.Read; +import static com.google.common.base.Preconditions.checkArgument; import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse; import static org.apache.cassandra.cql3.statements.RequestValidations.checkNotNull; import static org.apache.cassandra.cql3.statements.RequestValidations.checkTrue; @@ -92,8 +102,9 @@ import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.AU import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.RETURNING; import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.USER; import static org.apache.cassandra.service.accord.txn.TxnData.txnDataName; -import static org.apache.cassandra.service.accord.txn.TxnKeyRead.createTxnRead; +import static org.apache.cassandra.service.accord.txn.TxnRead.createTxnRead; import static org.apache.cassandra.service.accord.txn.TxnResult.Kind.retry_new_protocol; +import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.getTableMetadata; import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.shouldReadEphemerally; public class TransactionStatement implements CQLStatement.CompositeCQLStatement, CQLStatement.ReturningCQLStatement @@ -261,8 +272,8 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, for (NamedSelect select : assignments) { TxnNamedRead read = createNamedRead(select, options, state); + keyConsumer.accept((Key)read.key()); minEpoch = Math.max(minEpoch, read.command().metadata().epoch.getEpoch()); - keyConsumer.accept(read.key()); reads.add(read); } @@ -270,7 +281,7 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, { for (TxnNamedRead read : createNamedReads(returningSelect, options, state)) { - keyConsumer.accept(read.key()); + keyConsumer.accept((Key)read.key()); minEpoch = Math.max(minEpoch, read.command().metadata().epoch.getEpoch()); reads.add(read); } @@ -279,8 +290,11 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, if (autoReads != null) { for (NamedSelect select : autoReads.values()) - // don't need keyConsumer as the keys are known to exist due to Modification - reads.add(createNamedRead(select, options, state)); + { + TxnNamedRead read = createNamedRead(select, options, state); + keyConsumer.accept((Key)read.key()); + reads.add(read); + } } return reads; @@ -301,15 +315,15 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, return new TxnCondition.BooleanGroup(TxnCondition.Kind.AND, result); } - List createWriteFragments(ClientState state, QueryOptions options, Map autoReads, Consumer keyConsumer) + List createWriteFragments(ClientState state, QueryOptions options, Map autoReads, Set keys) { List fragments = new ArrayList<>(updates.size()); int idx = 0; for (ModificationStatement modification : updates) { TxnWrite.Fragment fragment = modification.getTxnWriteFragment(idx, state, options); + keys.add(fragment.key); minEpoch = Math.max(minEpoch, fragment.baseUpdate.metadata().epoch.getEpoch()); - keyConsumer.accept(fragment.key); fragments.add(fragment); if (modification.allReferenceOperations().stream().anyMatch(ReferenceOperation::requiresRead)) @@ -325,9 +339,12 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, return fragments; } - AccordUpdate createUpdate(ClientState state, QueryOptions options, Map autoReads, Consumer keyConsumer) + AccordUpdate createUpdate(ClusterMetadata cm, ClientState state, QueryOptions options, Map autoReads, Set keys) { - return new TxnUpdate(createWriteFragments(state, options, autoReads, keyConsumer), createCondition(options), null, false); + checkArgument(keys.isEmpty(), "Construct update before reads so the key set can be used to determine commit consistency level"); + List writeFragments = createWriteFragments(state, options, autoReads, keys); + ConsistencyLevel commitCL = consistencyLevelForAccordCommit(cm, keys, options.getConsistency()); + return new TxnUpdate(writeFragments, createCondition(options), commitCL, false); } Keys toKeys(SortedSet keySet) @@ -335,10 +352,90 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, return new Keys(keySet); } + private ConsistencyLevel consistencyLevelForAccordRead(ClusterMetadata cm, Set keys, @Nullable ConsistencyLevel consistencyLevel) + { + // Write transactions are read/write so it creates a read and ends up needing a consistency level + // which is fine to leave null + if (keys.isEmpty()) + return null; + + // Null means no specific consistency behavior is required from Accord, it's functionally similar to + // reading at ONE if you are reading data that wasn't written via Accord + if (consistencyLevel == null) + return null; + + for (Key key : keys) + { + // readCLForMode should return either null or the supplied consistency level + // in which case we will read everything at that CL since Accord doesn't support per table + // read consistency + ConsistencyLevel readCL = consistencyLevelForAccordRead(cm, key, consistencyLevel); + if (readCL != null) + return readCL; + } + return null; + } + + private ConsistencyLevel consistencyLevelForAccordRead(ClusterMetadata cm, Key key, ConsistencyLevel consistencyLevel) + { + // Null means no specific consistency behavior is required from Accord, it's functionally similar to + // reading at ONE if you are reading data that wasn't written via Accord + if (consistencyLevel == null) + return null; + + PartitionKey pk = (PartitionKey)key; + TableId tableId = pk.table(); + Token token = pk.token(); + TableParams tableParams = getTableMetadata(cm, tableId).params; + TransactionalMode mode = tableParams.transactionalMode; + TransactionalMigrationFromMode migrationFromMode = tableParams.transactionalMigrationFrom; + return mode.readCLForMode(migrationFromMode, consistencyLevel, cm, tableId, token); + } + + private static ConsistencyLevel consistencyLevelForAccordCommit(ClusterMetadata cm, Set keys, @Nullable ConsistencyLevel consistencyLevel) + { + checkArgument(!keys.isEmpty(), "keys should not be empty"); + // 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 (Key key : keys) + { + // commitCLForMode 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 = consistencyLevelForAccordCommit(cm, key, consistencyLevel); + if (commitCL != null) + return commitCL; + } + return null; + } + + private static ConsistencyLevel consistencyLevelForAccordCommit(ClusterMetadata cm, Key key, @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; + + PartitionKey pk = (PartitionKey)key; + TableId tableId = pk.table(); + Token token = pk.token(); + TableParams tableParams = getTableMetadata(cm, tableId).params; + TransactionalMode mode = tableParams.transactionalMode; + TransactionalMigrationFromMode migrationFromMode = tableParams.transactionalMigrationFrom; + // commitCLForMode 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 + return mode.commitCLForMode(migrationFromMode, consistencyLevel, cm, tableId, token); + } + @VisibleForTesting public Txn createTxn(ClientState state, QueryOptions options) { SortedSet keySet = new TreeSet<>(); + ClusterMetadata cm = ClusterMetadata.current(); if (updates.isEmpty()) { @@ -346,18 +443,17 @@ public class TransactionStatement implements CQLStatement.CompositeCQLStatement, Preconditions.checkState(conditions.isEmpty(), "No condition should exist without updates present"); List reads = createNamedReads(options, state, null, keySet::add); Keys txnKeys = toKeys(keySet); - TxnKeyRead read = createTxnRead(reads, null); + TxnRead read = createTxnRead(reads, consistencyLevelForAccordRead(cm, keySet, options.getSerialConsistency()), Domain.Key); Txn.Kind kind = shouldReadEphemerally(txnKeys, Schema.instance.getTableMetadata(((AccordRoutableKey) txnKeys.get(0)).table()).params, Read); return new Txn.InMemory(kind, txnKeys, read, TxnQuery.ALL, null); } else { Int2ObjectHashMap autoReads = new Int2ObjectHashMap<>(); - AccordUpdate update = createUpdate(state, options, autoReads, keySet::add); + AccordUpdate update = createUpdate(cm, state, options, autoReads, keySet); List reads = createNamedReads(options, state, autoReads, keySet::add); - Keys txnKeys = toKeys(keySet); - TxnKeyRead read = createTxnRead(reads, null); - return new Txn.InMemory(txnKeys, read, TxnQuery.ALL, update); + TxnRead read = createTxnRead(reads, null, Domain.Key); + return new Txn.InMemory(toKeys(keySet), read, TxnQuery.ALL, update); } } diff --git a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java index de19a8e567..a3867b608f 100644 --- a/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java +++ b/src/java/org/apache/cassandra/cql3/statements/UpdatesCollector.java @@ -23,6 +23,7 @@ import java.util.List; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; @@ -30,5 +31,5 @@ import org.apache.cassandra.service.ClientState; public interface UpdatesCollector { PartitionUpdate.Builder getPartitionUpdateBuilder(TableMetadata metadata, DecoratedKey dk, ConsistencyLevel consistency); - List toMutations(ClientState state, boolean allowPotentialTxnConflicts); + List toMutations(ClientState state, PotentialTxnConflicts allowPotentialTxnConflicts); } diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java index 2475e1dcaa..8c8ae8b45c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java @@ -595,7 +595,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement MemtableParams.get(attrs.getString(TableParams.Option.MEMTABLE.toString())); Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state); - validateDefaultTimeToLive(attrs.asNewTableParams()); + validateDefaultTimeToLive(attrs.asNewTableParams(keyspaceName)); } private TableParams validateAndUpdateTransactionalMigration(boolean isCounter, TableParams prev, TableParams next) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java index 3a79311dc7..c5b38e5e08 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CopyTableStatement.java @@ -236,7 +236,7 @@ public final class CopyTableStatement extends AlterSchemaStatement .sum(); Guardrails.tables.guard(totalUserTables + 1, targetTableName, false, state); } - validateDefaultTimeToLive(attrs.asNewTableParams()); + validateDefaultTimeToLive(attrs.asNewTableParams(keyspaceName)); } private void maybeCopyIndexes(TableMetadata.Builder builder, TableMetadata sourceTableMeta, KeyspaceMetadata targetKeyspaceMeta) diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java index 711af0c7ee..2e25291ed8 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java @@ -17,13 +17,20 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; - import javax.annotation.Nullable; import com.google.common.collect.ImmutableSet; - import org.apache.commons.lang3.StringUtils; import org.apache.cassandra.audit.AuditLogContext; @@ -32,26 +39,45 @@ import org.apache.cassandra.auth.DataResource; import org.apache.cassandra.auth.IResource; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.*; import org.apache.cassandra.cql3.constraints.ColumnConstraints; +import org.apache.cassandra.cql3.CQL3Type; +import org.apache.cassandra.cql3.CQLFragmentParser; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.CqlParser; +import org.apache.cassandra.cql3.QualifiedName; import org.apache.cassandra.cql3.functions.masking.ColumnMask; import org.apache.cassandra.db.guardrails.Guardrails; -import org.apache.cassandra.db.marshal.*; +import org.apache.cassandra.db.marshal.AbstractType; +import org.apache.cassandra.db.marshal.BytesType; +import org.apache.cassandra.db.marshal.CounterColumnType; +import org.apache.cassandra.db.marshal.EmptyType; +import org.apache.cassandra.db.marshal.ReversedType; +import org.apache.cassandra.db.marshal.UTF8Type; +import org.apache.cassandra.db.marshal.UserType; import org.apache.cassandra.exceptions.AlreadyExistsException; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.MemtableParams; +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.schema.TableParams; +import org.apache.cassandra.schema.Types; +import org.apache.cassandra.schema.UserFunctions; import org.apache.cassandra.service.ClientState; -import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; +import static com.google.common.collect.Iterables.concat; import static java.lang.String.format; import static java.util.Comparator.comparing; -import static com.google.common.collect.Iterables.concat; - public final class CreateTableStatement extends AlterSchemaStatement { private final String tableName; @@ -189,7 +215,7 @@ public final class CreateTableStatement extends AlterSchemaStatement if (useCompactStorage) Guardrails.compactTablesEnabled.ensureEnabled(state); - validateDefaultTimeToLive(attrs.asNewTableParams()); + validateDefaultTimeToLive(attrs.asNewTableParams(keyspaceName)); rawColumns.forEach((name, raw) -> raw.validate(state, name)); } @@ -224,7 +250,7 @@ public final class CreateTableStatement extends AlterSchemaStatement public TableMetadata.Builder builder(Types types, UserFunctions functions) { attrs.validate(); - TableParams params = attrs.asNewTableParams(); + TableParams params = attrs.asNewTableParams(keyspaceName); // use a TreeMap to preserve ordering across JDK versions (see CASSANDRA-9492) - important for stable unit tests Map columns = new TreeMap<>(comparing(o -> o.bytes)); diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java index e68e0c003d..284edf112b 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java @@ -17,7 +17,12 @@ */ package org.apache.cassandra.cql3.statements.schema; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; @@ -27,7 +32,11 @@ import org.apache.cassandra.audit.AuditLogContext; import org.apache.cassandra.audit.AuditLogEntryType; import org.apache.cassandra.auth.Permission; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.cql3.*; +import org.apache.cassandra.cql3.CQLStatement; +import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.cql3.QualifiedName; +import org.apache.cassandra.cql3.VariableSpecifications; +import org.apache.cassandra.cql3.WhereClause; import org.apache.cassandra.cql3.restrictions.StatementRestrictions; import org.apache.cassandra.cql3.selection.RawSelector; import org.apache.cassandra.cql3.selection.Selectable; @@ -38,19 +47,24 @@ import org.apache.cassandra.db.marshal.ReversedType; import org.apache.cassandra.db.view.View; import org.apache.cassandra.exceptions.AlreadyExistsException; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.ColumnMetadata; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; +import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.schema.ViewMetadata; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Event.SchemaChange; import org.apache.cassandra.transport.Event.SchemaChange.Change; import org.apache.cassandra.transport.Event.SchemaChange.Target; -import static java.lang.String.join; - import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.transform; +import static java.lang.String.join; import static org.apache.cassandra.config.CassandraRelevantProperties.MV_ALLOW_FILTERING_NONKEY_COLUMNS_UNSAFE; public final class CreateViewStatement extends AlterSchemaStatement @@ -334,7 +348,7 @@ public final class CreateViewStatement extends AlterSchemaStatement else if (!builder.hasId()) builder.id(TableId.get(metadata)); - builder.params(attrs.asNewTableParams()) + builder.params(attrs.asNewTableParams(keyspaceName)) .kind(TableMetadata.Kind.VIEW); partitionKeyColumns.stream() diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java index eb9c492798..2e643e5472 100644 --- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java +++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java @@ -31,6 +31,8 @@ import org.apache.cassandra.schema.CachingParams; import org.apache.cassandra.schema.CompactionParams; import org.apache.cassandra.schema.CompressionParams; import org.apache.cassandra.schema.MemtableParams; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.schema.TableParams.Option; @@ -41,7 +43,24 @@ import org.apache.cassandra.service.reads.SpeculativeRetryPolicy; import org.apache.cassandra.service.reads.repair.ReadRepairStrategy; import static java.lang.String.format; -import static org.apache.cassandra.schema.TableParams.Option.*; +import static org.apache.cassandra.schema.TableParams.Option.ADDITIONAL_WRITE_POLICY; +import static org.apache.cassandra.schema.TableParams.Option.ALLOW_AUTO_SNAPSHOT; +import static org.apache.cassandra.schema.TableParams.Option.BLOOM_FILTER_FP_CHANCE; +import static org.apache.cassandra.schema.TableParams.Option.CACHING; +import static org.apache.cassandra.schema.TableParams.Option.CDC; +import static org.apache.cassandra.schema.TableParams.Option.COMMENT; +import static org.apache.cassandra.schema.TableParams.Option.COMPACTION; +import static org.apache.cassandra.schema.TableParams.Option.COMPRESSION; +import static org.apache.cassandra.schema.TableParams.Option.CRC_CHECK_CHANCE; +import static org.apache.cassandra.schema.TableParams.Option.DEFAULT_TIME_TO_LIVE; +import static org.apache.cassandra.schema.TableParams.Option.GC_GRACE_SECONDS; +import static org.apache.cassandra.schema.TableParams.Option.INCREMENTAL_BACKUPS; +import static org.apache.cassandra.schema.TableParams.Option.MAX_INDEX_INTERVAL; +import static org.apache.cassandra.schema.TableParams.Option.MEMTABLE_FLUSH_PERIOD_IN_MS; +import static org.apache.cassandra.schema.TableParams.Option.MIN_INDEX_INTERVAL; +import static org.apache.cassandra.schema.TableParams.Option.READ_REPAIR; +import static org.apache.cassandra.schema.TableParams.Option.SPECULATIVE_RETRY; +import static org.apache.cassandra.schema.TableParams.Option.TRANSACTIONAL_MODE; public final class TableAttributes extends PropertyDefinitions { @@ -65,10 +84,10 @@ public final class TableAttributes extends PropertyDefinitions build(TableParams.builder()).validate(); } - TableParams asNewTableParams() + TableParams asNewTableParams(String keyspaceName) { TableParams.Builder builder = TableParams.builder(); - if (!hasOption(TRANSACTIONAL_MODE)) + if (!hasOption(TRANSACTIONAL_MODE) && !SchemaConstants.isSystemKeyspace(keyspaceName) && Schema.instance.distributedKeyspaces().names().contains(keyspaceName)) builder.transactionalMode(DatabaseDescriptor.defaultTransactionalMode()); return build(builder); } diff --git a/src/java/org/apache/cassandra/db/CounterMutation.java b/src/java/org/apache/cassandra/db/CounterMutation.java index 6935b09452..c5ed2364be 100644 --- a/src/java/org/apache/cassandra/db/CounterMutation.java +++ b/src/java/org/apache/cassandra/db/CounterMutation.java @@ -36,6 +36,7 @@ import com.google.common.collect.PeekingIterator; import com.google.common.util.concurrent.Striped; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.context.CounterContext; import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; import org.apache.cassandra.db.filter.ColumnFilter; @@ -189,9 +190,9 @@ public class CounterMutation implements IMutation * anyways and it's safe to continue non-transactionally updating them */ @Override - public boolean allowsPotentialTransactionConflicts() + public PotentialTxnConflicts potentialTxnConflicts() { - return true; + return PotentialTxnConflicts.ALLOW; } private void grabCounterLocks(Keyspace keyspace, List locks) throws WriteTimeoutException diff --git a/src/java/org/apache/cassandra/db/IMutation.java b/src/java/org/apache/cassandra/db/IMutation.java index 1e77ee7fc8..282db28916 100644 --- a/src/java/org/apache/cassandra/db/IMutation.java +++ b/src/java/org/apache/cassandra/db/IMutation.java @@ -24,6 +24,7 @@ import java.util.function.Supplier; import javax.annotation.Nullable; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.ClientState; @@ -84,9 +85,9 @@ public interface IMutation * like Accord that can't safely read data that is written non-transactionally. * */ - default boolean allowsPotentialTransactionConflicts() + default PotentialTxnConflicts potentialTxnConflicts() { - return false; + return PotentialTxnConflicts.DISALLOW; } // Construct replacement mutation that is identical except it only includes updates for the specified tables diff --git a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java index f9636909c4..3bbfe163b3 100644 --- a/src/java/org/apache/cassandra/db/MutableDeletionInfo.java +++ b/src/java/org/apache/cassandra/db/MutableDeletionInfo.java @@ -23,7 +23,8 @@ import java.util.Iterator; import com.google.common.base.Objects; import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.db.rows.RangeTombstoneMarker; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.memory.ByteBufferCloner; @@ -230,7 +231,7 @@ public class MutableDeletionInfo implements DeletionInfo return this; } - public DeletionInfo updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime) + public DeletionInfo updateAllTimestampAndLocalDeletionTime(long timestamp, long localDeletionTime) { if (partitionDeletion.markedForDeleteAt() != Long.MIN_VALUE) partitionDeletion = DeletionTime.build(timestamp, localDeletionTime); diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java index f7afb8d568..441163c8bd 100644 --- a/src/java/org/apache/cassandra/db/Mutation.java +++ b/src/java/org/apache/cassandra/db/Mutation.java @@ -39,6 +39,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.commitlog.CommitLog; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.DeserializationHelper; @@ -98,31 +99,31 @@ public class Mutation implements IMutation, Supplier // 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; + private PotentialTxnConflicts potentialTxnConflicts; public Mutation(PartitionUpdate update) { - this(update, false); + this(update, PotentialTxnConflicts.DISALLOW); } - public Mutation(PartitionUpdate update, boolean allowPotentialTransactionConflicts) + public Mutation(PartitionUpdate update, PotentialTxnConflicts potentialTxnConflicts) { - this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, allowPotentialTransactionConflicts); + this(update.metadata().keyspace, update.partitionKey(), ImmutableMap.of(update.metadata().id, update), approxTime.now(), update.metadata().params.cdc, potentialTxnConflicts); } - public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap modifications, long approxCreatedAtNanos, boolean allowPotentialTransactionConflicts) + public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap modifications, long approxCreatedAtNanos, PotentialTxnConflicts potentialTxnConflicts) { - this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), allowPotentialTransactionConflicts); + this(keyspaceName, key, modifications, approxCreatedAtNanos, cdcEnabled(modifications.values()), potentialTxnConflicts); } - public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap modifications, long approxCreatedAtNanos, boolean cdcEnabled, boolean allowPotentialTransactionConflicts) + public Mutation(String keyspaceName, DecoratedKey key, ImmutableMap modifications, long approxCreatedAtNanos, boolean cdcEnabled, PotentialTxnConflicts potentialTxnConflicts) { this.keyspaceName = keyspaceName; this.key = key; this.modifications = modifications; this.cdcEnabled = cdcEnabled; this.approxCreatedAtNanos = approxCreatedAtNanos; - this.allowPotentialTransactionConflicts = allowPotentialTransactionConflicts; + this.potentialTxnConflicts = potentialTxnConflicts; } private static boolean cdcEnabled(Iterable modifications) @@ -156,7 +157,7 @@ public class Mutation implements IMutation, Supplier Map updates = builder.build(); checkState(!updates.isEmpty(), "Updates should not be empty"); - return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, allowPotentialTransactionConflicts); + return new Mutation(keyspaceName, key, builder.build(), approxCreatedAtNanos, potentialTxnConflicts); } public @Nullable Mutation without(TableId tableId) @@ -248,12 +249,12 @@ public class Mutation implements IMutation, Supplier Set updatedTables = new HashSet<>(); String ks = null; DecoratedKey key = null; - Boolean allowPotentialTransactionConflicts = null; + PotentialTxnConflicts potentialTxnConflicts = null; for (Mutation mutation : mutations) { - if (allowPotentialTransactionConflicts != null && allowPotentialTransactionConflicts != mutation.allowPotentialTransactionConflicts) + if (potentialTxnConflicts != null && potentialTxnConflicts != mutation.potentialTxnConflicts) throw new IllegalArgumentException("Can't merge mutations with differing policies on allowing potential transaction conflicts"); - allowPotentialTransactionConflicts = mutation.allowPotentialTransactionConflicts; + potentialTxnConflicts = mutation.potentialTxnConflicts; updatedTables.addAll(mutation.modifications.keySet()); if (ks != null && !ks.equals(mutation.keyspaceName)) throw new IllegalArgumentException(); @@ -280,7 +281,7 @@ public class Mutation implements IMutation, Supplier modifications.put(table, updates.size() == 1 ? updates.get(0) : PartitionUpdate.merge(updates)); updates.clear(); } - return new Mutation(ks, key, modifications.build(), approxTime.now(), allowPotentialTransactionConflicts); + return new Mutation(ks, key, modifications.build(), approxTime.now(), potentialTxnConflicts); } public Future applyFuture() @@ -339,24 +340,24 @@ public class Mutation implements IMutation, Supplier public void allowPotentialTransactionConflicts() { - allowPotentialTransactionConflicts = true; + potentialTxnConflicts = PotentialTxnConflicts.ALLOW; Arrays.fill(cachedSerializations, null); } @Override - public boolean allowsPotentialTransactionConflicts() + public PotentialTxnConflicts potentialTxnConflicts() { - return allowPotentialTransactionConflicts; + return potentialTxnConflicts; } - private static int allowPotentialTransactionConflictsFlag(boolean allowPotentialTransactionConflicts) + private static int potentialTxnConflictsFlag(PotentialTxnConflicts potentialTxnConflicts) { - return allowPotentialTransactionConflicts ? ALLOW_POTENTIAL_TRANSACTION_CONFLICTS : 0; + return potentialTxnConflicts.allowed ? ALLOW_POTENTIAL_TRANSACTION_CONFLICTS : 0; } - public static boolean allowPotentialTransactionConflicts(int flags) + public static PotentialTxnConflicts potentialTxnConflicts(int flags) { - return (flags & ALLOW_POTENTIAL_TRANSACTION_CONFLICTS) != 0; + return (flags & ALLOW_POTENTIAL_TRANSACTION_CONFLICTS) != 0 ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW; } public String toString() @@ -443,7 +444,7 @@ public class Mutation implements IMutation, Supplier * being handled by the caller * @return this builder */ - public SimpleBuilder allowPotentialTransactionConflicts(); + public SimpleBuilder allowPotentialTxnConflicts(); /** * Sets the timestamp to use for the following additions to this builder or any derived (update or row) builder. @@ -560,7 +561,7 @@ public class Mutation implements IMutation, Supplier if (version >= VERSION_51) { int flags = 0; - flags |= allowPotentialTransactionConflictsFlag(mutation.allowPotentialTransactionConflicts); + flags |= potentialTxnConflictsFlag(mutation.potentialTxnConflicts); out.write(flags); } @@ -583,11 +584,11 @@ public class Mutation implements IMutation, Supplier { teeIn = new TeeDataInputPlus(in, dob, CACHEABLE_MUTATION_SIZE_LIMIT); - boolean allowPotentialTransactionConflicts = false; + PotentialTxnConflicts potentialTxnConflicts = PotentialTxnConflicts.DISALLOW; if (version >= VERSION_51) { int flags = teeIn.readByte(); - allowPotentialTransactionConflicts = allowPotentialTransactionConflicts(flags); + potentialTxnConflicts = potentialTxnConflicts(flags); } int size = teeIn.readUnsignedVInt32(); assert size > 0; @@ -595,7 +596,7 @@ public class Mutation implements IMutation, Supplier PartitionUpdate update = PartitionUpdate.serializer.deserialize(teeIn, version, flag); if (size == 1) { - m = new Mutation(update, allowPotentialTransactionConflicts); + m = new Mutation(update, potentialTxnConflicts); } else { @@ -608,7 +609,7 @@ public class Mutation implements IMutation, Supplier update = PartitionUpdate.serializer.deserialize(teeIn, version, flag); modifications.put(update.metadata().id, update); } - m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now(), allowPotentialTransactionConflicts); + m = new Mutation(update.metadata().keyspace, dk, modifications.build(), approxTime.now(), potentialTxnConflicts); } //Only cache serializations that don't hit the limit @@ -708,18 +709,18 @@ public class Mutation implements IMutation, Supplier private final long approxCreatedAtNanos = approxTime.now(); private boolean empty = true; - private boolean allowPotentialTransactionConflicts; + private PotentialTxnConflicts potentialTxnConflicts; public PartitionUpdateCollector(String keyspaceName, DecoratedKey key) { - this(keyspaceName, key, false); + this(keyspaceName, key, PotentialTxnConflicts.DISALLOW); } - public PartitionUpdateCollector(String keyspaceName, DecoratedKey key, boolean allowPotentialTransactionConflicts) + public PartitionUpdateCollector(String keyspaceName, DecoratedKey key, PotentialTxnConflicts potentialTxnConflicts) { this.keyspaceName = keyspaceName; this.key = key; - this.allowPotentialTransactionConflicts = allowPotentialTransactionConflicts; + this.potentialTxnConflicts = potentialTxnConflicts; } public PartitionUpdateCollector add(PartitionUpdate partitionUpdate) @@ -753,7 +754,7 @@ public class Mutation implements IMutation, Supplier public Mutation build() { - return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, allowPotentialTransactionConflicts); + return new Mutation(keyspaceName, key, modifications.build(), approxCreatedAtNanos, potentialTxnConflicts); } } } diff --git a/src/java/org/apache/cassandra/db/MutationVerbHandler.java b/src/java/org/apache/cassandra/db/MutationVerbHandler.java index c30fae63b4..3312a12b65 100644 --- a/src/java/org/apache/cassandra/db/MutationVerbHandler.java +++ b/src/java/org/apache/cassandra/db/MutationVerbHandler.java @@ -19,7 +19,10 @@ package org.apache.cassandra.db; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.net.*; +import org.apache.cassandra.net.ForwardingInfo; +import org.apache.cassandra.net.Message; +import org.apache.cassandra.net.MessagingService; +import org.apache.cassandra.net.ParamType; import org.apache.cassandra.tracing.Tracing; import static java.util.concurrent.TimeUnit.NANOSECONDS; diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java index 130c47a31f..52fbcaa98c 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java @@ -72,7 +72,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -82,7 +82,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR Index.QueryPlan indexQueryPlan, boolean trackWarnings) { - super(serializedAtEpoch, Kind.PARTITION_RANGE, isDigest, digestVersion, acceptsTransient, allowOutOfRangeReads, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); + super(serializedAtEpoch, Kind.PARTITION_RANGE, isDigest, digestVersion, acceptsTransient, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); this.requestedSlices = dataRange.clusteringIndexFilter.getSlices(metadata()); } @@ -90,7 +90,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -118,7 +118,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigest, digestVersion, acceptsTransient, - allowsOutOfRangeReads, + potentialTxnConflicts, metadata, nowInSec, columnFilter, @@ -140,7 +140,30 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR false, 0, false, + PotentialTxnConflicts.DISALLOW, + metadata, + nowInSec, + columnFilter, + rowFilter, + limits, + dataRange, + findIndexQueryPlan(metadata, rowFilter), + false); + } + + public static PartitionRangeReadCommand create(TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + DataRange dataRange, + PotentialTxnConflicts potentialTxnConflicts) + { + return create(metadata.epoch, false, + 0, + false, + potentialTxnConflicts, metadata, nowInSec, columnFilter, @@ -165,7 +188,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR false, 0, false, - false, + PotentialTxnConflicts.DISALLOW, metadata, nowInSec, ColumnFilter.all(metadata), @@ -212,7 +235,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -223,32 +246,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isTrackingWarnings()); } - /* - * The execution method does not need to perform reconciliation so the read command - * should execute in a mannager suited to not needing reconciliation. Such as when - * executing transactionally at a single replica and doing an index scan where the index - * scan should not return extra rows and expect post filtering at the coordinator. - */ - public PartitionRangeReadCommand withoutReconciliation() - { - if (rowFilter().isEmpty()) - return this; - return create(serializedAtEpoch(), - isDigestQuery(), - digestVersion(), - acceptsTransient(), - allowsOutOfRangeReads(), - metadata(), - nowInSec(), - columnFilter(), - rowFilter().withoutReconciliation(), - limits(), - dataRange(), - indexQueryPlan(), - isTrackingWarnings()); - } - - public PartitionRangeReadCommand forSubRangeWithNowInSeconds(long nowInSec, AbstractBounds range, boolean isRangeContinuation) + public PartitionRangeReadCommand withTransactionalSettings(long nowInSec, AbstractBounds range, boolean isRangeContinuation, boolean withoutReconciliation) { // If we're not a continuation of whatever range we've previously queried, we should ignore the states of the // DataLimits as it's either useless, or misleading. This is particularly important for GROUP BY queries, where @@ -259,26 +257,26 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + PotentialTxnConflicts.ALLOW, metadata(), nowInSec, columnFilter(), - rowFilter(), + withoutReconciliation ? rowFilter().withoutReconciliation() : rowFilter(), isRangeContinuation ? limits() : limits().withoutState(), dataRange().forSubRange(range), indexQueryPlan(), isTrackingWarnings()); } - public PartitionRangeReadCommand withNowInSec(long nowInSec) + public PartitionRangeReadCommand withTxnReadName(int txnReadName) { return create(serializedAtEpoch(), isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), - nowInSec, + txnReadName, columnFilter(), rowFilter(), limits(), @@ -293,7 +291,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -311,7 +309,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR true, digestVersion(), false, - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -329,7 +327,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR false, 0, true, - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -347,7 +345,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -365,7 +363,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -601,7 +599,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -611,7 +609,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR throws IOException { DataRange range = DataRange.serializer.deserialize(in, version, metadata); - return PartitionRangeReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, allowsOutOfRangeReads, metadata, nowInSec, columnFilter, rowFilter, limits, range, indexQueryPlan, false); + return PartitionRangeReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, range, indexQueryPlan, false); } } @@ -629,7 +627,7 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR Index.QueryPlan indexQueryPlan, boolean trackWarnings) { - super(metadata.epoch, isDigest, digestVersion, acceptsTransient, true, metadata, nowInSec, columnFilter, rowFilter, limits, dataRange, indexQueryPlan, trackWarnings); + super(metadata.epoch, isDigest, digestVersion, acceptsTransient, PotentialTxnConflicts.ALLOW, metadata, nowInSec, columnFilter, rowFilter, limits, dataRange, indexQueryPlan, trackWarnings); } @Override diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java b/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java index d91930125d..934c33eafa 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadQuery.java @@ -17,6 +17,7 @@ */ package org.apache.cassandra.db; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; @@ -36,9 +37,10 @@ public interface PartitionRangeReadQuery extends ReadQuery ColumnFilter columnFilter, RowFilter rowFilter, DataLimits limits, - DataRange dataRange) + DataRange dataRange, + PotentialTxnConflicts potentialTxnConflicts) { - return PartitionRangeReadCommand.create(table, nowInSec, columnFilter, rowFilter, limits, dataRange); + return PartitionRangeReadCommand.create(table, nowInSec, columnFilter, rowFilter, limits, dataRange, potentialTxnConflicts); } /** diff --git a/src/java/org/apache/cassandra/db/RangeTombstoneList.java b/src/java/org/apache/cassandra/db/RangeTombstoneList.java index 8eee422be3..963985788a 100644 --- a/src/java/org/apache/cassandra/db/RangeTombstoneList.java +++ b/src/java/org/apache/cassandra/db/RangeTombstoneList.java @@ -22,14 +22,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.Iterator; -import org.apache.cassandra.config.DatabaseDescriptor; -import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.utils.CassandraUInt; - import com.google.common.collect.Iterators; import org.apache.cassandra.cache.IMeasurableMemory; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.rows.Cell; +import org.apache.cassandra.db.rows.EncodingStats; +import org.apache.cassandra.utils.AbstractIterator; +import org.apache.cassandra.utils.CassandraUInt; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.memory.ByteBufferCloner; @@ -328,12 +328,13 @@ public class RangeTombstoneList implements Iterable, IMeasurable markedAts[i] = timestamp; } - public void updateAllTimestampAndLocalDeletionTime(long timestamp, int localDeletionTime) + public void updateAllTimestampAndLocalDeletionTime(long timestamp, long localDeletionTime) { + int unsignedLocalDeletionTime = Cell.deletionTimeLongToUnsignedInteger(localDeletionTime); for (int i = 0; i < size; i++) { markedAts[i] = timestamp; - delTimesUnsignedIntegers[i] = localDeletionTime; + delTimesUnsignedIntegers[i] = unsignedLocalDeletionTime; } } diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java index d6b0f4734d..8b80044f18 100644 --- a/src/java/org/apache/cassandra/db/ReadCommand.java +++ b/src/java/org/apache/cassandra/db/ReadCommand.java @@ -90,6 +90,7 @@ import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.ClientWarn; +import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.tracing.Tracing; @@ -100,6 +101,7 @@ import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.TimeUUID; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.any; import static com.google.common.collect.Iterables.filter; import static org.apache.cassandra.db.partitions.UnfilteredPartitionIterators.MergeListener.NOOP; @@ -119,6 +121,30 @@ public abstract class ReadCommand extends AbstractReadQuery protected static final Logger logger = LoggerFactory.getLogger(ReadCommand.class); public static final IVersionedSerializer serializer = new Serializer(); + public enum PotentialTxnConflicts + { + /** + * Check for and raise an error if this operation should have been transactionally managed. For use + * by queries that aren't issued by a transaction system managing potential conflicts in contexts where + * conflicts would be a problem. + */ + DISALLOW(false), + + /** + * Don't check or raise an error if this operation could conflict with transactions. For use when the thing + * being managed doesn't support transactions or the operation is being done by a transaction that is already + * managing any potential conflicts. + */ + ALLOW(true); + + public final boolean allowed; + + PotentialTxnConflicts(boolean allowed) + { + this.allowed = allowed; + } + } + // Expose the active command running so transitive calls can lookup this command. // This is useful for a few reasons, but mainly because the CQL query is here. private static final FastThreadLocal COMMAND = new FastThreadLocal<>(); @@ -128,7 +154,8 @@ public abstract class ReadCommand extends AbstractReadQuery private final boolean isDigestQuery; private final boolean acceptsTransient; private final Epoch serializedAtEpoch; - private boolean allowsOutOfRangeReads; + private final PotentialTxnConflicts potentialTxnConflicts; + // if a digest query, the version for which the digest is expected. Ignored if not a digest. private int digestVersion; @@ -147,7 +174,7 @@ public abstract class ReadCommand extends AbstractReadQuery boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -174,7 +201,7 @@ public abstract class ReadCommand extends AbstractReadQuery boolean isDigestQuery, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -193,7 +220,7 @@ public abstract class ReadCommand extends AbstractReadQuery this.digestVersion = digestVersion; this.acceptsTransient = acceptsTransient; this.indexQueryPlan = indexQueryPlan; - this.allowsOutOfRangeReads = allowsOutOfRangeReads; + this.potentialTxnConflicts = potentialTxnConflicts; this.trackWarnings = trackWarnings; this.serializedAtEpoch = serializedAtEpoch; this.dataRange = dataRange; @@ -345,7 +372,7 @@ public abstract class ReadCommand extends AbstractReadQuery */ public ReadCommand copyAsTransientQuery(Replica replica) { - Preconditions.checkArgument(replica.isTransient(), + checkArgument(replica.isTransient(), "Can't make a transient request on a full replica: " + replica); return copyAsTransientQuery(); } @@ -367,7 +394,7 @@ public abstract class ReadCommand extends AbstractReadQuery */ public ReadCommand copyAsDigestQuery(Replica replica) { - Preconditions.checkArgument(replica.isFull(), + checkArgument(replica.isFull(), "Can't make a digest request on a transient replica " + replica); return copyAsDigestQuery(); } @@ -462,6 +489,8 @@ public abstract class ReadCommand extends AbstractReadQuery try { ColumnFamilyStore cfs = Keyspace.openAndGetStore(metadata()); + if (!potentialTxnConflicts.allowed) + ConsensusRequestRouter.validateSafeToReadNonTransactionally(this); Index.QueryPlan indexQueryPlan = indexQueryPlan(); Index.Searcher searcher = null; @@ -550,15 +579,9 @@ public abstract class ReadCommand extends AbstractReadQuery return ReadExecutionController.forCommand(this, false); } - public ReadCommand allowOutOfRangeReads() + public PotentialTxnConflicts potentialTxnConflicts() { - allowsOutOfRangeReads = true; - return this; - } - - public boolean allowsOutOfRangeReads() - { - return allowsOutOfRangeReads; + return potentialTxnConflicts; } /** @@ -1250,7 +1273,7 @@ public abstract class ReadCommand extends AbstractReadQuery private static final int HAS_INDEX = 0x04; private static final int ACCEPTS_TRANSIENT = 0x08; private static final int NEEDS_RECONCILIATION = 0x10; - private static final int ALLOWS_OUT_OF_RANGE_READS = 0x20; + private static final int ALLOWS_POTENTIAL_TXN_CONFLICTS = 0x20; private final SchemaProvider schema; @@ -1315,14 +1338,14 @@ public abstract class ReadCommand extends AbstractReadQuery return (flags & NEEDS_RECONCILIATION) != 0; } - private static int allowsOutOfRangeReadsFlag(boolean allowsOutOfRangeReads) + private static int potentialTxnConflicts(PotentialTxnConflicts potentialTxnConflicts) { - return allowsOutOfRangeReads ? ALLOWS_OUT_OF_RANGE_READS: 0; + return potentialTxnConflicts.allowed ? ALLOWS_POTENTIAL_TXN_CONFLICTS : 0; } - private static boolean allowsOutOfRangeReads(int flags) + private static PotentialTxnConflicts potentialTxnConflicts(int flags) { - return (flags & ALLOWS_OUT_OF_RANGE_READS) != 0; + return (flags & ALLOWS_POTENTIAL_TXN_CONFLICTS) != 0 ? PotentialTxnConflicts.ALLOW : PotentialTxnConflicts.DISALLOW; } public void serialize(ReadCommand command, DataOutputPlus out, int version) throws IOException @@ -1333,7 +1356,7 @@ public abstract class ReadCommand extends AbstractReadQuery | indexFlag(null != command.indexQueryPlan()) | acceptsTransientFlag(command.acceptsTransient()) | needsReconciliationFlag(command.rowFilter().needsReconciliation()) - | allowsOutOfRangeReadsFlag(command.allowsOutOfRangeReads) + | potentialTxnConflicts(command.potentialTxnConflicts) ); if (command.isDigestQuery()) out.writeUnsignedVInt32(command.digestVersion()); @@ -1359,7 +1382,7 @@ public abstract class ReadCommand extends AbstractReadQuery int flags = in.readByte(); boolean isDigest = isDigest(flags); boolean acceptsTransient = acceptsTransient(flags); - boolean allowsOutOfRangeReads = allowsOutOfRangeReads(flags); + PotentialTxnConflicts potentialTxnConflicts = potentialTxnConflicts(flags); // Shouldn't happen or it's a user error (see comment above) but // better complain loudly than doing the wrong thing. if (isForThrift(flags)) @@ -1405,7 +1428,7 @@ public abstract class ReadCommand extends AbstractReadQuery indexQueryPlan = indexGroup.queryPlanFor(rowFilter); } - return kind.selectionDeserializer.deserialize(in, version, schemaVersion, isDigest, digestVersion, acceptsTransient, allowsOutOfRangeReads, tableMetadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan); + return kind.selectionDeserializer.deserialize(in, version, schemaVersion, isDigest, digestVersion, acceptsTransient, potentialTxnConflicts, tableMetadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan); } private IndexMetadata deserializeIndexMetadata(DataInputPlus in, int version, TableMetadata metadata) throws IOException diff --git a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java index 0094a0df92..8ff89f4e9b 100644 --- a/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java +++ b/src/java/org/apache/cassandra/db/ReadCommandVerbHandler.java @@ -28,6 +28,7 @@ import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.InvalidRoutingException; import org.apache.cassandra.exceptions.QueryCancelledException; +import org.apache.cassandra.exceptions.RetryOnDifferentSystemException; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.metrics.TCMMetrics; import org.apache.cassandra.net.IVerbHandler; @@ -42,6 +43,7 @@ import org.apache.cassandra.tracing.Tracing; import org.apache.cassandra.utils.FBUtilities; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.apache.cassandra.exceptions.RequestFailureReason.RETRY_ON_DIFFERENT_TRANSACTION_SYSTEM; public class ReadCommandVerbHandler implements IVerbHandler { @@ -99,6 +101,13 @@ public class ReadCommandVerbHandler implements IVerbHandler MessagingService.instance().send(reply, message.from()); return; } + 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"); + return; + } catch (AssertionError t) { throw new AssertionError(String.format("Caught an error while trying to process the command: %s", command.toCQLString()), t); @@ -164,7 +173,7 @@ public class ReadCommandVerbHandler implements IVerbHandler // Some read commands may be sent using an older Epoch intentionally so validating using the current Epoch // doesn't work - if (command.allowsOutOfRangeReads()) + if (command.potentialTxnConflicts().allowed) return metadata; if (command.isTopK()) diff --git a/src/java/org/apache/cassandra/db/ReadResponse.java b/src/java/org/apache/cassandra/db/ReadResponse.java index d4906b2dd5..65e17a6920 100644 --- a/src/java/org/apache/cassandra/db/ReadResponse.java +++ b/src/java/org/apache/cassandra/db/ReadResponse.java @@ -17,14 +17,19 @@ */ package org.apache.cassandra.db; -import java.io.*; +import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import com.google.common.annotations.VisibleForTesting; import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.partitions.*; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputBuffer; import org.apache.cassandra.io.util.DataInputPlus; @@ -132,6 +137,37 @@ public abstract class ReadResponse return sb.toString(); } + /** + * For range reads Accord generates multiple responses per node because each command store executes + * the reads independently. The responses are already sorted in token order so the iterators or digests can be + * merged and still produce a consistent result across different nodes. + * + * This can *only* be called from the node producing the results not the coordinator because isEmptyDigest is + * not serialized + */ + public static ReadResponse merge(List responses, ReadCommand command) + { + if (responses.get(0).isDigestResponse()) + { + Digest digest = Digest.forReadResponse(); + for (ReadResponse response : responses) + digest.update(((DigestResponse)response).digest); + return new DigestResponse(ByteBuffer.wrap(digest.digest())); + } + else + { + List iterators = new ArrayList<>(responses.size()); + for (ReadResponse response : responses) + iterators.add(response.makeIterator(command)); + + // Range responses will not respect the limit because each command store returns a separate response + // so we effectively deserialize and then reserialize in order to apply the limits + // Wasteful, but better than sending it to the coordinator to do it + UnfilteredPartitionIterator filtered = command.limits().filter(UnfilteredPartitionIterators.concat(iterators), 0, command.selectsFullPartition()); + return new LocalDataResponse(filtered, command, NO_OP_REPAIRED_DATA_INFO); + } + } + protected static ByteBuffer makeDigest(UnfilteredPartitionIterator iterator, ReadCommand command) { Digest digest = Digest.forReadResponse(); diff --git a/src/java/org/apache/cassandra/db/SimpleBuilders.java b/src/java/org/apache/cassandra/db/SimpleBuilders.java index af13cb9126..7139b6c8b2 100644 --- a/src/java/org/apache/cassandra/db/SimpleBuilders.java +++ b/src/java/org/apache/cassandra/db/SimpleBuilders.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import org.apache.cassandra.cql3.ColumnIdentifier; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.context.CounterContext; import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.CollectionType; @@ -120,7 +121,7 @@ public abstract class SimpleBuilders private final Map updateBuilders = new HashMap<>(); - private boolean allowPotentialTransactionConflicts = false; + private PotentialTxnConflicts potentialTxnConflicts = PotentialTxnConflicts.DISALLOW; public MutationBuilder(String keyspaceName, DecoratedKey key) { @@ -128,9 +129,9 @@ public abstract class SimpleBuilders this.key = key; } - public MutationBuilder allowPotentialTransactionConflicts() + public MutationBuilder allowPotentialTxnConflicts() { - allowPotentialTransactionConflicts = true; + potentialTxnConflicts = PotentialTxnConflicts.ALLOW; return this; } @@ -162,9 +163,9 @@ public abstract class SimpleBuilders assert !updateBuilders.isEmpty() : "Cannot create empty mutation"; if (updateBuilders.size() == 1) - return new Mutation(updateBuilders.values().iterator().next().build(), allowPotentialTransactionConflicts); + return new Mutation(updateBuilders.values().iterator().next().build(), potentialTxnConflicts); - Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key, allowPotentialTransactionConflicts); + Mutation.PartitionUpdateCollector mutationBuilder = new Mutation.PartitionUpdateCollector(keyspaceName, key, potentialTxnConflicts); for (PartitionUpdateBuilder builder : updateBuilders.values()) mutationBuilder.add(builder.build()); return mutationBuilder.build(); diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java index cdff02aa4f..0b4d25a925 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java @@ -98,7 +98,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -110,7 +110,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean trackWarnings, DataRange dataRange) { - super(serializedAtEpoch, Kind.SINGLE_PARTITION, isDigest, digestVersion, acceptsTransient, allowsOutOfRangeReads, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); + super(serializedAtEpoch, Kind.SINGLE_PARTITION, isDigest, digestVersion, acceptsTransient, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, indexQueryPlan, trackWarnings, dataRange); assert partitionKey.getPartitioner() == metadata.partitioner; this.partitionKey = partitionKey; this.clusteringIndexFilter = clusteringIndexFilter; @@ -120,7 +120,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -154,7 +154,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar isDigest, digestVersion, acceptsTransient, - allowsOutOfRangeReads, + potentialTxnConflicts, metadata, nowInSec, columnFilter, @@ -194,7 +194,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar false, 0, false, - false, + PotentialTxnConflicts.DISALLOW, metadata, nowInSec, columnFilter, @@ -206,6 +206,44 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar false); } + /** + * Creates a new read command on a single partition. + * + * @param metadata the table to query. + * @param nowInSec the time in seconds to use are "now" for this query. + * @param columnFilter the column filter to use for the query. + * @param rowFilter the row filter to use for the query. + * @param limits the limits to use for the query. + * @param partitionKey the partition key for the partition to query. + * @param clusteringIndexFilter the clustering index filter to use for the query. + * + * @return a newly created read command. + */ + public static SinglePartitionReadCommand create(TableMetadata metadata, + long nowInSec, + ColumnFilter columnFilter, + RowFilter rowFilter, + DataLimits limits, + DecoratedKey partitionKey, + ClusteringIndexFilter clusteringIndexFilter, + PotentialTxnConflicts potentialTxnConflicts) + { + return create(metadata.epoch, + false, + 0, + false, + potentialTxnConflicts, + metadata, + nowInSec, + columnFilter, + rowFilter, + limits, + partitionKey, + clusteringIndexFilter, + findIndexQueryPlan(metadata, rowFilter), + false); + } + /** * Creates a new read command on a single partition. * @@ -373,7 +411,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -392,7 +430,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar true, digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -411,7 +449,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar false, 0, true, - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -430,7 +468,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + potentialTxnConflicts(), metadata(), nowInSec(), columnFilter(), @@ -442,24 +480,6 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar isTrackingWarnings()); } - public SinglePartitionReadCommand withNowInSec(long nowInSec) - { - return create(serializedAtEpoch(), - isDigestQuery(), - digestVersion(), - acceptsTransient(), - allowsOutOfRangeReads(), - metadata(), - nowInSec, - columnFilter(), - rowFilter(), - limits(), - partitionKey(), - clusteringIndexFilter(), - indexQueryPlan(), - isTrackingWarnings()); - } - @Override public DecoratedKey partitionKey() { @@ -1274,24 +1294,21 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar } /* - * The execution method does not need to perform reconciliation so the read command - * should execute in a mannager suited to not needing reconciliation. Such as when - * executing transactionally at a single replica and doing an index scan where the index - * scan should not return extra rows and expect post filtering at the coordinator. + * When running transactionally we need to use the txn system nowInSeconds, and set whether reconciliation + * should be performed based on whether it's part of a multiple replica read. We also allow potential txn conflicts + * because we manage those conflicts from the txn system */ - public SinglePartitionReadCommand withoutReconciliation() + public SinglePartitionReadCommand withTransactionalSettings(boolean withoutReconciliation, long nowInSeconds) { - if (rowFilter().isEmpty()) - return this; return create(serializedAtEpoch(), isDigestQuery(), digestVersion(), acceptsTransient(), - allowsOutOfRangeReads(), + PotentialTxnConflicts.ALLOW, metadata(), - nowInSec(), + nowInSeconds, columnFilter(), - rowFilter().withoutReconciliation(), + withoutReconciliation ? rowFilter().withoutReconciliation() : rowFilter(), limits(), partitionKey(), clusteringIndexFilter(), @@ -1310,7 +1327,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar RowFilter rowFilter, DataLimits limits, List partitionKeys, - ClusteringIndexFilter clusteringIndexFilter) + ClusteringIndexFilter clusteringIndexFilter, + PotentialTxnConflicts potentialTxnConflicts) { List commands = new ArrayList<>(partitionKeys.size()); for (DecoratedKey partitionKey : partitionKeys) @@ -1321,7 +1339,8 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar rowFilter, limits, partitionKey, - clusteringIndexFilter)); + clusteringIndexFilter, + potentialTxnConflicts)); } return create(commands, limits); @@ -1377,7 +1396,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean isDigest, int digestVersion, boolean acceptsTransient, - boolean allowsOutOfRangeReads, + PotentialTxnConflicts potentialTxnConflicts, TableMetadata metadata, long nowInSec, ColumnFilter columnFilter, @@ -1388,7 +1407,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar { DecoratedKey key = metadata.partitioner.decorateKey(metadata.partitionKeyType.readBuffer(in, DatabaseDescriptor.getMaxValueSize())); ClusteringIndexFilter filter = ClusteringIndexFilter.serializer.deserialize(in, version, metadata); - return SinglePartitionReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, allowsOutOfRangeReads, metadata, nowInSec, columnFilter, rowFilter, limits, key, filter, indexQueryPlan, false); + return SinglePartitionReadCommand.create(serializedAtEpoch, isDigest, digestVersion, acceptsTransient, potentialTxnConflicts, metadata, nowInSec, columnFilter, rowFilter, limits, key, filter, indexQueryPlan, false); } } @@ -1436,7 +1455,7 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar boolean trackWarnings, DataRange dataRange) { - super(metadata.epoch, isDigest, digestVersion, acceptsTransient, true, metadata, nowInSec, columnFilter, + super(metadata.epoch, isDigest, digestVersion, acceptsTransient, PotentialTxnConflicts.ALLOW, metadata, nowInSec, columnFilter, rowFilter, limits, partitionKey, clusteringIndexFilter, indexQueryPlan, trackWarnings, dataRange); } diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java index 5409cde8c4..0ec2961861 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java @@ -23,9 +23,9 @@ import java.util.List; import java.util.stream.Collectors; import com.google.common.collect.Iterables; - import org.apache.commons.lang3.tuple.Pair; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.filter.ClusteringIndexFilter; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; @@ -51,9 +51,10 @@ public interface SinglePartitionReadQuery extends ReadQuery RowFilter rowFilter, DataLimits limits, List partitionKeys, - ClusteringIndexFilter clusteringIndexFilter) + ClusteringIndexFilter clusteringIndexFilter, + PotentialTxnConflicts potentialTxnConflicts) { - return SinglePartitionReadCommand.Group.create(metadata, nowInSec, columnFilter, rowFilter, limits, partitionKeys, clusteringIndexFilter); + return SinglePartitionReadCommand.Group.create(metadata, nowInSec, columnFilter, rowFilter, limits, partitionKeys, clusteringIndexFilter, potentialTxnConflicts); } diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java index b8a86d5a1a..30f3e75e62 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionIterators.java @@ -17,16 +17,16 @@ */ package org.apache.cassandra.db.partitions; -import java.util.*; +import java.util.List; import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.SinglePartitionReadQuery; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.RowIterators; import org.apache.cassandra.db.transform.MorePartitions; import org.apache.cassandra.db.transform.Transformation; import org.apache.cassandra.utils.AbstractIterator; -import org.apache.cassandra.db.SinglePartitionReadQuery; -import org.apache.cassandra.db.rows.*; - public abstract class PartitionIterators { private PartitionIterators() {} @@ -57,7 +57,7 @@ public abstract class PartitionIterators return Transformation.apply(toReturn, new Close()); } - public static PartitionIterator concat(final List iterators) + public static PartitionIterator concat(final List iterators) { if (iterators.size() == 1) return iterators.get(0); diff --git a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java index 87e2154e52..011629576f 100644 --- a/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java +++ b/src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java @@ -71,14 +71,14 @@ import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputBuffer; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.metrics.TCMMetrics; -import org.apache.cassandra.tcm.ClusterMetadata; -import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.net.MessagingService; 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.service.ClientState; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.btree.BTree; import org.apache.cassandra.utils.btree.UpdateFunction; import org.apache.cassandra.utils.vint.VIntCoding; @@ -1161,7 +1161,7 @@ public class PartitionUpdate extends AbstractBTreePartition return this; } - public Builder updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime) + public Builder updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime) { deletionInfo.updateAllTimestampAndLocalDeletionTime(newTimestamp - 1, newLocalDeletionTime); tree = BTree.transformAndFilter(tree, (x) -> x.updateTimesAndPathsForAccord(cellToMaybeNewListPath, newTimestamp, newLocalDeletionTime)); diff --git a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java index e68603c9f3..fd7880e367 100644 --- a/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java +++ b/src/java/org/apache/cassandra/db/partitions/UnfilteredPartitionIterators.java @@ -19,11 +19,21 @@ package org.apache.cassandra.db.partitions; import java.io.IOError; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.Digest; +import org.apache.cassandra.db.EmptyIterators; +import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.DeserializationHelper; +import org.apache.cassandra.db.rows.LazilyInitializedUnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIteratorSerializer; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.db.transform.FilteredPartitions; import org.apache.cassandra.db.transform.MorePartitions; import org.apache.cassandra.db.transform.Transformation; diff --git a/src/java/org/apache/cassandra/db/rows/AbstractCell.java b/src/java/org/apache/cassandra/db/rows/AbstractCell.java index d30489a109..0dbcfc4420 100644 --- a/src/java/org/apache/cassandra/db/rows/AbstractCell.java +++ b/src/java/org/apache/cassandra/db/rows/AbstractCell.java @@ -121,14 +121,14 @@ public abstract class AbstractCell extends Cell } @Override - public ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime) + public ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime) { long localDeletionTime = localDeletionTime() != NO_DELETION_TIME ? newLocalDeletionTime : NO_DELETION_TIME; return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime, buffer(), path()); } @Override - public Cell updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, int newLocalDeletionTime) + public Cell updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, long newLocalDeletionTime) { long localDeletionTime = localDeletionTime() != NO_DELETION_TIME ? newLocalDeletionTime : NO_DELETION_TIME; return new BufferCell(column, isTombstone() ? newTimestamp - 1 : newTimestamp, ttl(), localDeletionTime, buffer(), maybeNewPath); diff --git a/src/java/org/apache/cassandra/db/rows/BTreeRow.java b/src/java/org/apache/cassandra/db/rows/BTreeRow.java index ed445e8b6b..84ebdc00be 100644 --- a/src/java/org/apache/cassandra/db/rows/BTreeRow.java +++ b/src/java/org/apache/cassandra/db/rows/BTreeRow.java @@ -444,7 +444,7 @@ public class BTreeRow extends AbstractRow } @Override - public Row updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime) + public Row updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime) { LivenessInfo newInfo = primaryKeyLivenessInfo.isEmpty() ? primaryKeyLivenessInfo : primaryKeyLivenessInfo.withUpdatedTimestampAndLocalDeletionTime(newTimestamp, newLocalDeletionTime); // If the deletion is shadowable and the row has a timestamp, we'll forced the deletion timestamp to be less than the row one, so we diff --git a/src/java/org/apache/cassandra/db/rows/ColumnData.java b/src/java/org/apache/cassandra/db/rows/ColumnData.java index 8f055e1d14..c27d9bdabc 100644 --- a/src/java/org/apache/cassandra/db/rows/ColumnData.java +++ b/src/java/org/apache/cassandra/db/rows/ColumnData.java @@ -291,7 +291,7 @@ public abstract class ColumnData implements IMeasurableMemory /** * @param cellToMaybeNewListPath If the cell is a list append cell a new cell path is returned generated based on the Accord executeAt timestamp */ - public abstract ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime); + public abstract ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime); /** * List paths are time UUIDs that increment for each item in the list and for Accord and Paxos @@ -299,7 +299,7 @@ public abstract class ColumnData implements IMeasurableMemory * * @param maybeNewPath If this cell is a list append for a non-frozen list (multi-cell) then it will be new path generated using the executeAt timestamp, otherwise it will be the existing path */ - public abstract ColumnData updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, int newLocalDeletionTime); + public abstract ColumnData updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, long newLocalDeletionTime); public abstract ColumnData markCounterLocalToBeCleared(); diff --git a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java index f032fd5b6d..308c147f8a 100644 --- a/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java +++ b/src/java/org/apache/cassandra/db/rows/ComplexColumnData.java @@ -267,7 +267,7 @@ public class ComplexColumnData extends ColumnData implements Iterable> } @Override - public ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime) + public ColumnData updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime) { DeletionTime newDeletion = complexDeletion.isLive() ? complexDeletion : DeletionTime.build(newTimestamp - 1, newLocalDeletionTime); Function maybeNewListPath; @@ -279,7 +279,7 @@ public class ComplexColumnData extends ColumnData implements Iterable> } @Override - public ColumnData updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, int newLocalDeletionTime) + public ColumnData updateAllTimesWithNewCellPathForComplexColumnData(@Nonnull CellPath maybeNewPath, long newTimestamp, long newLocalDeletionTime) { throw new UnsupportedOperationException(); } diff --git a/src/java/org/apache/cassandra/db/rows/Row.java b/src/java/org/apache/cassandra/db/rows/Row.java index 3820e8c3a4..cb48c4ce6f 100644 --- a/src/java/org/apache/cassandra/db/rows/Row.java +++ b/src/java/org/apache/cassandra/db/rows/Row.java @@ -312,7 +312,7 @@ public interface Row extends Unfiltered, Iterable, IMeasurableMemory */ public Row updateAllTimestamp(long newTimestamp); - public Row updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, int newLocalDeletionTime); + public Row updateTimesAndPathsForAccord(@Nonnull Function cellToMaybeNewListPath, long newTimestamp, long newLocalDeletionTime); /** * Returns a copy of this row with the new deletion as row deletion if it is more recent diff --git a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java index f07e0e3282..ee98da29c1 100644 --- a/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java +++ b/src/java/org/apache/cassandra/db/virtual/VirtualMutation.java @@ -30,6 +30,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.IMutation; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.ClientState; @@ -142,8 +143,8 @@ public final class VirtualMutation implements IMutation * Accord doesn't support reading/writing virtual tables yet so updating them non-transactionally is always safe */ @Override - public boolean allowsPotentialTransactionConflicts() + public PotentialTxnConflicts potentialTxnConflicts() { - return true; + return PotentialTxnConflicts.ALLOW; } } diff --git a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java index 2885cd732b..c702a4d7c3 100644 --- a/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java +++ b/src/java/org/apache/cassandra/dht/Murmur3Partitioner.java @@ -258,6 +258,7 @@ public class Murmur3Partitioner implements IPartitioner // CASSANDRA-17109 Added the below checks, but paxos tests were not updated, rather than fix // the paxos tests, disabling the checks for now. The current paxos tests bias twards MIN but // not for MAX, which makes the test very flaky as when MAX is generated the test fails... + // TODO (mustfix): This was done as part of CEP-15 and needs to be added back // if (token == MAXIMUM) // throw new IllegalArgumentException("Cannot increase above MAXIMUM"); diff --git a/src/java/org/apache/cassandra/dht/NormalizedRanges.java b/src/java/org/apache/cassandra/dht/NormalizedRanges.java index 8a92467d3b..c9a040a658 100644 --- a/src/java/org/apache/cassandra/dht/NormalizedRanges.java +++ b/src/java/org/apache/cassandra/dht/NormalizedRanges.java @@ -136,6 +136,9 @@ public class NormalizedRanges> extends AbstractList subtract(NormalizedRanges b) { + if (b.isEmpty()) + return this; + if (b.size() == 1 && b.get(0).isFull()) return NormalizedRanges.empty(); @@ -199,10 +202,9 @@ public class NormalizedRanges> extends AbstractList invert() + NormalizedRanges invert() { - if (isEmpty()) - return this; + checkState(!isEmpty()); List> result = new ArrayList<>(size() + 2); T minValue = get(0).left.minValue(); diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java index 3407890a70..00204c66b4 100644 --- a/src/java/org/apache/cassandra/dht/Range.java +++ b/src/java/org/apache/cassandra/dht/Range.java @@ -35,6 +35,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.Token.KeyBound; import org.apache.cassandra.dht.Token.TokenFactory; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; @@ -44,6 +45,8 @@ import org.apache.cassandra.tcm.serialization.MetadataSerializer; import org.apache.cassandra.tcm.serialization.Version; import org.apache.cassandra.utils.Pair; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import static java.util.Collections.emptyList; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_RANGE_EXPENSIVE_CHECKS; @@ -776,4 +779,88 @@ public class Range> extends AbstractBounds implemen return tokenSerializer.serializedSize(t, SERDE_VERSION); } } + + /** + * Returns a Pair containing the intersection (or null) and the remainder of the bounds that is to the right of the + * range, the remainder to the left is discarded since it is assumed if you are checking for intersection of multiple ranges + * the ranges are being checked in order. + */ + public static Pair, AbstractBounds> intersectionAndRemainder(AbstractBounds bounds, org.apache.cassandra.dht.Range range) + { + checkArgument((bounds.inclusiveRight() && bounds.inclusiveLeft()) || (bounds.left.compareTo(bounds.right) < 0 || bounds.right.isMinimum()), "Wrap around not handled"); + boolean boundsInclusiveLeft = bounds.inclusiveLeft() || (bounds.left.getClass() == KeyBound.class && ((KeyBound)bounds.left).isMinimumBound); + boolean boundsInclusiveRight = bounds.inclusiveRight() || (bounds.right.getClass() == KeyBound.class && !((KeyBound)bounds.right).isMinimumBound); + Token boundsLeft = bounds.left.getToken(); + Token boundsRight = bounds.right.getToken(); + Token rangeLeft = range.left; + Token rangeRight = range.right; + checkState(rangeLeft.compareTo(rangeRight) < 0 || rangeRight.isMinimum(), "Wrap around is not handled"); + + // Completely before + int rightLeftCmp = boundsRight.compareTo(rangeLeft); + // Nothing is > min on the right + if (boundsRight.isMinimum()) + rightLeftCmp = 1; + // Range left is not inclusive, doesn't matter whether the bound is inclusive/exclusive left + rightLeftCmp = rightLeftCmp == 0 ? -1 : rightLeftCmp; + if (rightLeftCmp < 0) + return Pair.create(null, null); + + // Completely after + int leftRightCmp = boundsLeft.compareTo(rangeRight); + // Nothing is > min on the right + if (rangeRight.isMinimum()) + leftRightCmp = -1; + // Fixed mismatched inclusivity + leftRightCmp = leftRightCmp == 0 && !boundsInclusiveLeft ? 1 : leftRightCmp; + if (leftRightCmp > 0) + return Pair.create(null, bounds); + + int rightRightCmp = boundsRight.compareTo(rangeRight); + // min on the right is > than everything + if (rangeRight.isMinimum() && boundsRight.isMinimum()) + rightRightCmp = 0; + else if (boundsRight.isMinimum()) + rightRightCmp = 1; + else if (rangeRight.isMinimum()) + rightRightCmp = -1; + // Fixed mismatched inclusivity + rightRightCmp = rightRightCmp == 0 && !boundsInclusiveRight ? -1 : rightRightCmp; + + int leftLeftCmp = boundsLeft.compareTo(rangeLeft); + // Range left is not inclusive, doesn't matter whether the bound is inclusive/exclusive left + leftLeftCmp = leftLeftCmp == 0 ? -1 : leftLeftCmp; + + // Fully contained + if (leftLeftCmp > 0 && rightRightCmp <= 0) + return Pair.create(bounds, null); + // Split by the right bound of the range (rightRightCmp is implicitly > 0 given the preceding condition) + else if (leftLeftCmp >= 0) + return bounds.split(rangeRight.maxKeyBound()); + // Intersects but has some portion that needs to be discarded first + else + { + // Remove everything before the intersection + Pair, AbstractBounds> split = bounds.split(rangeLeft.maxKeyBound()); + AbstractBounds intersectionAndRemainder = bounds; + if (split != null) + intersectionAndRemainder = split.right; + // There is a remainder + if (rightRightCmp > 0) + return intersectionAndRemainder.split(rangeRight.maxKeyBound()); + // There is no remainder everything that + return Pair.create(intersectionAndRemainder, null); + } + } + + public static int compareRightToken(Token a, Token b) + { + if (a.isMinimum() && b.isMinimum()) + return 0; + if (a.isMinimum()) + return 1; + if (b.isMinimum()) + return 0; + return a.compareTo(b); + } } diff --git a/src/java/org/apache/cassandra/hints/HintsDispatcher.java b/src/java/org/apache/cassandra/hints/HintsDispatcher.java index 6022f284df..faeefcf973 100644 --- a/src/java/org/apache/cassandra/hints/HintsDispatcher.java +++ b/src/java/org/apache/cassandra/hints/HintsDispatcher.java @@ -42,6 +42,8 @@ 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.RetryOnDifferentSystemException; +import org.apache.cassandra.exceptions.WriteFailureException; import org.apache.cassandra.exceptions.WriteTimeoutException; import org.apache.cassandra.io.util.File; import org.apache.cassandra.locator.InetAddressAndPort; @@ -248,7 +250,9 @@ final class HintsDispatcher implements AutoCloseable failedRetryDifferentSystem = true; } - if (failures > 0 || timeouts > 0 || failedRetryDifferentSystem) + // The batchlog Accord hints need to return abort if any hint needs to be retried and retry the whole page + // since we don't want hints to ping pong back and forth via hintsNeedingRehinting + if (failures > 0 || timeouts > 0 || failedRetryDifferentSystem || (isBatchLogHints && retryDifferentSystem > 0)) { HintDiagnostics.pageFailureResult(this, success, failures, timeouts, retryDifferentSystem); return Action.ABORT; @@ -534,16 +538,26 @@ final class HintsDispatcher implements AutoCloseable try { IAccordService accord = AccordService.instance(); - TxnResult.Kind kind = accord.getTxnResult(accordTxnResult, true, null, requestTime).kind(); + TxnResult.Kind kind = accord.getTxnResult(accordTxnResult).kind(); if (kind == retry_new_protocol) accordOutcome = RETRY_DIFFERENT_SYSTEM; else accordOutcome = SUCCESS; } + catch (WriteTimeoutException | WriteFailureException | RetryOnDifferentSystemException e) + { + if (e instanceof RetryOnDifferentSystemException) + accordOutcome = RETRY_DIFFERENT_SYSTEM; + else + accordOutcome = TIMEOUT; + String msg = "Accord hint delivery transaction failed retriably"; + if (noSpamLogger.getStatement(msg).shouldLog(Clock.Global.nanoTime())) + logger.error(msg, e); + } catch (Exception e) { - accordOutcome = e instanceof WriteTimeoutException ? TIMEOUT : FAILURE; - String msg = "Accord hint delivery transaction failed"; + accordOutcome = FAILURE; + String msg = "Accord hint delivery transaction failed permanently"; if (noSpamLogger.getStatement(msg).shouldLog(Clock.Global.nanoTime())) logger.error(msg, e); } diff --git a/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java b/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java index 8f357027e9..6629c06256 100644 --- a/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java +++ b/src/java/org/apache/cassandra/index/internal/composites/CompositesSearcher.java @@ -21,17 +21,29 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.ClusteringComparator; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.WriteContext; import org.apache.cassandra.db.filter.ClusteringIndexNamesFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.db.rows.UnfilteredRowIterators; import org.apache.cassandra.db.transform.Transformation; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.index.internal.CassandraIndexSearcher; import org.apache.cassandra.index.internal.IndexEntry; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.utils.btree.BTreeSet; @@ -159,7 +171,7 @@ public class CompositesSearcher extends CassandraIndexSearcher DataLimits.NONE, partitionKey, filter, - null); + (Index.QueryPlan) null); } // by the next caller of next, or through closing this iterator is this come before. diff --git a/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java b/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java index 9baa6f6f47..98e4e02160 100644 --- a/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java +++ b/src/java/org/apache/cassandra/index/internal/keys/KeysSearcher.java @@ -22,12 +22,22 @@ import java.nio.ByteBuffer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.*; +import org.apache.cassandra.db.Clustering; +import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.DeletionTime; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.db.WriteContext; import org.apache.cassandra.db.filter.ColumnFilter; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.filter.RowFilter; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.*; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.db.rows.Rows; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.index.Index; import org.apache.cassandra.index.internal.CassandraIndex; import org.apache.cassandra.index.internal.CassandraIndexSearcher; import org.apache.cassandra.schema.TableMetadata; @@ -91,7 +101,7 @@ public class KeysSearcher extends CassandraIndexSearcher DataLimits.NONE, key, command.clusteringIndexFilter(key), - null); + (Index.QueryPlan) null); // Otherwise, we close right away if empty, and if it's assigned to next it will be called either // by the next caller of next, or through closing this iterator is this come before. diff --git a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java b/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java index 279f7441e4..2f07f76ad4 100644 --- a/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java +++ b/src/java/org/apache/cassandra/index/sai/utils/IndexTermType.java @@ -788,7 +788,7 @@ public class IndexTermType return CompositeType.getInstance(collection.nameComparator(), collection.valueComparator()); } default: - throw new IllegalArgumentException("Unsupported collection type: " + collection.kind + "; index type: " + indexType.name()); + throw new IllegalArgumentException("Unsupported collection type: " + collection.kind); } } diff --git a/src/java/org/apache/cassandra/metrics/TableMetrics.java b/src/java/org/apache/cassandra/metrics/TableMetrics.java index 380333058c..d65bdebcdd 100644 --- a/src/java/org/apache/cassandra/metrics/TableMetrics.java +++ b/src/java/org/apache/cassandra/metrics/TableMetrics.java @@ -820,7 +820,7 @@ public class TableMetrics accordRepairUnexpectedFailures = createTableMeter("AccordRepairUnexpectedFailures", cfs.keyspace.metric.rangeMigrationUnexpectedFailures); accordRepairDependencyLimitFailures = createTableMeter("AccordRepairDependencyLimitFaiures", cfs.keyspace.metric.rangeMigrationDependencyLimitFailures); mutationsRejectedOnWrongSystem = createTableMeter("MutationsRejectedOnWrongSystem", cfs.keyspace.metric.mutationsRejectedOnWrongSystem); - readsRejectedOnWrongSystem = createTableMeter("ReadsRejectedOnWrongSystem", cfs.keyspace.metric.mutationsRejectedOnWrongSystem); + readsRejectedOnWrongSystem = createTableMeter("ReadsRejectedOnWrongSystem", cfs.keyspace.metric.readsRejectedOnWrongSystem); repairsStarted = createTableCounter("RepairJobsStarted"); repairsCompleted = createTableCounter("RepairJobsCompleted"); diff --git a/src/java/org/apache/cassandra/schema/TableParams.java b/src/java/org/apache/cassandra/schema/TableParams.java index d3d059ab79..7c3e52943c 100644 --- a/src/java/org/apache/cassandra/schema/TableParams.java +++ b/src/java/org/apache/cassandra/schema/TableParams.java @@ -27,6 +27,7 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.cql3.Attributes; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.exceptions.ConfigurationException; @@ -244,6 +245,9 @@ public final class TableParams if (cdc && memtable.factory().writesShouldSkipCommitLog()) fail("CDC cannot work if writes skip the commit log. Check your memtable configuration."); + + if (transactionalMode.isTestMode() && !CassandraRelevantProperties.ACCORD_ALLOW_TEST_MODES.getBoolean()) + fail("Transactional mode " + transactionalMode + " can't be used if " + CassandraRelevantProperties.ACCORD_ALLOW_TEST_MODES.getKey() + " is not set"); } private static void fail(String format, Object... args) diff --git a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java index 4219cad301..84dfe49b57 100644 --- a/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java +++ b/src/java/org/apache/cassandra/service/AbstractWriteResponseHandler.java @@ -167,7 +167,8 @@ public abstract class AbstractWriteResponseHandler implements RequestCallback // 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"); + if (coordinatorBehindErrors > 0) + throw new CoordinatorBehindException("Write request failed due to coordinator behind"); } throw new WriteFailureException(replicaPlan.consistencyLevel(), ackCount(), blockFor(), writeType, getFailureReasonByEndpointMap()); diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index 9c939269c9..c640263454 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -22,8 +22,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -43,8 +45,10 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.cache.CacheLoader; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -84,7 +88,6 @@ import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.db.view.ViewUtils; import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.CasWriteTimeoutException; import org.apache.cassandra.exceptions.CasWriteUnknownResultException; @@ -138,9 +141,7 @@ import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult; import org.apache.cassandra.service.accord.txn.TxnData; import org.apache.cassandra.service.accord.txn.TxnDataKeyValue; import org.apache.cassandra.service.accord.txn.TxnDataValue; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; import org.apache.cassandra.service.accord.txn.TxnQuery; -import org.apache.cassandra.service.accord.txn.TxnRangeRead; import org.apache.cassandra.service.accord.txn.TxnRangeReadResult; import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.service.accord.txn.TxnResult; @@ -148,6 +149,7 @@ 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.consensus.migration.ConsensusRequestRouter.SplitReads; import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode; import org.apache.cassandra.service.paxos.Ballot; import org.apache.cassandra.service.paxos.Commit; @@ -181,11 +183,11 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static accord.primitives.Txn.Kind.Read; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.concat; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.apache.cassandra.db.ConsistencyLevel.SERIAL; +import static org.apache.cassandra.db.partitions.PartitionIterators.singletonIterator; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casReadMetrics; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.casWriteMetrics; import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics; @@ -213,6 +215,7 @@ import static org.apache.cassandra.service.consensus.migration.ConsensusMigratio import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.ConsensusRoutingDecision; import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.getTableMetadata; import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.shouldReadEphemerally; +import static org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.splitReadsIntoAccordAndNormal; import static org.apache.cassandra.service.paxos.Ballot.Flag.GLOBAL; import static org.apache.cassandra.service.paxos.Ballot.Flag.LOCAL; import static org.apache.cassandra.service.paxos.BallotGenerator.Global.nextBallot; @@ -1272,7 +1275,7 @@ public class StorageProxy implements StorageProxyMBean { writeMetrics.retryDifferentSystem.mark(); writeMetricsForLevel(consistencyLevel).retryDifferentSystem.mark(); - logger.debug("Retrying mutations on different system because some mutations were misrouted"); + logger.debug("Retrying mutations on different system because some mutations were misrouted according to Cassandra"); Tracing.trace("Got {} from normal mutations, will retry", e); continue; } @@ -1296,9 +1299,13 @@ public class StorageProxy implements StorageProxyMBean if (accordResult != null) { IAccordService accord = AccordService.instance(); - TxnResult.Kind kind = accord.getTxnResult(accordResult, true, consistencyLevel, requestTime).kind(); - if (kind == retry_new_protocol) + TxnResult.Kind kind = accord.getTxnResult(accordResult).kind(); + if (kind == retry_new_protocol && failure == null) + { + Tracing.trace("Accord returned retry new protocol"); + logger.debug("Retrying mutations on different system because some mutations were misrouted according to Accord"); continue; + } Tracing.trace("Successfully wrote Accord mutations"); } } @@ -1400,9 +1407,9 @@ public class StorageProxy implements StorageProxyMBean boolean wroteToBatchLog = false; while (true) { + ClusterMetadata cm = ClusterMetadata.current(); // In case we hit an error in before/during splitting attributeNonAccordLatency = true; - ClusterMetadata cm = ClusterMetadata.current(); List wrappers = new ArrayList<>(mutations.size()); List accordMutations = new ArrayList<>(mutations.size()); BatchlogCleanup cleanup = new BatchlogCleanup(() -> asyncRemoveFromBatchlog(batchlogReplicaPlan, batchUUID, requestTime)); @@ -1506,7 +1513,7 @@ public class StorageProxy implements StorageProxyMBean if (accordResult != null) { IAccordService accord = AccordService.instance(); - TxnResult.Kind kind = accord.getTxnResult(accordResult, true, consistencyLevel, requestTime).kind(); + TxnResult.Kind kind = accord.getTxnResult(accordResult).kind(); if (kind == retry_new_protocol && failure == null) continue; Tracing.trace("Successfully wrote Accord mutations"); @@ -2121,7 +2128,7 @@ public class StorageProxy implements StorageProxyMBean return consistencyLevel.isSerialConsistency() ? readWithConsensus(group, consistencyLevel, requestTime) - : readRegular(group, consistencyLevel, requestTime); + : dispatchReadWithRetryOnDifferentSystem(group, consistencyLevel, ReadCoordinator.DEFAULT, requestTime); } public static boolean hasJoined() @@ -2176,45 +2183,44 @@ public class StorageProxy implements StorageProxyMBean return lastResult.serialReadResult; } - private static ConsistencyLevel consistencyLevelForAccordRead(ClusterMetadata cm, SinglePartitionReadCommand.Group group, @Nullable ConsistencyLevel consistencyLevel) + public static ConsistencyLevel consistencyLevelForAccordRead(ClusterMetadata cm, TableId tableId, SinglePartitionReadCommand.Group group, @Nullable ConsistencyLevel consistencyLevel) { // Null means no specific consistency behavior is required from Accord, it's functionally similar to // reading at ONE if you are reading data that wasn't written via Accord if (consistencyLevel == null) return null; - TableId tableId = group.queries.get(0).metadata().id; TableParams tableParams = getTableMetadata(cm, tableId).params; TransactionalMode mode = tableParams.transactionalMode; TransactionalMigrationFromMode migrationFromMode = tableParams.transactionalMigrationFrom; for (SinglePartitionReadCommand command : group.queries) { - // readCLForStrategy should return either null or the supplied consistency level + // readCLForMode should return either null or the supplied consistency level // in which case we will read everything at that CL since Accord doesn't support per table // read consistency - ConsistencyLevel commitCL = mode.readCLForStrategy(migrationFromMode, consistencyLevel, cm, tableId, command.partitionKey().getToken()); - if (commitCL != null) - return commitCL; + ConsistencyLevel readCL = mode.readCLForMode(migrationFromMode, consistencyLevel, cm, tableId, command.partitionKey().getToken()); + if (readCL != null) + return readCL; } return null; } - public static AsyncTxnResult readWithAccord(ClusterMetadata cm, PartitionRangeReadCommand command, List> ranges, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) { + public static AsyncTxnResult readWithAccord(ClusterMetadata cm, PartitionRangeReadCommand command, AbstractBounds range, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + { if (consistencyLevel != null && !IAccordService.SUPPORTED_READ_CONSISTENCY_LEVELS.contains(consistencyLevel)) throw new InvalidRequestException(consistencyLevel + " is not supported by Accord"); TableMetadata tableMetadata = getTableMetadata(cm, command.metadata().id); TableParams tableParams = tableMetadata.params; - Range readRange = new Range<>(command.dataRange().startKey().getToken(), command.dataRange().stopKey().getToken()); - consistencyLevel = tableParams.transactionalMode.readCLForStrategy(tableParams.transactionalMigrationFrom, consistencyLevel, cm, tableMetadata.id, readRange); - TxnRead read = new TxnRangeRead(command, ranges, consistencyLevel); + consistencyLevel = tableParams.transactionalMode.readCLForMode(tableParams.transactionalMigrationFrom, consistencyLevel, cm, tableMetadata.id, command.dataRange().keyRange()); + TxnRead read = TxnRead.createRangeRead(command, range, consistencyLevel); Txn.Kind kind = shouldReadEphemerally(read.keys(), tableParams, Read); Txn txn = new Txn.InMemory(kind, read.keys(), read, TxnQuery.RANGE_QUERY, null); IAccordService accordService = AccordService.instance(); return accordService.coordinateAsync(tableMetadata.epoch.getEpoch(), txn, consistencyLevel, requestTime); } - private static ConsensusAttemptResult readWithAccord(ClusterMetadata cm, SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + private static AsyncTxnResult readWithAccordAsync(ClusterMetadata cm, SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) { if (consistencyLevel != null && !IAccordService.SUPPORTED_READ_CONSISTENCY_LEVELS.contains(consistencyLevel)) throw new InvalidRequestException(consistencyLevel + " is not supported by Accord"); @@ -2223,20 +2229,25 @@ public class StorageProxy implements StorageProxyMBean // level since Accord will manage reading safely TableMetadata tableMetadata = getTableMetadata(cm, group.metadata().id); TableParams tableParams = tableMetadata.params; - consistencyLevel = consistencyLevelForAccordRead(cm, group, consistencyLevel); - TxnKeyRead read = TxnKeyRead.createSerialRead(group.queries, consistencyLevel); + consistencyLevel = consistencyLevelForAccordRead(cm, group.queries.get(0).metadata().id, group, consistencyLevel); + TxnRead read = TxnRead.createSerialRead(group.queries, consistencyLevel); Txn.Kind kind = shouldReadEphemerally(read.keys(), tableParams, Read); Txn txn = new Txn.InMemory(kind, read.keys(), read, TxnQuery.ALL, null); - AsyncTxnResult asyncTxnResult = AccordService.instance().coordinateAsync(tableMetadata.epoch.getEpoch(), txn, consistencyLevel, requestTime); - return getConsensusAttemptResultFromAsyncTxnResult(asyncTxnResult, group.queries.size(), index -> group.queries.get(index).isReversed(), consistencyLevel, requestTime); + return AccordService.instance().coordinateAsync(tableMetadata.epoch.getEpoch(), txn, consistencyLevel, requestTime); + } + + private static ConsensusAttemptResult readWithAccord(ClusterMetadata cm, SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + { + AsyncTxnResult asyncTxnResult = readWithAccordAsync(cm, group, consistencyLevel, requestTime); + return getConsensusAttemptResultFromAsyncTxnResult(asyncTxnResult, group.queries.size(), index -> group.queries.get(index).isReversed()); } /* * Used for both the SERIAL and non-SERIAL read path into Accord */ - public static ConsensusAttemptResult getConsensusAttemptResultFromAsyncTxnResult(AsyncTxnResult asyncTxnResult, int numQueries, IntPredicate isQueryReversed, ConsistencyLevel cl, Dispatcher.RequestTime requestTime) + public static ConsensusAttemptResult getConsensusAttemptResultFromAsyncTxnResult(AsyncTxnResult asyncTxnResult, int numQueries, IntPredicate isQueryReversed) { - TxnResult txnResult = AccordService.instance().getTxnResult(asyncTxnResult, false, cl, requestTime); + TxnResult txnResult = AccordService.instance().getTxnResult(asyncTxnResult); // TODO (required): Converge on a single approach to RETRY_NEW_PROTOCOL, this works for now because reads don't support it anyways if (txnResult.kind() == retry_new_protocol) return RETRY_NEW_PROTOCOL; @@ -2251,7 +2262,7 @@ public class StorageProxy implements StorageProxyMBean else if (data.size() == 1) { TxnDataKeyValue value = ((TxnDataKeyValue)data.values().iterator().next()); - return serialReadResult(PartitionIterators.singletonIterator(value.rowIterator(isQueryReversed.test(0)))); + return serialReadResult(singletonIterator(value.rowIterator(isQueryReversed.test(0)))); } else { @@ -2263,7 +2274,7 @@ public class StorageProxy implements StorageProxyMBean { int queryIndex = e.getKey(); TxnDataKeyValue value = ((TxnDataKeyValue)e.getValue()); - partitionIterators.set(queryIndex, PartitionIterators.singletonIterator(value.rowIterator(isQueryReversed.test(queryIndex)))); + partitionIterators.set(queryIndex, singletonIterator(value.rowIterator(isQueryReversed.test(queryIndex)))); } return serialReadResult(partitionIterators.size() == 1 ? partitionIterators.get(0) : PartitionIterators.concat(partitionIterators)); } @@ -2364,45 +2375,124 @@ public class StorageProxy implements StorageProxyMBean } } + public static PartitionIterator dispatchReadWithRetryOnDifferentSystem(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ReadCoordinator coordinator, Dispatcher.RequestTime requestTime) + throws UnavailableException, ReadFailureException, ReadTimeoutException + { + while (true) + { + ClusterMetadata cm = ClusterMetadata.current(); + try + { + SplitReads splitReads = splitReadsIntoAccordAndNormal(cm, group, coordinator, requestTime); + SinglePartitionReadCommand.Group accordReads = splitReads.accordReads; + AsyncTxnResult accordResult = accordReads != null ? readWithAccordAsync(cm, accordReads, consistencyLevel, requestTime) : null; + SinglePartitionReadCommand.Group normalReads = splitReads.normalReads; + Tracing.trace("Split reads into Accord {} and normal {}", accordReads, normalReads); + + Throwable failure = null; + PartitionIterator normalPartitions = null; + try + { + if (normalReads != null) + { + normalPartitions = readRegular(normalReads, consistencyLevel, coordinator, requestTime); + Tracing.trace("Successfully executed normal reads"); + } + } + catch (RetryOnDifferentSystemException e) + { + readMetrics.retryDifferentSystem.mark(); + readMetricsForLevel(consistencyLevel).retryDifferentSystem.mark(); + logger.debug("Retrying reads on different system because some reads were misrouted according to Accord"); + Tracing.trace("Got {} from normal reads, will retry", e); + continue; + } + catch (CoordinatorBehindException e) + { + readMetrics.retryCoordinatorBehind.mark(); + readMetricsForLevel(consistencyLevel).retryCoordinatorBehind.mark(); + logger.debug("Retrying reads now that coordinator has caught up to cluster metadata"); + Tracing.trace("Got {} from normal reads, will retry", e); + continue; + } + catch (Exception e) + { + failure = Throwables.merge(failure, e); + } + + // Check if the Accord reads succeeded asynchronously + PartitionIterator accordPartitions = null; + try + { + if (accordResult != null) + { + ConsensusAttemptResult consensusResult = getConsensusAttemptResultFromAsyncTxnResult(accordResult, accordReads.queries.size(), index -> group.queries.get(index).isReversed()); + if (consensusResult == RETRY_NEW_PROTOCOL) + { + readMetrics.retryDifferentSystem.mark(); + readMetricsForLevel(consistencyLevel).retryDifferentSystem.mark(); + Tracing.trace("Accord returned retry new protocol"); + logger.debug("Retrying reads on different system because some reads were misrouted according to Accord"); + continue; + } + Tracing.trace("Successfully executed Accord reads"); + accordPartitions = consensusResult.serialReadResult; + } + } + catch (Exception e) + { + failure = Throwables.merge(failure, e); + } + + if (failure != null) + throw unchecked(failure); + + PartitionIterator resultIterator = null; + if (normalPartitions != null && (accordPartitions == null || !accordPartitions.hasNext())) + resultIterator = normalPartitions; + else if ((normalPartitions == null || !normalPartitions.hasNext()) && accordPartitions != null) + resultIterator = accordPartitions; + else + { + // Merge into partition key order + List partitions = new ArrayList<>(group.queries.size()); + Iterator mergeIterator = Iterators.mergeSorted(ImmutableList.of(normalPartitions, accordPartitions), Comparator.comparing(RowIterator::partitionKey)); + while (mergeIterator.hasNext()) + { + partitions.add(singletonIterator(mergeIterator.next())); + } + resultIterator = PartitionIterators.concat(partitions);} + return maybeEnforceLimits(resultIterator, group); + } + catch (Exception t) + { + // Unexpected error so it would be helpful to have details + Tracing.trace("{}", getStackTraceAsToString(t)); + throw t; + } + } + } + + public static PartitionIterator maybeEnforceLimits(PartitionIterator iterator, SinglePartitionReadCommand.Group group) + { + // Note that the only difference between the command in a group must be the partition key on which + // they applied. + boolean enforceStrictLiveness = group.queries.get(0).metadata().enforceStrictLiveness(); + // If we have more than one command, then despite each read command honoring the limit, the total result + // might not honor it and so we should enforce it + if (group.queries.size() > 1) + return group.limits().filter(iterator, group.nowInSec(), group.selectsFullPartition(), enforceStrictLiveness); + return iterator; + } + @SuppressWarnings("resource") - public static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ReadCoordinator coordinator, Dispatcher.RequestTime requestTime) + private static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, ReadCoordinator coordinator, Dispatcher.RequestTime requestTime) throws UnavailableException, ReadFailureException, ReadTimeoutException { long start = nanoTime(); try { - ClusterMetadata cm = ClusterMetadata.current(); - TableId tableId = group.queries.get(0).metadata().id; - // Returns null for local tables - TableMetadata tableMetadata = getTableMetadata(cm, tableId); - if (tableMetadata == null) - tableMetadata = Schema.instance.localKeyspaces().getTableOrViewNullable(tableId); - TableParams tableParams = tableMetadata.params; - - TransactionalMode transactionalMode = tableParams.transactionalMode; -// TransactionalMigrationFromMode transactionalMigrationFromMode = tableParams.transactionalMigrationFrom; - // TODO (required): Tests would fail with this and we need to add live migration support anyways so for now allow it -// if (transactionalMigrationFromMode != TransactionalMigrationFromMode.none) -// throw new UnsupportedOperationException("Live migration is not supported, can't safely read when migrating from " + transactionalMigrationFromMode + " to " + transactionalMode); - - PartitionIterator result; - if (transactionalMode.nonSerialReadsThroughAccord && coordinator.isEventuallyConsistent()) - { - ConsensusAttemptResult consensusAttemptResult = readWithAccord(cm, group, consistencyLevel, requestTime); - checkState(!consensusAttemptResult.shouldRetryOnNewConsensusProtocol, "Live migration is not supported with non-SERIAL reads yet"); - result = consensusAttemptResult.serialReadResult; - } - else - result = fetchRows(group.queries, consistencyLevel, coordinator, requestTime); - - // Note that the only difference between the command in a group must be the partition key on which - // they applied. - boolean enforceStrictLiveness = tableMetadata.enforceStrictLiveness(); - // If we have more than one command, then despite each read command honoring the limit, the total result - // might not honor it and so we should enforce it - if (group.queries.size() > 1) - result = group.limits().filter(result, group.nowInSec(), group.selectsFullPartition(), enforceStrictLiveness); - return result; + return fetchRows(group.queries, consistencyLevel, coordinator, requestTime); } catch (UnavailableException e) { @@ -2444,11 +2534,6 @@ public class StorageProxy implements StorageProxyMBean } } - public static PartitionIterator readRegular(SinglePartitionReadCommand.Group group, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) - { - return readRegular(group, consistencyLevel, ReadCoordinator.DEFAULT, requestTime); - } - public static void recordReadRegularAbort(ConsistencyLevel consistencyLevel, Throwable cause) { readMetrics.markAbort(cause); @@ -2493,8 +2578,11 @@ public class StorageProxy implements StorageProxyMBean * 3. Wait for a response from R replicas * 4. If the digests (if any) match the data return the data * 5. else carry out read repair by getting data from all the nodes. + * + * This should not be called directly because it bypasses statistics and error handling. It is public + * so it can be used by Accord to fetch rows and the statistics will be tracked by Accord. */ - private static PartitionIterator fetchRows(List commands, + public static PartitionIterator fetchRows(List commands, ConsistencyLevel consistencyLevel, ReadCoordinator coordinator, Dispatcher.RequestTime requestTime) diff --git a/src/java/org/apache/cassandra/service/StorageService.java b/src/java/org/apache/cassandra/service/StorageService.java index 3615f2c7e2..be6699f292 100644 --- a/src/java/org/apache/cassandra/service/StorageService.java +++ b/src/java/org/apache/cassandra/service/StorageService.java @@ -1677,14 +1677,12 @@ public class StorageService extends NotificationBroadcasterSupport implements IE } @Override - public void migrateConsensusProtocol(@Nonnull String targetProtocol, - @Nonnull List keyspaceNames, + public void migrateConsensusProtocol(@Nonnull List keyspaceNames, @Nullable List maybeTableNames, @Nullable String maybeRangesStr) { - checkNotNull(targetProtocol, "targetProtocol is null"); checkArgument(!keyspaceNames.contains(SchemaConstants.METADATA_KEYSPACE_NAME)); - startMigrationToConsensusProtocol(targetProtocol, keyspaceNames, Optional.ofNullable(maybeTableNames), Optional.ofNullable(maybeRangesStr)); + startMigrationToConsensusProtocol(keyspaceNames, Optional.ofNullable(maybeTableNames), Optional.ofNullable(maybeRangesStr)); } @Override diff --git a/src/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/java/org/apache/cassandra/service/StorageServiceMBean.java index deb0b249d3..73e9fcd680 100644 --- a/src/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -1142,8 +1142,7 @@ public interface StorageServiceMBean extends NotificationEmitter public String getBootstrapState(); void abortBootstrap(String nodeId, String endpoint); - void migrateConsensusProtocol(@Nonnull String targetProtocol, - @Nullable List keyspaceNames, + void migrateConsensusProtocol(@Nullable List keyspaceNames, @Nullable List maybeTableNames, @Nullable String maybeRangesStr); diff --git a/src/java/org/apache/cassandra/service/accord/AccordCallback.java b/src/java/org/apache/cassandra/service/accord/AccordCallback.java deleted file mode 100644 index 6e658948a4..0000000000 --- a/src/java/org/apache/cassandra/service/accord/AccordCallback.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.service.accord; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import accord.coordinate.Timeout; -import accord.local.AgentExecutor; -import accord.messages.Callback; -import accord.messages.Reply; -import accord.messages.SafeCallback; -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; - -class AccordCallback extends SafeCallback implements RequestCallback -{ - private static final Logger logger = LoggerFactory.getLogger(AccordCallback.class); - private final AccordEndpointMapper endpointMapper; - - public AccordCallback(AgentExecutor executor, Callback callback, AccordEndpointMapper endpointMapper) - { - super(executor, callback); - this.endpointMapper = endpointMapper; - } - - @Override - public void onResponse(Message msg) - { - logger.trace("Received response {} from {}", msg.payload, msg.from()); - success(endpointMapper.mappedId(msg.from()), msg.payload); - } - - private static Throwable convertFailureMessage(RequestFailure failure) - { - return failure.reason == RequestFailureReason.TIMEOUT ? - new Timeout(null, null) : - new RuntimeException(failure.failure); - } - - @Override - public void onFailure(InetAddressAndPort from, RequestFailure failure) - { - logger.trace("Received failure {} from {} for {}", failure, from, this); - // TODO (now): we should distinguish timeout failures with some placeholder Exception - failure(endpointMapper.mappedId(from), convertFailureMessage(failure)); - } - - @Override - public boolean trackLatencyForSnitch() - { - return true; - } - - @Override - public boolean invokeOnFailure() - { - return true; - } -} diff --git a/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java index 4cf16f0241..0dbf99635a 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java +++ b/src/java/org/apache/cassandra/service/accord/AccordConfigurationService.java @@ -52,13 +52,16 @@ import org.apache.cassandra.repair.SharedContext; import org.apache.cassandra.service.accord.AccordKeyspace.EpochDiskState; 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.tcm.membership.NodeState; import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Simulate; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.apache.cassandra.utils.concurrent.Future; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; +import static org.apache.cassandra.service.accord.AccordTopology.tcmIdToAccord; import static org.apache.cassandra.utils.Simulate.With.MONITORS; // TODO: listen to FailureDetector and rearrange fast path accordingly @@ -344,16 +347,22 @@ public class AccordConfigurationService extends AbstractConfigurationService= topology.epoch()) checkIfNodesRemoved(topology); - else epochs.acknowledgeFuture(topology.epoch()).addCallback(() -> checkIfNodesRemoved(topology)); + Set stillLiveNodes = metadata.directory.states.entrySet() + .stream() + .filter(e -> e.getValue() != NodeState.LEFT && e.getValue() != NodeState.LEAVING) + .map(e -> tcmIdToAccord(e.getKey())) + .collect(Collectors.toSet()); + if (epochs.lastAcknowledged() >= topology.epoch()) checkIfNodesRemoved(topology, stillLiveNodes); + else epochs.acknowledgeFuture(topology.epoch()).addCallback(() -> checkIfNodesRemoved(topology, stillLiveNodes)); } - private void checkIfNodesRemoved(Topology topology) + private void checkIfNodesRemoved(Topology topology, Set stillLiveNodes) { if (epochs.minEpoch() == topology.epoch()) return; Topology previous = getTopologyForEpoch(topology.epoch() - 1); // for all nodes removed, or pending removal, mark them as removed so we don't wait on their replies - Sets.SetView removedNodes = Sets.difference(previous.nodes(), topology.nodes()); + Set removedNodes = Sets.difference(previous.nodes(), topology.nodes()); + removedNodes = Sets.filter(removedNodes, id -> !stillLiveNodes.contains(id)); // TODO (desired, efficiency): there should be no need to notify every epoch for every removed node for (Node.Id removedNode : removedNodes) { @@ -420,29 +429,36 @@ public class AccordConfigurationService extends AbstractConfigurationService peers = new HashSet<>(ClusterMetadata.current().directory.allJoinedEndpoints()); - peers.remove(FBUtilities.getBroadcastAddressAndPort()); - if (peers.isEmpty()) - return; - Topology topology; - while ((topology =FetchTopology.fetch(SharedContext.Global.instance, peers, epoch).get()) == null) {} - reportTopology(topology); - } - catch (InterruptedException e) - { - if (currentEpoch() >= epoch) - return; - Thread.currentThread().interrupt(); - throw new UncheckedInterruptedException(e); - } - catch (Throwable e) - { - if (currentEpoch() >= epoch) - return; - throw new RuntimeException(e.getCause()); - } + // It's not safe for this to block on CMS so for now pick a thread pool to handle it + Stage.ACCORD_MIGRATION.execute(() -> { + if (ClusterMetadata.current().epoch.getEpoch() < epoch) + ClusterMetadataService.instance().fetchLogFromCMS(Epoch.create(epoch)); + try + { + Set peers = new HashSet<>(ClusterMetadata.current().directory.allJoinedEndpoints()); + peers.remove(FBUtilities.getBroadcastAddressAndPort()); + if (peers.isEmpty()) + return; + Topology topology; + while ((topology = FetchTopology.fetch(SharedContext.Global.instance, peers, epoch).get()) == null) + { + } + reportTopology(topology); + } + catch (InterruptedException e) + { + if (currentEpoch() >= epoch) + return; + Thread.currentThread().interrupt(); + throw new UncheckedInterruptedException(e); + } + catch (Throwable e) + { + if (currentEpoch() >= epoch) + return; + throw new RuntimeException(e.getCause()); + } + }); } @Override diff --git a/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java index fdcd4f5391..346b1015c5 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java +++ b/src/java/org/apache/cassandra/service/accord/AccordObjectSizes.java @@ -50,6 +50,7 @@ import accord.primitives.Ranges; import accord.primitives.Routable.Domain; import accord.primitives.RoutingKeys; import accord.primitives.SaveStatus; +import accord.primitives.Seekable; import accord.primitives.Seekables; import accord.primitives.Status; import accord.primitives.Timestamp; @@ -94,6 +95,16 @@ public class AccordObjectSizes return EMPTY_RANGE_SIZE + key(range.start()) + key(range.end()); } + public static long seekable(Seekable seekable) + { + switch (seekable.domain()) + { + default: throw new AssertionError(); + case Key: return key((Key) seekable); + case Range: return range((Range) seekable); + } + } + private static final long EMPTY_RANGES_SIZE = measure(Ranges.of()); public static long ranges(Ranges ranges) { diff --git a/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java b/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java index 16b48c8399..330043d351 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java +++ b/src/java/org/apache/cassandra/service/accord/AccordResponseVerbHandler.java @@ -62,6 +62,7 @@ class AccordResponseVerbHandler implements IVerbHandler } Node.Id from = endpointMapper.mappedId(message.from()); + logger.trace("Receiving {} from {}", message.payload, message.from()); if (message.isFailureResponse()) { Tracing.trace("Processing failure response from {}", message.from()); diff --git a/src/java/org/apache/cassandra/service/accord/AccordService.java b/src/java/org/apache/cassandra/service/accord/AccordService.java index 0c89bcb734..fc6c68dd56 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordService.java +++ b/src/java/org/apache/cassandra/service/accord/AccordService.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -118,11 +119,11 @@ 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.dht.AccordSplitter; +import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestTimeoutException; @@ -156,6 +157,7 @@ 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.RetryWithNewProtocolResult; import org.apache.cassandra.service.accord.txn.TxnResult; import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.service.consensus.migration.TableMigrationState; @@ -609,12 +611,14 @@ public class AccordService implements IAccordService, Shutdownable // 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) + if (tm.params.transactionalMode != TransactionalMode.off || tm.params.transactionalMigrationFrom.migratingFromAccord()) { 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; + // Use migratingAndMigratedRanges (not accordSafeToReadRanges) because barriers are allowed even if Accord can't perform + // a read because they are only finishing/recovering existing Accord transactions Ranges migratingAndMigratedRanges = AccordTopology.toAccordRanges(tms.tableId, tms.migratingAndMigratedRanges); return keysOrRanges.slice(migratingAndMigratedRanges); } @@ -739,6 +743,34 @@ public class AccordService implements IAccordService, Shutdownable return node.topology(); } + private static Set txnDroppedTables(Seekables keys) + { + Set tables = new HashSet<>(); + for (Seekable seekable : keys) + { + switch (seekable.domain()) + { + default: + throw new IllegalStateException("Unhandled domain " + seekable.domain()); + case Key: + tables.add(((PartitionKey) seekable).table()); + break; + case Range: + tables.add(((TokenRange) seekable).table()); + break; + } + } + + Iterator tablesIterator = tables.iterator(); + while (tablesIterator.hasNext()) + { + TableId table = tablesIterator.next(); + if (Schema.instance.getTableMetadata(table) != null) + tablesIterator.remove(); + } + return tables; + } + /** * Consistency level is just echoed back in timeouts, in the future it may be used for interoperability * with non-Accord operations. @@ -747,7 +779,7 @@ public class AccordService implements IAccordService, Shutdownable public @Nonnull TxnResult coordinate(long minEpoch, @Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, @Nonnull Dispatcher.RequestTime requestTime) { AsyncTxnResult asyncTxnResult = coordinateAsync(minEpoch, txn, consistencyLevel, requestTime); - return getTxnResult(asyncTxnResult, txn.isWrite(), consistencyLevel, requestTime); + return getTxnResult(asyncTxnResult); } @Override @@ -769,7 +801,8 @@ public class AccordService implements IAccordService, Shutdownable metrics.keySize.update(txn.keys().size()); long deadlineNanos = requestTime.computeDeadline(DatabaseDescriptor.getTransactionTimeout(NANOSECONDS)); AsyncResult asyncResult = node.coordinate(txnId, txn, minEpoch, deadlineNanos); - AsyncTxnResult asyncTxnResult = new AsyncTxnResult(txnId); + Seekables keys = txn.keys(); + AsyncTxnResult asyncTxnResult = new AsyncTxnResult(txnId, minEpoch, consistencyLevel, txn.isWrite(), requestTime); asyncResult.addCallback((success, failure) -> { long durationNanos = nanoTime() - requestTime.startedAtNanos(); sharedMetrics.addNano(durationNanos); @@ -786,6 +819,9 @@ public class AccordService implements IAccordService, Shutdownable return; } + sharedMetrics.failures.mark(); + metrics.failures.mark(); + if (cause instanceof Timeout) { // Don't mark the metric here, should be done in getTxnResult to ensure it only happens once @@ -795,7 +831,6 @@ public class AccordService implements IAccordService, Shutdownable } if (cause instanceof Preempted || cause instanceof Invalidated) { - sharedMetrics.timeouts.mark(); metrics.preempted.mark(); //TODO need to improve // Coordinator "could" query the accord state to see whats going on but that doesn't exist yet. @@ -803,25 +838,39 @@ public class AccordService implements IAccordService, Shutdownable asyncTxnResult.tryFailure(newPreempted(txnId, txn.isWrite(), consistencyLevel)); return; } - sharedMetrics.failures.mark(); + // TODO (desired): It would be better to check if the topology mismatch is due to Accord ownership changes + // or the txn accessing tables that don't exist or something. if (cause instanceof TopologyMismatch) { + // Excluding bugs topology mismatch can occur because a table was dropped in between creating the txn + // and executing it. + // It could also race with the table stopping/starting being managed by Accord. + // The caller can retry if the table indeed exists and is managed by Accord. + Set txnDroppedTables = txnDroppedTables(keys); + Tracing.trace("Accord returned topology mismatch: " + cause.getMessage()); + logger.debug("Accord returned topology mismatch", cause); metrics.topologyMismatches.mark(); - asyncTxnResult.tryFailure(RequestValidations.invalidRequest(cause.getMessage())); + // Throw IRE in case the caller fails to check if the table still exists + if (!txnDroppedTables.isEmpty()) + { + Tracing.trace("Accord txn uses dropped tables {}", txnDroppedTables); + logger.debug("Accord txn uses dropped tables {}", txnDroppedTables); + asyncTxnResult.setFailure(new InvalidRequestException("Accord transaction uses dropped tables")); + } + asyncTxnResult.setSuccess(RetryWithNewProtocolResult.instance); return; } - metrics.failures.mark(); asyncTxnResult.tryFailure(new RuntimeException(cause)); }); return asyncTxnResult; } @Override - public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult) { ClientRequestMetrics sharedMetrics; AccordClientRequestMetrics metrics; - if (isWrite) + if (asyncTxnResult.isWrite) { sharedMetrics = ClientRequestsMetricsHolder.writeMetrics; metrics = accordWriteMetrics; @@ -833,7 +882,7 @@ public class AccordService implements IAccordService, Shutdownable } try { - long deadlineNanos = requestTime.computeDeadline(DatabaseDescriptor.getTransactionTimeout(NANOSECONDS)); + long deadlineNanos = asyncTxnResult.requestTime.computeDeadline(DatabaseDescriptor.getTransactionTimeout(NANOSECONDS)); TxnResult result = asyncTxnResult.get(deadlineNanos - nanoTime(), NANOSECONDS); return result; } @@ -851,6 +900,8 @@ public class AccordService implements IAccordService, Shutdownable } else if (cause instanceof RuntimeException) throw (RuntimeException) cause; + else if (cause instanceof InvalidRequestException) + throw ((InvalidRequestException)cause); else throw new RuntimeException(cause); } @@ -864,7 +915,7 @@ public class AccordService implements IAccordService, Shutdownable { metrics.timeouts.mark(); sharedMetrics.timeouts.mark(); - throw newTimeout(asyncTxnResult.txnId, isWrite, consistencyLevel); + throw newTimeout(asyncTxnResult.txnId, asyncTxnResult.isWrite, asyncTxnResult.consistencyLevel); } } diff --git a/src/java/org/apache/cassandra/service/accord/AccordTopology.java b/src/java/org/apache/cassandra/service/accord/AccordTopology.java index 83c74d82fa..8b51a923a1 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordTopology.java +++ b/src/java/org/apache/cassandra/service/accord/AccordTopology.java @@ -18,17 +18,25 @@ package org.apache.cassandra.service.accord; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import accord.local.Node.Id; -import accord.primitives.Ranges; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; +import accord.local.Node.Id; +import accord.primitives.Ranges; import accord.topology.Shard; import accord.topology.Topology; import accord.utils.Invariants; @@ -38,7 +46,13 @@ import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.InetAddressAndPort; -import org.apache.cassandra.schema.*; +import org.apache.cassandra.schema.Diff; +import org.apache.cassandra.schema.DistributedSchema; +import org.apache.cassandra.schema.KeyspaceMetadata; +import org.apache.cassandra.schema.Keyspaces; +import org.apache.cassandra.schema.ReplicationParams; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.accord.api.AccordRoutingKey.SentinelKey; import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.accord.fastpath.FastPathStrategy; diff --git a/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java index 0fdf3d9479..48cd307ccd 100644 --- a/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java +++ b/src/java/org/apache/cassandra/service/accord/AccordVerbHandler.java @@ -27,6 +27,9 @@ import accord.local.Node; import accord.messages.Request; import org.apache.cassandra.net.IVerbHandler; import org.apache.cassandra.net.Message; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.NoSpamLogger; public class AccordVerbHandler implements IVerbHandler @@ -53,6 +56,7 @@ public class AccordVerbHandler implements IVerbHandler } logger.trace("Receiving {} from {}", message.payload, message.from()); + T request = message.payload; /* @@ -62,14 +66,32 @@ public class AccordVerbHandler implements IVerbHandler */ Node.Id fromNodeId = endpointMapper.mappedId(message.from()); long waitForEpoch = request.waitForEpoch(); - - if (node.topology().hasAtLeastEpoch(waitForEpoch)) + ClusterMetadata cm = ClusterMetadata.current(); + boolean cmUpToDate = ClusterMetadata.current().epoch.getEpoch() >= waitForEpoch; + if (node.topology().hasAtLeastEpoch(waitForEpoch) && cmUpToDate) request.process(node, fromNodeId, message.header); else - 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.header); - }); + { + // withEpoch does not reliably ensure that TCM is up to date, if Accord has the topology it won't + // wait for TCM to come up to date, so do it here in the verb handler + if (!cmUpToDate) + { + ClusterMetadataService.instance().fetchLogFromPeerOrCMSAsync(cm, message.from(), Epoch.create(waitForEpoch)).addCallback((success, failure) -> + 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.header); + }) + ); + } + else + { + 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.header); + }); + } + } } } diff --git a/src/java/org/apache/cassandra/service/accord/IAccordService.java b/src/java/org/apache/cassandra/service/accord/IAccordService.java index 068c4c1b19..8750745993 100644 --- a/src/java/org/apache/cassandra/service/accord/IAccordService.java +++ b/src/java/org/apache/cassandra/service/accord/IAccordService.java @@ -110,17 +110,27 @@ public interface IAccordService class AsyncTxnResult extends AsyncPromise { public final @Nonnull TxnId txnId; + public final long minEpoch; + @Nullable + public final ConsistencyLevel consistencyLevel; + public final boolean isWrite; + public final Dispatcher.RequestTime requestTime; - public AsyncTxnResult(@Nonnull TxnId txnId) + public AsyncTxnResult(@Nonnull TxnId txnId, long minEpoch, @Nullable ConsistencyLevel consistencyLevel, boolean isWrite, @Nonnull Dispatcher.RequestTime requestTime) { checkNotNull(txnId); + checkNotNull(requestTime); this.txnId = txnId; + this.minEpoch = minEpoch; + this.consistencyLevel = consistencyLevel; + this.isWrite = isWrite; + this.requestTime = requestTime; } } @Nonnull AsyncTxnResult coordinateAsync(long minEpoch, @Nonnull Txn txn, @Nonnull ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime); - TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime); + TxnResult getTxnResult(AsyncTxnResult asyncTxnResult); long currentEpoch(); @@ -236,7 +246,7 @@ public interface IAccordService } @Override - public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) + public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult) { throw new UnsupportedOperationException("No accord transaction should be executed when accord.enabled = false in cassandra.yaml"); } @@ -418,9 +428,9 @@ public interface IAccordService } @Override - public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult, boolean isWrite, @Nullable ConsistencyLevel consistencyLevel, RequestTime requestTime) + public TxnResult getTxnResult(AsyncTxnResult asyncTxnResult) { - return delegate.getTxnResult(asyncTxnResult, isWrite, consistencyLevel, requestTime); + return delegate.getTxnResult(asyncTxnResult); } @Override diff --git a/src/java/org/apache/cassandra/service/accord/TokenRange.java b/src/java/org/apache/cassandra/service/accord/TokenRange.java index 1f684fb4b8..8dd96cfaaa 100644 --- a/src/java/org/apache/cassandra/service/accord/TokenRange.java +++ b/src/java/org/apache/cassandra/service/accord/TokenRange.java @@ -34,10 +34,14 @@ import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.accord.api.AccordRoutingKey; import org.apache.cassandra.service.accord.api.AccordRoutingKey.SentinelKey; +import org.apache.cassandra.utils.ObjectSizes; public class TokenRange extends Range.EndInclusive { - public TokenRange(AccordRoutingKey start, AccordRoutingKey end) + public static final long EMPTY_SIZE = ObjectSizes.measure(new TokenRange(SentinelKey.min(TableId.fromLong(0)), SentinelKey.max(TableId.fromLong(0)))); + + // Don't make this public use create or createUnsafe + private TokenRange(AccordRoutingKey start, AccordRoutingKey end) { super(start, end); } @@ -72,6 +76,11 @@ public class TokenRange extends Range.EndInclusive return (AccordRoutingKey) super.end(); } + public long estimatedSizeOnHeap() + { + return EMPTY_SIZE + start().estimatedSizeOnHeap() + end().estimatedSizeOnHeap(); + } + public boolean isFullRange() { return start().kindOfRoutingKey() == AccordRoutingKey.RoutingKeyKind.SENTINEL && end().kindOfRoutingKey() == AccordRoutingKey.RoutingKeyKind.SENTINEL; @@ -94,7 +103,12 @@ public class TokenRange extends Range.EndInclusive return new TokenRange((AccordRoutingKey) start, (AccordRoutingKey) end); } - public org.apache.cassandra.dht.Range toKeyspaceRange () + /* + * This behaves quite incorrectly with MinTokenKey because it loses the inclusivity of MinTokenKey in the conversion. + * It's not a problem for cluster metadata and topology, but it's quite wrong for queries that convert from Bounds to + * Range. + */ + public org.apache.cassandra.dht.Range toKeyspaceRange() { IPartitioner partitioner = DatabaseDescriptor.getPartitioner(); AccordRoutingKey start = start(); @@ -104,7 +118,6 @@ public class TokenRange extends Range.EndInclusive return new org.apache.cassandra.dht.Range<>(left, right); } - public static final Serializer serializer = new Serializer(); public static final class Serializer implements IVersionedSerializer diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java index c5561be97c..8ee5957209 100644 --- a/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java +++ b/src/java/org/apache/cassandra/service/accord/api/AccordAgent.java @@ -52,8 +52,8 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.metrics.AccordMetrics; import org.apache.cassandra.net.ResponseContext; import org.apache.cassandra.service.accord.AccordService; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; import org.apache.cassandra.service.accord.txn.TxnQuery; +import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.JVMStabilityInspector; @@ -174,7 +174,7 @@ public class AccordAgent implements Agent @Override public Txn emptySystemTxn(Kind kind, Routable.Domain domain) { - return new Txn.InMemory(kind, domain == Key ? Keys.EMPTY : Ranges.EMPTY, TxnKeyRead.EMPTY, TxnQuery.UNSAFE_EMPTY, null); + return new Txn.InMemory(kind, domain == Key ? Keys.EMPTY : Ranges.EMPTY, TxnRead.empty(domain), TxnQuery.UNSAFE_EMPTY, null); } @Override diff --git a/src/java/org/apache/cassandra/service/accord/api/AccordRoutingKey.java b/src/java/org/apache/cassandra/service/accord/api/AccordRoutingKey.java index 9088faccc3..61454eab64 100644 --- a/src/java/org/apache/cassandra/service/accord/api/AccordRoutingKey.java +++ b/src/java/org/apache/cassandra/service/accord/api/AccordRoutingKey.java @@ -240,7 +240,7 @@ public abstract class AccordRoutingKey extends AccordRoutableKey implements Rout public Range asRange() { checkState(!isMinMinSentinel, "It might be possible to support converting a minmin sentinel to a range, but it needs to be evaluated in the context where it is failing"); - return new TokenRange(new SentinelKey(table, isMinSentinel, true), this); + return TokenRange.create(new SentinelKey(table, isMinSentinel, true), this); } } @@ -373,7 +373,7 @@ public abstract class AccordRoutingKey extends AccordRoutableKey implements Rout ? new SentinelKey(table, true, false) : new TokenKey(table, token.decreaseSlightly()); - return new TokenRange(before, this); + return TokenRange.create(before, this); } public MinTokenKey(TableId tableId, Token token) diff --git a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropAdapter.java b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropAdapter.java index 574d962a96..fb3d8a606c 100644 --- a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropAdapter.java +++ b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropAdapter.java @@ -107,7 +107,7 @@ public class AccordInteropAdapter extends AbstractTxnAdapter if (updateKind != AccordUpdate.Kind.UNRECOVERABLE_REPAIR && (consistencyLevel == null || consistencyLevel == ConsistencyLevel.ONE || txn.read().keys().isEmpty())) return false; - new AccordInteropExecution(node, txnId, txn, updateKind, route, txn.read().keys().toParticipants(), executeAt, deps, callback, executor, consistencyLevel, endpointMapper) + new AccordInteropExecution(node, txnId, txn, updateKind, route, executeAt, deps, callback, executor, consistencyLevel, endpointMapper) .start(); return true; } diff --git a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropExecution.java b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropExecution.java index 0a6a10f5ae..0bc6cdbe32 100644 --- a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropExecution.java +++ b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropExecution.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; @@ -80,19 +79,16 @@ import org.apache.cassandra.service.accord.AccordEndpointMapper; import org.apache.cassandra.service.accord.TokenRange; import org.apache.cassandra.service.accord.api.AccordAgent; import org.apache.cassandra.service.accord.api.AccordRoutingKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.interop.AccordInteropReadCallback.MaximalCommitSender; import org.apache.cassandra.service.accord.txn.AccordUpdate; import org.apache.cassandra.service.accord.txn.TxnData; -import org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind; import org.apache.cassandra.service.accord.txn.TxnDataKeyValue; import org.apache.cassandra.service.accord.txn.TxnDataRangeValue; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; -import org.apache.cassandra.service.accord.txn.TxnRangeRead; +import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.service.accord.txn.UnrecoverableRepairUpdate; import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; -import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration; -import org.apache.cassandra.service.consensus.migration.TableMigrationState; import org.apache.cassandra.service.reads.ReadCoordinator; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.Dispatcher; @@ -151,7 +147,6 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen private final TxnId txnId; private final Txn txn; private final FullRoute route; - private final Participants readScope; private final Timestamp executeAt; private final Deps deps; private final BiConsumer callback; @@ -169,7 +164,7 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen private final Set contacted; private final AccordUpdate.Kind updateKind; - public AccordInteropExecution(Node node, TxnId txnId, Txn txn, AccordUpdate.Kind updateKind, FullRoute route, Participants readScope, Timestamp executeAt, Deps deps, BiConsumer callback, + public AccordInteropExecution(Node node, TxnId txnId, Txn txn, AccordUpdate.Kind updateKind, FullRoute route, Timestamp executeAt, Deps deps, BiConsumer callback, AgentExecutor executor, ConsistencyLevel consistencyLevel, AccordEndpointMapper endpointMapper) { requireArgument(!txn.read().keys().isEmpty() || updateKind == AccordUpdate.Kind.UNRECOVERABLE_REPAIR); @@ -177,7 +172,6 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen this.txnId = txnId; this.txn = txn; this.route = route; - this.readScope = readScope; this.executeAt = executeAt; this.deps = deps; this.callback = callback; @@ -233,123 +227,125 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen public void sendReadCommand(Message message, InetAddressAndPort to, RequestCallback callback) { Node.Id id = endpointMapper.mappedId(to); - SinglePartitionReadCommand command = (SinglePartitionReadCommand) message.payload; + // TODO (nicetohave): It would be better to use the re-use the command from the transaction but it's fragile + // to try and figure out exactly what changed for things like read repair and short read protection + // Also this read scope doesn't reflect the contents of this particular read and is larger than it needs to be // TODO (required): understand interop and whether StableFastPath is appropriate - AccordInteropStableThenRead commit = new AccordInteropStableThenRead(id, allTopologies, txnId, Kind.StableFastPath, executeAt, txn, deps, route, command); + AccordInteropStableThenRead commit = new AccordInteropStableThenRead(id, allTopologies, txnId, Kind.StableFastPath, executeAt, txn, deps, route, message.payload); node.send(id, commit, executor, new AccordInteropRead.ReadCallback(id, to, message, callback, this)); } @Override public void sendReadRepairMutation(Message message, InetAddressAndPort to, RequestCallback callback) { - requireArgument(message.payload.allowsPotentialTransactionConflicts()); + requireArgument(message.payload.potentialTxnConflicts().allowed); + requireArgument(message.payload.getTableIds().size() == 1); Node.Id id = endpointMapper.mappedId(to); + Participants readScope = Participants.singleton(txn.read().keys().domain(), new TokenKey(message.payload.getTableIds().iterator().next(), message.payload.key().getToken())); 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)); } - private List> keyReadChains(int nowInSeconds, Dispatcher.RequestTime requestTime) + private List> readChains(Dispatcher.RequestTime requestTime) { - TxnKeyRead read = (TxnKeyRead) txn.read(); - List> results = new ArrayList<>(); + TxnRead read = (TxnRead) txn.read(); Seekables keys = txn.read().keys(); + switch (keys.domain()) + { + case Key: + return keyReadChains(read, keys, requestTime); + case Range: + return rangeReadChains(read, keys, requestTime); + default: + throw new IllegalStateException("Unhandled domain " + keys.domain()); + } + } + + private List> keyReadChains(TxnRead read, Seekables keys, Dispatcher.RequestTime requestTime) + { + ClusterMetadata cm = ClusterMetadata.current(); + List> results = new ArrayList<>(); keys.forEach(key -> { - read.forEachWithKey((PartitionKey) key, fragment -> { - SinglePartitionReadCommand command = (SinglePartitionReadCommand) fragment.command(); + read.forEachWithKey((PartitionKey) key, fragment -> { + SinglePartitionReadCommand command = (SinglePartitionReadCommand) fragment.command(); - // 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, 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())) - { - metrics.migrationSkippedReads.mark(); - results.add(AsyncChains.success(TxnData.emptyPartition(fragment.txnDataName(), command))); - return; - } + // 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, because it will be rejected in `TxnQuery` anyways, + // but it's faster to skip the read + AccordClientRequestMetrics metrics = txn.kind().isWrite() ? accordWriteMetrics : accordReadMetrics; + // TODO (required): This doesn't use the metadata from the correct epoch + if (!ConsensusRequestRouter.instance.isKeyManagedByAccordForReadAndWrite(cm, command.metadata().id, command.partitionKey())) + { + metrics.migrationSkippedReads.mark(); + results.add(AsyncChains.success(TxnData.emptyPartition(fragment.txnDataName(), command))); + return; + } + + Group group = Group.one(command); + results.add(AsyncChains.ofCallable(Stage.ACCORD_MIGRATION.executor(), () -> { + TxnData result = new TxnData(); + // Enforcing limits is redundant since we only have a group of size 1, but checking anyways + // documents the requirement here + try (PartitionIterator iterator = StorageProxy.maybeEnforceLimits(StorageProxy.fetchRows(group.queries, consistencyLevel, this, requestTime), group)) + { + if (iterator.hasNext()) + { + try (RowIterator partition = iterator.next()) + { + TxnDataKeyValue value = new TxnDataKeyValue(partition); + if (value.hasRows() || command.selectsFullPartition()) + result.put(fragment.txnDataName(), value); + } + } + } + return result; + })); + }); + + }); + return results; + } + + private List> rangeReadChains(TxnRead read, Seekables keys, Dispatcher.RequestTime requestTime) + { + List> results = new ArrayList<>(); + keys.forEach(key -> { + read.forEachWithKey(key, fragment -> { + PartitionRangeReadCommand command = ((PartitionRangeReadCommand) fragment.command()).withTxnReadName(fragment.txnDataName()); + + // TODO (required): To make migration work we need to validate that the range is all on Accord - Group group = Group.one(command.withNowInSec(nowInSeconds)); results.add(AsyncChains.ofCallable(Stage.ACCORD_MIGRATION.executor(), () -> { TxnData result = new TxnData(); - try (PartitionIterator iterator = StorageProxy.readRegular(group, consistencyLevel, this, requestTime)) + try (PartitionIterator iterator = StorageProxy.getRangeSlice(command, consistencyLevel, this, requestTime)) { - if (iterator.hasNext()) + TxnDataRangeValue value = new TxnDataRangeValue(); + while (iterator.hasNext()) { try (RowIterator partition = iterator.next()) { - TxnDataKeyValue value = new TxnDataKeyValue(partition); - if (value.hasRows() || command.selectsFullPartition()) - result.put(fragment.txnDataName(), value); + FilteredPartition filtered = FilteredPartition.create(partition); + if (filtered.hasRows() || command.selectsFullPartition()) + value.add(filtered); } } + result.put(fragment.txnDataName(), value); } return result; })); }); - }); - return results; - } - private List> rangeReadChains(long nowInSeconds, Dispatcher.RequestTime requestTime) - { - TxnRangeRead read = (TxnRangeRead) txn.read(); - Seekables keys = txn.read().keys(); - List> results = new ArrayList<>(); - keys.forEach(key -> { - TokenRange range = (TokenRange)key; - PartitionRangeReadCommand command = read.commandForSubrange(range, nowInSeconds); - - // 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, because it will be rejected in `TxnQuery` anyways, - // but it's faster to skip the read - // TODO (required): To make migration work we need to validate that the range is all on Accord - // if any part isn't we should reject 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())) -// { -// metrics.migrationSkippedReads.mark(); -// results.add(AsyncChains.success(TxnData.emptyPartition(fragment.txnDataName(), command))); -// return; -// } - - results.add(AsyncChains.ofCallable(Stage.ACCORD_MIGRATION.executor(), () -> { - TxnData result = new TxnData(); - try (PartitionIterator iterator = StorageProxy.getRangeSlice(command, consistencyLevel, this, requestTime)) - { - TxnDataRangeValue value = new TxnDataRangeValue(); - while (iterator.hasNext()) - { - try (RowIterator partition = iterator.next()) - { - FilteredPartition filtered = FilteredPartition.create(partition); - if (filtered.hasRows() || command.selectsFullPartition()) - value.add(filtered); - } - } - result.put(TxnData.txnDataName(TxnDataNameKind.USER), value); - } - return result; - })); }); return results; } private AsyncChain readChains() { - int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(executeAt.hlc()); // TODO (expected): use normal query nano time Dispatcher.RequestTime requestTime = Dispatcher.RequestTime.forImmediateExecution(); - List> results; - if (txn.keys().domain().isKey()) - results = keyReadChains(nowInSeconds, requestTime); - else - results = rangeReadChains(nowInSeconds, requestTime); - + List> results = readChains(requestTime); if (results.isEmpty()) return AsyncChains.success(new TxnData()); @@ -431,9 +427,20 @@ public class AccordInteropExecution implements ReadCoordinator, MaximalCommitSen } @Override - public ReadCommand maybeAllowOutOfRangeReads(ReadCommand readCommand) + public ReadCommand maybeAllowOutOfRangeReads(ReadCommand readCommand, ConsistencyLevel cl) { - return readCommand.allowOutOfRangeReads(); + // Reading from a single coordinator so there is no reconciliation at the coordinator and filtering/limits + // need to be pushed down to query execution + boolean withoutReconciliation = cl == null || cl == ConsistencyLevel.ONE; + // Really just want to enable allowPotentialTxnConflicts without changing anything else + // but didn't want to add another method for constructing a modified read command + if (readCommand instanceof SinglePartitionReadCommand) + return ((SinglePartitionReadCommand)readCommand).withTransactionalSettings(withoutReconciliation, readCommand.nowInSec()); + else + { + PartitionRangeReadCommand rangeCommand = ((PartitionRangeReadCommand)readCommand); + return rangeCommand.withTransactionalSettings(readCommand.nowInSec(), rangeCommand.dataRange().keyRange(), true, withoutReconciliation); + } } // Provide request callbacks with a way to send maximal commits on Insufficient responses diff --git a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropRead.java b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropRead.java index 67d1ecc72b..04e18a6d9d 100644 --- a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropRead.java +++ b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropRead.java @@ -19,22 +19,34 @@ package org.apache.cassandra.service.accord.interop; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + import accord.api.Data; import accord.local.Node; import accord.local.SafeCommandStore; -import accord.messages.ReadData; import accord.messages.MessageType; +import accord.messages.ReadData; import accord.primitives.PartialTxn; import accord.primitives.Participants; +import accord.primitives.Range; import accord.primitives.Ranges; +import accord.primitives.Routables.Slice; import accord.primitives.Timestamp; import accord.primitives.TxnId; import accord.topology.Topologies; import accord.utils.async.AsyncChain; import accord.utils.async.AsyncChains; import org.apache.cassandra.concurrent.Stage; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadCommandVerbHandler; import org.apache.cassandra.db.ReadResponse; import org.apache.cassandra.db.SinglePartitionReadCommand; @@ -46,17 +58,26 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Message; import org.apache.cassandra.net.RequestCallback; import org.apache.cassandra.service.accord.AccordMessageSink.AccordMessageType; +import org.apache.cassandra.service.accord.TokenRange; +import org.apache.cassandra.service.accord.api.AccordRoutingKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.accord.serializers.CommandSerializers; import org.apache.cassandra.service.accord.serializers.KeySerializers; import org.apache.cassandra.service.accord.serializers.ReadDataSerializers; import org.apache.cassandra.service.accord.serializers.ReadDataSerializers.ReadDataSerializer; +import org.apache.cassandra.service.accord.txn.TxnNamedRead; +import org.apache.cassandra.service.accord.txn.TxnRead; +import org.apache.cassandra.utils.Pair; import static accord.primitives.SaveStatus.PreApplied; import static accord.primitives.SaveStatus.ReadyToExecute; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; public class AccordInteropRead extends ReadData { - public static final IVersionedSerializer requestSerializer = new ReadDataSerializer<>() + + public static final IVersionedSerializer requestSerializer = new ReadDataSerializer() { @Override public void serialize(AccordInteropRead read, DataOutputPlus out, int version) throws IOException @@ -64,7 +85,7 @@ public class AccordInteropRead extends ReadData CommandSerializers.txnId.serialize(read.txnId, out, version); KeySerializers.participants.serialize(read.scope, out, version); out.writeUnsignedVInt(read.executeAtEpoch); - SinglePartitionReadCommand.serializer.serialize(read.command, out, version); + ReadCommand.serializer.serialize(read.command, out, version); } @Override @@ -73,7 +94,7 @@ public class AccordInteropRead extends ReadData TxnId txnId = CommandSerializers.txnId.deserialize(in, version); Participants scope = KeySerializers.participants.deserialize(in, version); long executeAtEpoch = in.readUnsignedVInt(); - SinglePartitionReadCommand command = (SinglePartitionReadCommand) SinglePartitionReadCommand.serializer.deserialize(in, version); + ReadCommand command = ReadCommand.serializer.deserialize(in, version); return new AccordInteropRead(txnId, scope, executeAtEpoch, command); } @@ -83,7 +104,7 @@ public class AccordInteropRead extends ReadData return CommandSerializers.txnId.serializedSize(read.txnId, version) + KeySerializers.participants.serializedSize(read.scope, version) + TypeSizes.sizeofUnsignedVInt(read.executeAtEpoch) - + SinglePartitionReadCommand.serializer.serializedSize(read.command, version); + + ReadCommand.serializer.serializedSize(read.command, version); } }; @@ -91,12 +112,15 @@ public class AccordInteropRead extends ReadData private static class LocalReadData implements Data { + private static final Comparator> RESPONSE_COMPARATOR = Comparator.comparing(Pair::left); + static final IVersionedSerializer serializer = new IVersionedSerializer<>() { @Override public void serialize(LocalReadData data, DataOutputPlus out, int version) throws IOException { - ReadResponse.serializer.serialize(data.response, out, version); + data.ensureRemoteResponse(); + ReadResponse.serializer.serialize(data.remoteResponse, out, version); } @Override @@ -108,27 +132,76 @@ public class AccordInteropRead extends ReadData @Override public long serializedSize(LocalReadData data, int version) { - return ReadResponse.serializer.serializedSize(data.response, version); + data.ensureRemoteResponse(); + return ReadResponse.serializer.serializedSize(data.remoteResponse, version); } }; - final ReadResponse response; + // Will be null at coordinator + List> localResponses; + // Will be null at coordinator + final ReadCommand readCommand; + // Will be not null at coordinator, but null at the node creating the response until it serialized + ReadResponse remoteResponse; - public LocalReadData(ReadResponse response) + public LocalReadData(@Nullable AccordRoutingKey start, @Nonnull ReadResponse response, @Nonnull ReadCommand readCommand) { - this.response = response; + checkNotNull(response, "response is null"); + checkNotNull(readCommand, "readCommand is null"); + localResponses = ImmutableList.of(Pair.create(start, response)); + this.readCommand = readCommand; + this.remoteResponse = null; + } + + public LocalReadData(@Nonnull ReadResponse remoteResponse) + { + checkNotNull(remoteResponse); + this.remoteResponse = remoteResponse; + readCommand = null; } @Override public String toString() { - return "LocalReadData{" + response + '}'; + if (localResponses != null) + return "LocalReadData{" + localResponses + '}'; + else + return "LocalReadData{" + remoteResponse + '}'; } @Override public Data merge(Data data) { - throw new IllegalStateException("Should only ever be a single partition"); + checkState(remoteResponse == null, "Already serialized"); + checkState(readCommand.isRangeRequest(), "Should only ever be a single partition"); + LocalReadData other = (LocalReadData)data; + checkState(readCommand == other.readCommand, "Should share the same ReadCommand"); + if (localResponses.size() == 1) + { + List> merged = new ArrayList<>(); + merged.add(localResponses.get(0)); + localResponses = merged; + } + localResponses.addAll(other.localResponses); + return this; + } + + private void ensureRemoteResponse() + { + if (remoteResponse != null) + return; + // Range reads will be spread across command stores and need to be merged in token order + List> responses = localResponses; + if (responses.size() == 1) + { + remoteResponse = responses.get(0).right; + } + else + { + responses = new ArrayList(responses); + Collections.sort(responses, RESPONSE_COMPARATOR); + remoteResponse = ReadResponse.merge(Lists.transform(responses, Pair::right), readCommand); + } } } @@ -142,21 +215,21 @@ public class AccordInteropRead extends ReadData @Override ReadResponse convertResponse(ReadOk ok) { - return ((LocalReadData) ok.data).response; + return ((LocalReadData)ok.data).remoteResponse; } } private static final ExecuteOn EXECUTE_ON = new ExecuteOn(ReadyToExecute, PreApplied); - final SinglePartitionReadCommand command; + protected final ReadCommand command; - public AccordInteropRead(Node.Id to, Topologies topologies, TxnId txnId, Participants scope, long executeAtEpoch, SinglePartitionReadCommand command) + public AccordInteropRead(Node.Id to, Topologies topologies, TxnId txnId, Participants scope, long executeAtEpoch, ReadCommand command) { super(to, topologies, txnId, scope, executeAtEpoch); this.command = command; } - public AccordInteropRead(TxnId txnId, Participants scope, long executeAtEpoch, SinglePartitionReadCommand command) + public AccordInteropRead(TxnId txnId, Participants scope, long executeAtEpoch, ReadCommand command) { super(txnId, scope, executeAtEpoch); this.command = command; @@ -171,8 +244,42 @@ public class AccordInteropRead extends ReadData @Override protected AsyncChain beginRead(SafeCommandStore safeStore, Timestamp executeAt, PartialTxn txn, Ranges unavailable) { - // TODO (required): subtract unavailable ranges, either from read or from response (or on coordinator) - return AsyncChains.ofCallable(Stage.READ.executor(), () -> new LocalReadData(ReadCommandVerbHandler.instance.doRead(command, false))); + TxnRead txnRead = (TxnRead)txn.read(); + Ranges ranges = safeStore.ranges().allAt(executeAt).without(unavailable).intersecting(scope, Slice.Minimal); + long nowInSeconds = TxnNamedRead.nowInSeconds(executeAt); + List> chains = new ArrayList<>(ranges.size()); + for (Range r : ranges) + { + ReadCommand readCommand = this.command; + AccordRoutingKey routingKey = null; + final ReadCommand readCommandFinal; + if (readCommand.isRangeRequest()) + { + // This path can have a subrange we have never seen before provided by short read protection or read repair so we need to + // calculate the intersection with this instance of the command store and the actual command if it is not empty we + // will need to execute it + TokenRange commandRange = TxnNamedRead.boundsAsAccordRange(readCommand.dataRange().keyRange(), readCommand.metadata().id); + Range intersection = commandRange.intersection(r); + if (intersection == null) + continue; + readCommandFinal = TxnNamedRead.commandForSubrange((PartitionRangeReadCommand) readCommand, intersection, txnRead.cassandraConsistencyLevel(), nowInSeconds); + routingKey = ((TokenRange)r).start(); + } + else + { + SinglePartitionReadCommand singlePartitionReadCommand = ((SinglePartitionReadCommand)readCommand); + if (!r.contains(new TokenKey(singlePartitionReadCommand.metadata().id, singlePartitionReadCommand.partitionKey().getToken()))) + continue; + readCommandFinal = ((SinglePartitionReadCommand)readCommand).withTransactionalSettings(TxnNamedRead.readsWithoutReconciliation(txnRead.cassandraConsistencyLevel()), nowInSeconds); + } + AccordRoutingKey routingKeyFinal = routingKey; + chains.add(AsyncChains.ofCallable(Stage.READ.executor(), () -> new LocalReadData(routingKeyFinal, ReadCommandVerbHandler.instance.doRead(readCommandFinal, false), readCommand))); + } + + if (chains.isEmpty()) + return AsyncChains.success(null); + + return AsyncChains.reduce(chains, Data::merge); } @Override diff --git a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropReadCallback.java b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropReadCallback.java index 908d1f3cf7..ea9ffabe11 100644 --- a/src/java/org/apache/cassandra/service/accord/interop/AccordInteropReadCallback.java +++ b/src/java/org/apache/cassandra/service/accord/interop/AccordInteropReadCallback.java @@ -58,6 +58,7 @@ public abstract class AccordInteropReadCallback implements Callback implements Callback implements Callback route = kind.withTxn == HasTxn ? KeySerializers.fullRoute.deserialize(in, version) : null; - SinglePartitionReadCommand command = (SinglePartitionReadCommand) SinglePartitionReadCommand.serializer.deserialize(in, version); + ReadCommand command = ReadCommand.serializer.deserialize(in, version); return new AccordInteropStableThenRead(txnId, scope, kind, minEpoch, executeAt, partialTxn, partialDeps, route, command); } @@ -107,7 +107,7 @@ public class AccordInteropStableThenRead extends AccordInteropRead + (read.kind.withTxn == NoTxn ? 0 : CommandSerializers.nullablePartialTxn.serializedSize(read.partialTxn, version)) + (read.kind.withDeps != HasDeps ? 0 : DepsSerializers.partialDeps.serializedSize(read.partialDeps, version)) + (read.kind.withTxn != HasTxn ? 0 : KeySerializers.fullRoute.serializedSize(read.route, version)) - + SinglePartitionReadCommand.serializer.serializedSize(read.command, version); + + ReadCommand.serializer.serializedSize(read.command, version); } }; @@ -121,18 +121,18 @@ public class AccordInteropStableThenRead extends AccordInteropRead public final @Nullable PartialDeps partialDeps; public final @Nullable FullRoute route; - public AccordInteropStableThenRead(Node.Id to, Topologies topologies, TxnId txnId, Commit.Kind kind, Timestamp executeAt, Txn txn, Deps deps, FullRoute route, SinglePartitionReadCommand command) + public AccordInteropStableThenRead(Node.Id to, Topologies topologies, TxnId txnId, Commit.Kind kind, Timestamp executeAt, Txn txn, Deps deps, FullRoute route, ReadCommand command) { super(to, topologies, txnId, route, executeAt.epoch(), command); this.kind = kind; this.minEpoch = topologies.oldestEpoch(); this.executeAt = executeAt; - this.partialTxn = kind.withTxn.select(txn, scope, topologies, txnId, to); - this.partialDeps = kind.withDeps.select(deps, scope); + this.partialTxn = kind.withTxn.select(txn, route, topologies, txnId, to); + this.partialDeps = kind.withDeps.select(deps, route); this.route = kind.withTxn.select(route); } - public AccordInteropStableThenRead(TxnId txnId, Participants scope, Commit.Kind kind, long minEpoch, Timestamp executeAt, @Nullable PartialTxn partialTxn, @Nullable PartialDeps partialDeps, @Nullable FullRoute route, SinglePartitionReadCommand command) + public AccordInteropStableThenRead(TxnId txnId, Participants scope, Commit.Kind kind, long minEpoch, Timestamp executeAt, @Nullable PartialTxn partialTxn, @Nullable PartialDeps partialDeps, @Nullable FullRoute route, ReadCommand command) { super(txnId, scope, executeAt.epoch(), command); this.minEpoch = minEpoch; diff --git a/src/java/org/apache/cassandra/service/accord/serializers/KeySerializers.java b/src/java/org/apache/cassandra/service/accord/serializers/KeySerializers.java index f6b641f859..6eb7a0fe6b 100644 --- a/src/java/org/apache/cassandra/service/accord/serializers/KeySerializers.java +++ b/src/java/org/apache/cassandra/service/accord/serializers/KeySerializers.java @@ -45,6 +45,7 @@ import accord.primitives.Ranges; import accord.primitives.RoutableKey; import accord.primitives.Route; import accord.primitives.RoutingKeys; +import accord.primitives.Seekable; import accord.primitives.Seekables; import accord.primitives.Unseekables; import accord.primitives.Unseekables.UnseekablesKind; @@ -418,6 +419,51 @@ public class KeySerializers } } + public static final IVersionedSerializer seekable = new IVersionedSerializer<>() + { + @Override + public void serialize(Seekable seekable, DataOutputPlus out, int version) throws IOException + { + switch (seekable.domain()) + { + default: throw new AssertionError(); + case Key: + out.writeByte(0); + PartitionKey.serializer.serialize((PartitionKey) seekable, out, version); + break; + case Range: + out.writeByte(1); + TokenRange.serializer.serialize((TokenRange) seekable, out, version); + break; + } + } + + @Override + public Seekable deserialize(DataInputPlus in, int version) throws IOException + { + byte b = in.readByte(); + switch (b) + { + default: throw new IOException("Corrupted input: expected byte 1 or 2, received " + b); + case 0: return PartitionKey.serializer.deserialize(in, version); + case 1: return TokenRange.serializer.deserialize(in, version); + } + } + + @Override + public long serializedSize(Seekable seekable, int version) + { + switch (seekable.domain()) + { + default: throw new AssertionError(); + case Key: + return 1 + PartitionKey.serializer.serializedSize((PartitionKey) seekable, version); + case Range: + return 1 + TokenRange.serializer.serializedSize((TokenRange) seekable, version); + } + } + }; + public static class AbstractSeekablesSerializer implements IVersionedSerializer> { final IVersionedSerializer keys; diff --git a/src/java/org/apache/cassandra/service/accord/txn/AbstractKeySorted.java b/src/java/org/apache/cassandra/service/accord/txn/AbstractKeySorted.java index 90cd946d98..3cc2f6fc22 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/AbstractKeySorted.java +++ b/src/java/org/apache/cassandra/service/accord/txn/AbstractKeySorted.java @@ -28,7 +28,14 @@ import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Iterators; +import accord.api.Key; import accord.primitives.Keys; +import accord.primitives.Range; +import accord.primitives.Ranges; +import accord.primitives.Routable.Domain; +import accord.primitives.Seekable; +import accord.primitives.Seekables; +import org.apache.cassandra.service.accord.TokenRange; import org.apache.cassandra.service.accord.api.PartitionKey; /** @@ -38,30 +45,55 @@ public abstract class AbstractKeySorted implements Iterable { public static final String ITEMS_OUT_OF_ORDER_MESSAGE = "Items are out of order ([%s] %s >= [%s] %s)"; - protected final Keys itemKeys; + protected final Seekables itemKeys; protected final T[] items; - public AbstractKeySorted(T[] items) + public AbstractKeySorted(T[] items, Domain domain) { this.items = items; - this.itemKeys = extractItemKeys(); + this.itemKeys = extractItemKeys(domain); } - public AbstractKeySorted(List items) + public AbstractKeySorted(List items, Domain domain) { T[] arr = newArray(items.size()); items.toArray(arr); - Arrays.sort(arr, this::compare); this.items = arr; - this.itemKeys = extractItemKeys(); + switch (domain) + { + case Key: + Arrays.sort(arr, this::compareKey); + break; + case Range: + Arrays.sort(arr, this::compareRange); + break; + default: + throw new IllegalStateException("Unhandled domain " + domain); + } + this.itemKeys = extractItemKeys(domain); } - private Keys extractItemKeys() + private Seekables extractItemKeys(Domain domain) { - PartitionKey[] keys = new PartitionKey[items.length]; - for (int i = 0 ; i < keys.length ; ++i) - keys[i] = getKey(items[i]); - return Keys.ofSorted(keys); + switch (domain) + { + case Key: + if (items.length == 0) + return Keys.EMPTY; + PartitionKey[] keys = new PartitionKey[items.length]; + for (int i = 0 ; i < keys.length; i++) + keys[i] = (PartitionKey)getKey(items[i]); + return Keys.ofSorted(keys); + case Range: + if (items.length == 0) + return Ranges.EMPTY; + TokenRange[] ranges = new TokenRange[items.length]; + for (int i = 0 ; i < ranges.length; i++) + ranges[i] = (TokenRange)getKey(items[i]); + return Ranges.ofSortedAndDeoverlapped(ranges); + default: + throw new IllegalStateException("Unhandled domain " + domain); + } } @Override @@ -94,7 +126,7 @@ public abstract class AbstractKeySorted implements Iterable } @VisibleForTesting - public Keys keys() + public Seekables keys() { return itemKeys; } @@ -104,25 +136,51 @@ public abstract class AbstractKeySorted implements Iterable */ abstract int compareNonKeyFields(T left, T right); - abstract PartitionKey getKey(T item); + abstract Seekable getKey(T item); abstract T[] newArray(int size); - public int compare(T left, T right) + abstract Domain domain(); + + public int compareKey(T left, T right) { - int cmp = getKey(left).compareTo(getKey(right)); + int cmp = ((PartitionKey)getKey(left)).compareTo(((PartitionKey)getKey(right))); + return cmp != 0 ? cmp : compareNonKeyFields(left, right); + } + + public int compareRange(T left, T right) + { + int cmp = ((TokenRange)getKey(left)).compareTo(((TokenRange)getKey(right))); return cmp != 0 ? cmp : compareNonKeyFields(left, right); } @VisibleForTesting void validateOrder() { - for (int i = 1; i < items.length; i++) + Domain domain = getKey(items[0]).domain(); + switch (domain) { - T prev = items[i-1]; - T next = items[i]; + case Key: + for (int i = 1; i < items.length; i++) + { + T prev = items[i-1]; + T next = items[i]; - if (compare(prev, next) >= 0) - throw new IllegalStateException(String.format(ITEMS_OUT_OF_ORDER_MESSAGE, i - 1, prev, i, next)); + if (compareKey(prev, next) >= 0) + throw new IllegalStateException(String.format(ITEMS_OUT_OF_ORDER_MESSAGE, i - 1, prev, i, next)); + } + break; + case Range: + for (int i = 1; i < items.length; i++) + { + T prev = items[i-1]; + T next = items[i]; + + if (compareRange(prev, next) >= 0) + throw new IllegalStateException(String.format(ITEMS_OUT_OF_ORDER_MESSAGE, i - 1, prev, i, next)); + } + break; + default: + throw new IllegalStateException("Unhandled domain " + domain); } } @@ -131,16 +189,55 @@ public abstract class AbstractKeySorted implements Iterable return items.length; } - public void forEachWithKey(PartitionKey key, Consumer consumer) + public void forEachWithKey(Seekable key, Consumer consumer) { - for (int i = firstPossibleKeyIdx(key); i < items.length && getKey(items[i]).equals(key); i++) - consumer.accept(items[i]); + switch (key.domain()) + { + case Key: + for (int i = firstPossibleKeyIdx((PartitionKey) key); i < items.length; i++) + { + Key itemKey = (Key)getKey(items[i]); + if (key.equals(itemKey)) + consumer.accept(items[i]); + else + break; + } + break; + case Range: + TokenRange range = (TokenRange) key; + for (int i = firstPossibleRangeIdx(range); i < items.length; i++) + { + Range itemRange = (Range) getKey(items[i]); + if (range.compareIntersecting(itemRange) == 0) + consumer.accept(items[i]); + else + break; + } + break; + default: + throw new IllegalStateException("Unhandled domain " + key.domain()); + } + } + + private int firstPossibleRangeIdx(TokenRange range) + { + int idx = Arrays.binarySearch(items, range, (l, r) -> { + Range itemRange = (Range)getKey((T) l); + if (itemRange.compareIntersecting((TokenRange)r) == 0) + return 1; + if (((TokenRange) r).end().compareTo(itemRange.end()) > 0) + return 1; + else + return -1; + }); + + return -1 - idx; } private int firstPossibleKeyIdx(PartitionKey key) { int idx = Arrays.binarySearch(items, key, (l, r) -> { - PartitionKey lk = getKey((T) l); + PartitionKey lk = (PartitionKey) getKey((T) l); PartitionKey rk = (PartitionKey) r; int cmp = lk.compareTo(rk); return cmp != 0 ? cmp : 1; diff --git a/src/java/org/apache/cassandra/service/accord/txn/RetryWithNewProtocolResult.java b/src/java/org/apache/cassandra/service/accord/txn/RetryWithNewProtocolResult.java index 6156b6a72f..5f80e304e4 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/RetryWithNewProtocolResult.java +++ b/src/java/org/apache/cassandra/service/accord/txn/RetryWithNewProtocolResult.java @@ -18,22 +18,16 @@ package org.apache.cassandra.service.accord.txn; -import org.apache.cassandra.tcm.Epoch; -import org.apache.cassandra.utils.ObjectSizes; - /** * Potentially returned by any transaction that tries to execute in an Epoch * where the range has migrated away from Accord */ public class RetryWithNewProtocolResult implements TxnResult { - private static final long EMPTY_SIZE = ObjectSizes.measure(new RetryWithNewProtocolResult(null)); + public static final RetryWithNewProtocolResult instance = new RetryWithNewProtocolResult(); - public final Epoch epoch; - - RetryWithNewProtocolResult(Epoch epoch) + private RetryWithNewProtocolResult() { - this.epoch = epoch; } @Override @@ -45,6 +39,6 @@ public class RetryWithNewProtocolResult implements TxnResult @Override public long estimatedSizeOnHeap() { - return EMPTY_SIZE + epoch.estimatedSizeOnHeap(); + return 0; } } diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnKeyRead.java b/src/java/org/apache/cassandra/service/accord/txn/TxnKeyRead.java deleted file mode 100644 index eaab7793b8..0000000000 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnKeyRead.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.service.accord.txn; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import com.google.common.collect.ImmutableList; - -import accord.api.Data; -import accord.api.DataStore; -import accord.api.Read; -import accord.local.SafeCommandStore; -import accord.primitives.Keys; -import accord.primitives.Participants; -import accord.primitives.Ranges; -import accord.primitives.Seekable; -import accord.primitives.Timestamp; -import accord.utils.async.AsyncChain; -import accord.utils.async.AsyncChains; -import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.SinglePartitionReadCommand; -import org.apache.cassandra.io.util.DataInputPlus; -import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.service.accord.api.PartitionKey; -import org.apache.cassandra.utils.ObjectSizes; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static org.apache.cassandra.service.accord.AccordSerializers.consistencyLevelSerializer; -import static org.apache.cassandra.service.accord.IAccordService.SUPPORTED_READ_CONSISTENCY_LEVELS; -import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.CAS_READ; -import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.USER; -import static org.apache.cassandra.service.accord.txn.TxnData.txnDataName; -import static org.apache.cassandra.utils.ArraySerializers.deserializeArray; -import static org.apache.cassandra.utils.ArraySerializers.serializeArray; -import static org.apache.cassandra.utils.ArraySerializers.serializedArraySize; -import static org.apache.cassandra.utils.NullableSerializer.deserializeNullable; -import static org.apache.cassandra.utils.NullableSerializer.serializeNullable; -import static org.apache.cassandra.utils.NullableSerializer.serializedNullableSize; - -public class TxnKeyRead extends AbstractKeySorted implements TxnRead -{ - public static final TxnKeyRead EMPTY = new TxnKeyRead(new TxnNamedRead[0], null); - private static final long EMPTY_SIZE = ObjectSizes.measure(EMPTY); - private static final Comparator TXN_NAMED_READ_KEY_COMPARATOR = Comparator.comparing(TxnNamedRead::key); - - // Cassandra's consistency level used by Accord to safely read data written outside of Accord - @Nullable - private final ConsistencyLevel cassandraConsistencyLevel; - - private TxnKeyRead(@Nonnull TxnNamedRead[] items, @Nullable ConsistencyLevel cassandraConsistencyLevel) - { - super(items); - checkNotNull(items, "items is null"); - checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read"); - this.cassandraConsistencyLevel = cassandraConsistencyLevel; - } - - private TxnKeyRead(@Nonnull List items, @Nullable ConsistencyLevel cassandraConsistencyLevel) - { - super(items); - checkNotNull(items, "items is null"); - checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read"); - this.cassandraConsistencyLevel = cassandraConsistencyLevel; - } - - public static TxnKeyRead createTxnRead(@Nonnull List items, @Nullable ConsistencyLevel consistencyLevel) - { - items.sort(Comparator.comparing(TxnNamedRead::key)); - return new TxnKeyRead(items, consistencyLevel); - } - - public static TxnKeyRead createSerialRead(List readCommands, ConsistencyLevel consistencyLevel) - { - List reads = new ArrayList<>(readCommands.size()); - for (int i = 0; i < readCommands.size(); i++) - reads.add(new TxnNamedRead(txnDataName(USER, i), readCommands.get(i))); - reads.sort(TXN_NAMED_READ_KEY_COMPARATOR); - return new TxnKeyRead(reads, consistencyLevel); - } - - public static TxnKeyRead createCasRead(SinglePartitionReadCommand readCommand, ConsistencyLevel consistencyLevel) - { - TxnNamedRead read = new TxnNamedRead(txnDataName(CAS_READ), readCommand); - return new TxnKeyRead(ImmutableList.of(read), consistencyLevel); - } - - // A read that declares it will read from keys but doesn't actually read any data so dependent transactions will - // still be applied first - public static TxnKeyRead createNoOpRead(Keys keys) - { - List reads = new ArrayList<>(keys.size()); - for (int i = 0; i < keys.size(); i++) - reads.add(new TxnNamedRead(txnDataName(USER, i), (PartitionKey)keys.get(i), null)); - return new TxnKeyRead(reads, null); - } - - @Override - public long estimatedSizeOnHeap() - { - long size = EMPTY_SIZE; - for (TxnNamedRead read : items) - size += read.estimatedSizeOnHeap(); - return size; - } - - @Override - int compareNonKeyFields(TxnNamedRead left, TxnNamedRead right) - { - return Integer.compare(left.txnDataName(), right.txnDataName()); - } - - @Override - PartitionKey getKey(TxnNamedRead read) - { - return read.key(); - } - - @Override - TxnNamedRead[] newArray(int size) - { - return new TxnNamedRead[size]; - } - - @Override - public Keys keys() - { - return itemKeys; - } - - @Override - public ConsistencyLevel cassandraConsistencyLevel() - { - return cassandraConsistencyLevel; - } - - @Override - public Read slice(Ranges ranges) - { - return intersecting(itemKeys.slice(ranges)); - } - - @Override - public Read intersecting(Participants participants) - { - return intersecting(itemKeys.intersecting(participants)); - } - - private Read intersecting(Keys select) - { - Keys keys = itemKeys.intersecting(select); - List reads = new ArrayList<>(keys.size()); - - for (TxnNamedRead read : items) - if (keys.contains(read.key())) - reads.add(read); - - return createTxnRead(reads, cassandraConsistencyLevel); - } - - @Override - public Read merge(Read read) - { - List reads = new ArrayList<>(items.length); - Collections.addAll(reads, items); - - for (TxnNamedRead namedRead : (TxnKeyRead) read) - if (!reads.contains(namedRead)) - reads.add(namedRead); - - return createTxnRead(reads, cassandraConsistencyLevel); - } - - @Override - public void unmemoize() - { - for (TxnNamedRead read : items) - read.unmemoize(); - } - - @Override - public AsyncChain read(Seekable key, SafeCommandStore safeStore, Timestamp executeAt, DataStore store) - { - // Set to null since we don't need it and interop can pass in null - safeStore = null; - - List> results = new ArrayList<>(); - forEachWithKey((PartitionKey) key, read -> results.add(read.read(cassandraConsistencyLevel, executeAt))); - - if (results.isEmpty()) - // Result type must match everywhere - return AsyncChains.success(new TxnData()); - - if (results.size() == 1) - return results.get(0); - - return AsyncChains.reduce(results, Data::merge); - } - - @Override - public Kind kind() - { - return Kind.key; - } - - public static final TxnReadSerializer serializer = new TxnReadSerializer() - { - @Override - public void serialize(TxnKeyRead read, DataOutputPlus out, int version) throws IOException - { - serializeArray(read.items, out, version, TxnNamedRead.serializer); - serializeNullable(read.cassandraConsistencyLevel, out, version, consistencyLevelSerializer); - } - - @Override - public TxnKeyRead deserialize(DataInputPlus in, int version) throws IOException - { - TxnNamedRead[] items = deserializeArray(in, version, TxnNamedRead.serializer, TxnNamedRead[]::new); - ConsistencyLevel consistencyLevel = deserializeNullable(in, version, consistencyLevelSerializer); - return new TxnKeyRead(items, consistencyLevel); - } - - @Override - public long serializedSize(TxnKeyRead read, int version) - { - long size = 0; - size += serializedArraySize(read.items, version, TxnNamedRead.serializer); - size += serializedNullableSize(read.cassandraConsistencyLevel, version, consistencyLevelSerializer); - return size; - } - }; -} diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnNamedRead.java b/src/java/org/apache/cassandra/service/accord/txn/TxnNamedRead.java index 525be9f775..5598eaeaed 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnNamedRead.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnNamedRead.java @@ -29,6 +29,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import accord.api.Data; +import accord.primitives.Range; +import accord.primitives.Seekable; import accord.primitives.Timestamp; import accord.utils.async.AsyncChain; import accord.utils.async.AsyncChains; @@ -36,20 +38,38 @@ import accord.utils.async.AsyncResults; import org.apache.cassandra.concurrent.DebuggableTask; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.PartitionRangeReadCommand; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadExecutionController; import org.apache.cassandra.db.SinglePartitionReadCommand; import org.apache.cassandra.db.TypeSizes; +import org.apache.cassandra.db.partitions.FilteredPartition; import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; +import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.dht.Token.KeyBound; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.accord.AccordObjectSizes; +import org.apache.cassandra.service.accord.TokenRange; +import org.apache.cassandra.service.accord.api.AccordRoutingKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.MinTokenKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.SentinelKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.accord.api.PartitionKey; +import org.apache.cassandra.service.accord.serializers.KeySerializers; +import org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.MonotonicClock; import org.apache.cassandra.utils.ObjectSizes; +import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.utils.ByteBufferUtil.readWithVIntLength; import static org.apache.cassandra.utils.ByteBufferUtil.serializedSizeWithVIntLength; import static org.apache.cassandra.utils.ByteBufferUtil.writeWithVIntLength; @@ -59,10 +79,10 @@ public class TxnNamedRead extends AbstractSerialized @SuppressWarnings("unused") private static final Logger logger = LoggerFactory.getLogger(TxnNamedRead.class); - private static final long EMPTY_SIZE = ObjectSizes.measure(new TxnNamedRead(0, null, null)); + private static final long EMPTY_SIZE = ObjectSizes.measure(new TxnNamedRead(0, null, (ByteBuffer) null)); private final int name; - private final PartitionKey key; + private final Seekable key; public TxnNamedRead(int name, @Nullable SinglePartitionReadCommand value) { @@ -71,7 +91,51 @@ public class TxnNamedRead extends AbstractSerialized this.key = new PartitionKey(value.metadata().id, value.partitionKey()); } - public TxnNamedRead(int name, PartitionKey key, ByteBuffer bytes) + public static TokenRange boundsAsAccordRange(AbstractBounds range, TableId tableId) + { + // Should already have been unwrapped + checkState(!AbstractBounds.strictlyWrapsAround(range.left, range.right)); + + // Read commands can contain a mix of different kinds of bounds to facilitate paging + // and we need to communicate that to Accord as its own ranges. This uses + // TokenKey, SentinelKey, and MinTokenKey and sticks exclusively with left exclusive/right inclusive + // ranges rather add more types of ranges to the mix + // MinTokenKey allows emulating inclusive left and exclusive right with Range + boolean inclusiveLeft = range.inclusiveLeft(); + PartitionPosition startPP = range.left; + boolean startIsMinKeyBound = startPP.getClass() == KeyBound.class ? ((KeyBound)startPP).isMinimumBound : false; + Token startToken = startPP.getToken(); + AccordRoutingKey startAccordRoutingKey; + if (startToken.isMinimum() && inclusiveLeft) + startAccordRoutingKey = SentinelKey.min(tableId); + else if (inclusiveLeft || startIsMinKeyBound) + startAccordRoutingKey = new MinTokenKey(tableId, startToken); + else + startAccordRoutingKey = new TokenKey(tableId, startToken); + + boolean inclusiveRight = range.inclusiveRight(); + PartitionPosition endPP = range.right; + boolean endIsMinKeyBound = endPP.getClass() == KeyBound.class ? ((KeyBound)endPP).isMinimumBound : false; + Token stopToken = range.right.getToken(); + AccordRoutingKey stopAccordRoutingKey; + if (stopToken.isMinimum()) + stopAccordRoutingKey = SentinelKey.max(tableId); + else if (inclusiveRight && !endIsMinKeyBound) + stopAccordRoutingKey = new TokenKey(tableId, stopToken); + else + stopAccordRoutingKey = new MinTokenKey(tableId, stopToken); + return TokenRange.create(startAccordRoutingKey, stopAccordRoutingKey); + } + + public TxnNamedRead(int name, AbstractBounds range, PartitionRangeReadCommand value) + { + super(value); + TableId tableId = value.metadata().id; + this.name = name; + this.key = boundsAsAccordRange(range, tableId); + } + + public TxnNamedRead(int name, Seekable key, ByteBuffer bytes) { super(bytes); this.name = name; @@ -80,13 +144,16 @@ public class TxnNamedRead extends AbstractSerialized public long estimatedSizeOnHeap() { - return EMPTY_SIZE + key.estimatedSizeOnHeap() + (bytes() != null ? ByteBufferUtil.estimatedSizeOnHeap(bytes()) : 0); + long size = EMPTY_SIZE; + size += AccordObjectSizes.seekable(key); + size += (bytes() != null ? ByteBufferUtil.estimatedSizeOnHeap(bytes()) : 0); + return size; } @Override protected IVersionedSerializer serializer() { - return SinglePartitionReadCommand.serializer; + return ReadCommand.serializer; } @Override @@ -108,7 +175,7 @@ public class TxnNamedRead extends AbstractSerialized @Override public String toString() { - return "TxnNamedRead{name='" + name + '\'' + ", key=" + key + ", update=" + get() + '}'; + return "TxnNamedRead{name='" + name + '\'' + ", keys=" + key + ", update=" + get() + '}'; } public int txnDataName() @@ -116,39 +183,58 @@ public class TxnNamedRead extends AbstractSerialized return name; } - public PartitionKey key() + public Seekable key() { return key; } - public AsyncChain read(ConsistencyLevel consistencyLevel, Timestamp executeAt) + public static long nowInSeconds(Timestamp executeAt) { - SinglePartitionReadCommand command = (SinglePartitionReadCommand) get(); + return TimeUnit.MICROSECONDS.toSeconds(executeAt.hlc()); + } + + public AsyncChain read(ConsistencyLevel consistencyLevel, Seekable key, Timestamp executeAt) + { + ReadCommand command = get(); if (command == null) return AsyncResults.success(TxnData.NOOP_DATA); + // TODO (required, safety): before release, double check reasoning that this is safe // AccordCommandsForKey cfk = ((SafeAccordCommandStore)safeStore).commandsForKey(key); // int nowInSeconds = cfk.nowInSecondsFor(executeAt, isForWriteTxn); // It's fine for our nowInSeconds to lag slightly our insertion timestamp, as to the user // this simply looks like the transaction witnessed TTL'd data and the data then expired // immediately after the transaction executed, and this simplifies things a great deal - int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(executeAt.hlc()); - if (consistencyLevel == null || consistencyLevel == ConsistencyLevel.ONE) - command = command.withoutReconciliation(); - return performLocalRead(command, nowInSeconds); + long nowInSeconds = nowInSeconds(executeAt); + + boolean withoutReconciliation = readsWithoutReconciliation(consistencyLevel); + switch (key.domain()) + { + case Key: + return performLocalKeyRead(((SinglePartitionReadCommand) command).withTransactionalSettings(withoutReconciliation, nowInSeconds)); + case Range: + return performLocalRangeRead(((PartitionRangeReadCommand) command), key.asRange(), consistencyLevel, nowInSeconds); + default: + throw new IllegalStateException("Unhandled domain " + key.domain()); + } } + public static boolean readsWithoutReconciliation(ConsistencyLevel consistencyLevel) + { + boolean withoutReconciliation = consistencyLevel == null || consistencyLevel == ConsistencyLevel.ONE; + return withoutReconciliation; + } + + public ReadCommand command() { return get(); } - private AsyncChain performLocalRead(SinglePartitionReadCommand command, int nowInSeconds) + private AsyncChain performLocalKeyRead(SinglePartitionReadCommand read) { Callable readCallable = () -> { - SinglePartitionReadCommand read = command.withNowInSec(nowInSeconds); - try (ReadExecutionController controller = read.executionController(); PartitionIterator iterator = UnfilteredPartitionIterators.filter(read.executeLocally(controller), read.nowInSec())) { @@ -201,19 +287,132 @@ public class TxnNamedRead extends AbstractSerialized @Override public String description() { - return command.toCQLString(); + return read.toCQLString(); } } ); } + public static PartitionRangeReadCommand commandForSubrange(PartitionRangeReadCommand command, Range r, ConsistencyLevel consistencyLevel, long nowInSeconds) + { + AbstractBounds bounds = command.dataRange().keyRange(); + PartitionPosition startPP = bounds.left; + PartitionPosition endPP = bounds.right; + TokenKey startTokenKey = new TokenKey(command.metadata().id, startPP.getToken()); + AccordRoutingKey startRoutingKey = ((AccordRoutingKey)r.start()); + AccordRoutingKey endRoutingKey = ((AccordRoutingKey)r.end()); + Token subRangeStartToken = startRoutingKey.getClass() == SentinelKey.class ? startPP.getToken() : ((AccordRoutingKey)r.start()).asTokenKey().token(); + Token subRangeEndToken = endRoutingKey.getClass() == SentinelKey.class ? endPP.getToken() : ((AccordRoutingKey)r.end()).asTokenKey().token(); + + /* + * The way ranges/bounds work for range queries is that the beginning and ending bounds from the command + * could be tokens (and min/max key bounds) or actual keys depending on the bounds of the top level query we + * are running and where we are in paging. We need to preserve whatever is in the command in case it is a + * key and not a token, or it's a token but might be a min/max key bound. + * + * Then Accord will further subdivide the range in the command so need to inject additional bounds in the middle + * that match the range ownership of Accord. + * + * The command still contains the original bound and then the Accord range passed in determines what subset of + * that bound we want. We have to make sure to use the bounds from the command if it is the start or end instead + * of a key bound created from the Accord range since it could be a real key or min/max bound. + * + * When we are dealing with a bound created by Accord's further subdivision we use a maxKeyBound (exclusive) + * for both beginning and end because Bounds is left and right inclusive while Range is only left inclusive. + * We only use TokenRange with Accord which matches the left/right inclusivity of Cassandra's Range. + * + * That means the Range we get from Accord overlaps the previous Range on the left which when converted to a Bound + * would potentially read the same Token twice. So the left needs to be a maxKeyBound to exclude the data that isn't + * owned here and to avoid potentially reading the same data twice. The right bound also needs to be a maxKeyBound since Range + * is right inclusive so every partition we find needs to be < the right bound. + */ + boolean isFirstSubrange = startPP.getToken().equals(subRangeStartToken); + PartitionPosition subRangeStartPP = isFirstSubrange ? startPP : subRangeStartToken.maxKeyBound(); + PartitionPosition subRangeEndPP = endPP.getToken().equals(subRangeEndToken) ? endPP : subRangeEndToken.maxKeyBound(); + // Need to preserve the fact it is a bounds for paging to work, a range is not left inclusive and will not start from where we left off + AbstractBounds subRange = isFirstSubrange ? bounds.withNewRight(subRangeEndPP) : new org.apache.cassandra.dht.Range(subRangeStartPP, subRangeEndPP); + return command.withTransactionalSettings(nowInSeconds, subRange, startTokenKey.equals(r.start()), readsWithoutReconciliation(consistencyLevel)); + } + + private AsyncChain performLocalRangeRead(PartitionRangeReadCommand command, Range r, ConsistencyLevel consistencyLevel, long nowInSeconds) + { + PartitionRangeReadCommand read = commandForSubrange(command, r, consistencyLevel, nowInSeconds); + Callable readCallable = () -> + { + try (ReadExecutionController controller = read.executionController(); + UnfilteredPartitionIterator partition = read.executeLocally(controller); + PartitionIterator iterator = UnfilteredPartitionIterators.filter(partition, read.nowInSec())) + { + TxnData result = new TxnData(); + TxnDataRangeValue value = new TxnDataRangeValue(); + while(iterator.hasNext()) + { + try (RowIterator rows = iterator.next()) + { + FilteredPartition filtered = FilteredPartition.create(rows); + if (filtered.hasRows() || read.selectsFullPartition()) + { + value.add(filtered); + } + } + } + result.put(TxnData.txnDataName(TxnDataNameKind.USER), value); + return result; + } + }; + + return AsyncChains.ofCallable(Stage.READ.executor(), readCallable, (callable, receiver) -> + new DebuggableTask.RunnableDebuggableTask() + { + private final long approxCreationTimeNanos = MonotonicClock.Global.approxTime.now(); + private volatile long approxStartTimeNanos; + + @Override + public void run() + { + approxStartTimeNanos = MonotonicClock.Global.approxTime.now(); + + try + { + Data call = callable.call(); + receiver.accept(call, null); + } + catch (Throwable t) + { + logger.debug("AsyncChain Callable threw an Exception", t); + receiver.accept(null, t); + } + } + + @Override + public long creationTimeNanos() + { + return approxCreationTimeNanos; + } + + @Override + public long startTimeNanos() + { + return approxStartTimeNanos; + } + + @Override + public String description() + { + return command.toCQLString(); + } + } + ); + } + static final IVersionedSerializer serializer = new IVersionedSerializer<>() { @Override public void serialize(TxnNamedRead read, DataOutputPlus out, int version) throws IOException { out.writeInt(read.name); - PartitionKey.serializer.serialize(read.key, out, version); + KeySerializers.seekable.serialize(read.key, out, version); + if (read.bytes() != null) { out.write(0); @@ -229,7 +428,7 @@ public class TxnNamedRead extends AbstractSerialized public TxnNamedRead deserialize(DataInputPlus in, int version) throws IOException { int name = in.readInt(); - PartitionKey key = PartitionKey.serializer.deserialize(in, version); + Seekable key = KeySerializers.seekable.deserialize(in, version); ByteBuffer bytes = in.readByte() == 1 ? null : readWithVIntLength(in); return new TxnNamedRead(name, key, bytes); } @@ -239,8 +438,8 @@ public class TxnNamedRead extends AbstractSerialized { long size = 0; size += TypeSizes.sizeof(read.name); - size += PartitionKey.serializer.serializedSize(read.key, version); - size += TypeSizes.BOOL_SIZE; // is null + size += KeySerializers.seekable.serializedSize(read.key, version); + size += TypeSizes.BYTE_SIZE; // is null if (read.bytes() != null) size += serializedSizeWithVIntLength(read.bytes()); return size; diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnQuery.java b/src/java/org/apache/cassandra/service/accord/txn/TxnQuery.java index 2b2743d826..638781f85f 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnQuery.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnQuery.java @@ -29,6 +29,7 @@ import accord.api.Query; import accord.api.Read; import accord.api.Result; import accord.api.Update; +import accord.primitives.Ranges; import accord.primitives.Seekables; import accord.primitives.Timestamp; import accord.primitives.TxnId; @@ -41,22 +42,27 @@ import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.metrics.ClientRequestsMetricsHolder; +import org.apache.cassandra.service.accord.TokenRange; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind; import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; -import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.ObjectSizes; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.CAS_READ; import static org.apache.cassandra.service.accord.txn.TxnData.txnDataName; 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. + * + * Also used by SERIAL key reads, and non-SERIAL key reads when they are executed on Accord. */ public static final TxnQuery ALL = new TxnQuery() { @@ -118,7 +124,7 @@ public abstract class TxnQuery implements Query return new TxnData(); else if (txnData.isEmpty()) { - TxnKeyRead txnKeyRead = (TxnKeyRead)read; + TxnRead txnKeyRead = (TxnRead)read; SinglePartitionReadCommand command = (SinglePartitionReadCommand) txnKeyRead.iterator().next().get(); // For CAS must return a non-empty result to indicate error even if there was no partition found return TxnData.of(txnDataName(CAS_READ), new TxnDataKeyValue(EmptyIterators.row(command.metadata(), command.partitionKey(), command.isReversed()))); @@ -132,7 +138,7 @@ public abstract class TxnQuery implements Query /** * 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 things like blind writes. Empty is used by Accord for things like sync points which may need to execute * for ranges Accord used to manage, but no longer does. */ public static final TxnQuery UNSAFE_EMPTY = new TxnQuery() @@ -175,8 +181,8 @@ public abstract class TxnQuery implements Query private Result concat(TxnData data, Read read) { - TxnRangeRead txnRead = (TxnRangeRead) read; - PartitionRangeReadCommand command = (PartitionRangeReadCommand) txnRead.get(); + TxnRead txnRead = (TxnRead) read; + PartitionRangeReadCommand command = (PartitionRangeReadCommand) txnRead.iterator().next().get(); TxnDataRangeValue value = (TxnDataRangeValue) data.get(txnDataName(TxnDataNameKind.USER)); Supplier source = value.toPartitionIterator(command.isReversed()); // Because the query was split across multiple command stores the pushed down limit won't be sufficient @@ -200,14 +206,17 @@ public abstract class TxnQuery implements Query @Override public Result compute(TxnId txnId, Timestamp executeAt, Seekables keys, @Nullable Data data, @Nullable Read read, @Nullable Update update) { - Epoch epoch = Epoch.create(executeAt.epoch()); - if (transactionIsInMigratingOrMigratedRange(epoch, keys)) + // TODO (required): This is not the cluster metadata of the current transaction + ClusterMetadata clusterMetadata = ClusterMetadata.current(); + checkState(clusterMetadata.epoch.getEpoch() >= executeAt.epoch(), "TCM epoch %d is < executeAt epoch %d", clusterMetadata.epoch.getEpoch(), executeAt.epoch()); + boolean reads = read != null && !read.keys().isEmpty(); + if (transactionShouldBeBlocked(clusterMetadata, reads, keys)) { if (txnId.isWrite()) ClientRequestsMetricsHolder.accordWriteMetrics.accordMigrationRejects.mark(); else ClientRequestsMetricsHolder.accordReadMetrics.accordMigrationRejects.mark(); - return new RetryWithNewProtocolResult(epoch); + return RetryWithNewProtocolResult.instance; } return doCompute(txnId, executeAt, keys, data, read, update); } @@ -249,19 +258,56 @@ public abstract class TxnQuery implements Query } }; - private static boolean transactionIsInMigratingOrMigratedRange(Epoch epoch, Seekables keys) + private static boolean transactionShouldBeBlocked(ClusterMetadata clusterMetadata, boolean reads, Seekables keys) { - // TODO (required): This is going to be problematic when we presumably support range reads and don't validate them - // Whatever this transaction might be it isn't one supported for migration anyways - if (!keys.domain().isKey()) - return false; + // TxnQuery needs to be smart enough to allow blind writes through for the non-transactional use cases during migration + // This also allows blind write TransactionStatement to run before TransactionStatemetns with reads can run, + // but this is harmless since we only promise that TransactionStatement works when migrated to Accord. + // TODO (lowpri): This could look at read keys vs write keys to see if it can run in more cases + if (reads) + return !transactionIsSafeToReadAndWrite(clusterMetadata, keys); + else + return !transactionIsSafeToWrite(clusterMetadata, keys); + } + + private static boolean transactionIsSafeToReadAndWrite(ClusterMetadata clusterMetadata, Seekables keys) + { + switch (keys.domain()) + { + case Key: + for (PartitionKey partitionKey : (Seekables)keys) + { + // TODO (required): This is looking at ClusterMetadata, but not the ClusterMetadata for the specified epoch, just that epoch or later. Need to store ConsensusMigrationState in the global Topologies Accord stores for itself. + if (!ConsensusRequestRouter.instance.isKeyManagedByAccordForReadAndWrite(clusterMetadata, partitionKey.table(), partitionKey.partitionKey())) + return false; + } + break; + case Range: + for (accord.primitives.Range range : (Ranges)keys) + { + TokenRange tokenRange = (TokenRange)range; + // TODO (required): This is looking at ClusterMetadata, but not the ClusterMetadata for the specified epoch, just that epoch or later. Need to store ConsensusMigrationState in the global Topologies Accord stores for itself. + if (!ConsensusRequestRouter.instance.isRangeManagedByAccordForReadAndWrite(clusterMetadata, tokenRange.table(), tokenRange)) + return false; + } + break; + default: + throw new IllegalStateException("Unsupported domain " + keys.domain()); + } + + return true; + } + + private static boolean transactionIsSafeToWrite(ClusterMetadata clusterMetadata, Seekables keys) + { + checkState(keys.domain().isKey(), "Only key transactions are supported for writes"); for (PartitionKey partitionKey : (Seekables)keys) { // TODO (required): This is looking at ClusterMetadata, but not the ClusterMetadata for the specified epoch, just that epoch or later. Need to store ConsensusMigrationState in the global Topologies Accord stores for itself. - if (ConsensusRequestRouter.instance.isKeyInMigratingOrMigratedRangeFromAccord(epoch, partitionKey.table(), partitionKey.partitionKey())) - return true; + if (!ConsensusRequestRouter.instance.isKeyManagedByAccordForWrite(clusterMetadata, partitionKey.table(), partitionKey.partitionKey())) + return false; } - return false; + return true; } } diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnRangeRead.java b/src/java/org/apache/cassandra/service/accord/txn/TxnRangeRead.java deleted file mode 100644 index 1e592faed5..0000000000 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnRangeRead.java +++ /dev/null @@ -1,387 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.cassandra.service.accord.txn; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import accord.api.Data; -import accord.api.DataStore; -import accord.api.Read; -import accord.local.SafeCommandStore; -import accord.primitives.AbstractRanges.UnionMode; -import accord.primitives.Participants; -import accord.primitives.Range; -import accord.primitives.Ranges; -import accord.primitives.Routable.Domain; -import accord.primitives.Routables.Slice; -import accord.primitives.Seekable; -import accord.primitives.Seekables; -import accord.primitives.Timestamp; -import accord.utils.async.AsyncChain; -import accord.utils.async.AsyncChains; -import org.apache.cassandra.concurrent.DebuggableTask; -import org.apache.cassandra.concurrent.Stage; -import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.PartitionPosition; -import org.apache.cassandra.db.PartitionRangeReadCommand; -import org.apache.cassandra.db.ReadCommand; -import org.apache.cassandra.db.ReadExecutionController; -import org.apache.cassandra.db.partitions.FilteredPartition; -import org.apache.cassandra.db.partitions.PartitionIterator; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; -import org.apache.cassandra.db.rows.RowIterator; -import org.apache.cassandra.dht.AbstractBounds; -import org.apache.cassandra.dht.Token; -import org.apache.cassandra.dht.Token.KeyBound; -import org.apache.cassandra.io.IVersionedSerializer; -import org.apache.cassandra.io.util.DataInputPlus; -import org.apache.cassandra.io.util.DataOutputPlus; -import org.apache.cassandra.schema.TableId; -import org.apache.cassandra.service.accord.AccordObjectSizes; -import org.apache.cassandra.service.accord.TokenRange; -import org.apache.cassandra.service.accord.api.AccordRoutingKey; -import org.apache.cassandra.service.accord.api.AccordRoutingKey.MinTokenKey; -import org.apache.cassandra.service.accord.api.AccordRoutingKey.SentinelKey; -import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; -import org.apache.cassandra.service.accord.serializers.KeySerializers; -import org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind; -import org.apache.cassandra.utils.ByteBufferUtil; -import org.apache.cassandra.utils.MonotonicClock; -import org.apache.cassandra.utils.ObjectSizes; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static org.apache.cassandra.service.accord.AccordSerializers.consistencyLevelSerializer; -import static org.apache.cassandra.service.accord.IAccordService.SUPPORTED_READ_CONSISTENCY_LEVELS; -import static org.apache.cassandra.utils.ByteBufferUtil.readWithVIntLength; -import static org.apache.cassandra.utils.ByteBufferUtil.serializedSizeWithVIntLength; -import static org.apache.cassandra.utils.ByteBufferUtil.writeWithVIntLength; -import static org.apache.cassandra.utils.NullableSerializer.deserializeNullable; -import static org.apache.cassandra.utils.NullableSerializer.serializeNullable; -import static org.apache.cassandra.utils.NullableSerializer.serializedNullableSize; - -public class TxnRangeRead extends AbstractSerialized implements TxnRead -{ - private static final Logger logger = LoggerFactory.getLogger(TxnRangeRead.class); - - public static final TxnRangeRead EMPTY = new TxnRangeRead(null, null, (Ranges)null); - private static final long EMPTY_SIZE = ObjectSizes.measure(EMPTY); - - @Nonnull - private final ConsistencyLevel cassandraConsistencyLevel; - @Nonnull - private final Ranges covering; - - public TxnRangeRead(@Nonnull PartitionRangeReadCommand command, @Nonnull List> ranges, @Nonnull ConsistencyLevel cassandraConsistencyLevel) - { - super(command); - checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read"); - this.cassandraConsistencyLevel = cassandraConsistencyLevel; - TableId tableId = command.metadata().id; - - TokenRange[] accordRanges = new TokenRange[ranges.size()]; - for (int i = 0; i < ranges.size(); i++) - { - AbstractBounds range = ranges.get(i); - // Should already have been unwrapped - checkState(!AbstractBounds.strictlyWrapsAround(range.left, range.right)); - - // Read commands can contain a mix of different kinds of bounds to facilitate paging - // and we need to communicate that to Accord as its own ranges. This uses - // TokenKey, SentinelKey, and MinTokenKey and sticks exclusively with left exclusive/right inclusive - // ranges rather add more types of ranges to the mix - // MinTokenKey allows emulating inclusive left and exclusive right with Range - boolean inclusiveLeft = range.inclusiveLeft(); - PartitionPosition startPP = range.left; - boolean startIsMinKeyBound = startPP.getClass() == KeyBound.class ? ((KeyBound)startPP).isMinimumBound : false; - Token startToken = startPP.getToken(); - AccordRoutingKey startAccordRoutingKey; - if (startToken.isMinimum() && inclusiveLeft) - startAccordRoutingKey = SentinelKey.min(tableId); - else if (inclusiveLeft || startIsMinKeyBound) - startAccordRoutingKey = new MinTokenKey(tableId, startToken); - else - startAccordRoutingKey = new TokenKey(tableId, startToken); - - boolean inclusiveRight = range.inclusiveRight(); - PartitionPosition endPP = range.right; - boolean endIsMinKeyBound = endPP.getClass() == KeyBound.class ? ((KeyBound)endPP).isMinimumBound : false; - Token stopToken = range.right.getToken(); - AccordRoutingKey stopAccordRoutingKey; - if (stopToken.isMinimum()) - stopAccordRoutingKey = SentinelKey.max(tableId); - else if (inclusiveRight && !endIsMinKeyBound) - stopAccordRoutingKey = new TokenKey(tableId, stopToken); - else - stopAccordRoutingKey = new MinTokenKey(tableId, stopToken); - accordRanges[i] = TokenRange.create(startAccordRoutingKey, stopAccordRoutingKey); - } - covering = Ranges.of(accordRanges); - } - - private TxnRangeRead(@Nonnull ByteBuffer commandBytes, @Nonnull ConsistencyLevel cassandraConsistencyLevel, @Nonnull Ranges covering) - { - super(commandBytes); - checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read"); - this.cassandraConsistencyLevel = cassandraConsistencyLevel; - this.covering = covering; - } - - @Override - public Seekables keys() - { - return covering; - } - - @Override - public AsyncChain read(Seekable range, SafeCommandStore commandStore, Timestamp executeAt, DataStore store) - { - // Set to null since we don't need it and interop can pass in null - commandStore = null; - - // It's fine for our nowInSeconds to lag slightly our insertion timestamp, as to the user - // this simply looks like the transaction witnessed TTL'd data and the data then expired - // immediately after the transaction executed, and this simplifies things a great deal - long nowInSeconds = TimeUnit.MICROSECONDS.toSeconds(executeAt.hlc()); - - List> results = new ArrayList<>(); - PartitionRangeReadCommand command = (PartitionRangeReadCommand)get(); - if (cassandraConsistencyLevel == null || cassandraConsistencyLevel == ConsistencyLevel.ONE) - command = command.withoutReconciliation(); - - Ranges intersecting = covering.slice(Ranges.of(range.asRange()), Slice.Minimal); - for (Range subRange : intersecting) - results.add(performLocalRead(command, subRange, nowInSeconds)); - if (results.isEmpty()) - // Result type must match everywhere - return AsyncChains.success(new TxnData()); - - return AsyncChains.reduce(results, Data::merge); - } - - public PartitionRangeReadCommand commandForSubrange(Range r, long nowInSeconds) - { - return commandForSubrange((PartitionRangeReadCommand) get(), r, nowInSeconds); - } - - public PartitionRangeReadCommand commandForSubrange(PartitionRangeReadCommand command, Range r, long nowInSeconds) - { - AbstractBounds bounds = command.dataRange().keyRange(); - PartitionPosition startPP = bounds.left; - PartitionPosition endPP = bounds.right; - TokenKey startTokenKey = new TokenKey(command.metadata().id, startPP.getToken()); - AccordRoutingKey startRoutingKey = ((AccordRoutingKey)r.start()); - AccordRoutingKey endRoutingKey = ((AccordRoutingKey)r.end()); - Token subRangeStartToken = startRoutingKey.getClass() == SentinelKey.class ? startPP.getToken() : ((AccordRoutingKey)r.start()).asTokenKey().token(); - Token subRangeEndToken = endRoutingKey.getClass() == SentinelKey.class ? endPP.getToken() : ((AccordRoutingKey)r.end()).asTokenKey().token(); - - /* - * The way ranges/bounds work for range queries is that the beginning and ending bounds from the command - * could be tokens (and min/max key bounds) or actual keys depending on the bounds of the top level query we - * are running and where we are in paging. We need to preserve whatever is in the command in case it is a - * key and not a token, or it's a token but might be a min/max key bound. - * - * Then Accord will further subdivide the range in the command so need to inject additional bounds in the middle - * that match the range ownership of Accord. - * - * The command still contains the original bound and then the Accord range passed in determines what subset of - * that bound we want. We have to make sure to use the bounds from the command if it is the start or end instead - * of a key bound created from the Accord range since it could be a real key or min/max bound. - * - * When we are dealing with a bound created by Accord's further subdivision we use a maxKeyBound (exclusive) - * for both beginning and end because Bounds is left and right inclusive while Range is only left inclusive. - * We only use TokenRange with Accord which matches the left/right inclusivity of Cassandra's Range. - * - * That means the Range we get from Accord overlaps the previous Range on the left which when converted to a Bound - * would potentially read the same Token twice. So the left needs to be a maxKeyBound to exclude the data that isn't - * owned here and to avoid potentially reading the same data twice. The right bound also needs to be a maxKeyBound since Range - * is right inclusive so every partition we find needs to be < the right bound. - */ - boolean isFirstSubrange = startPP.getToken().equals(subRangeStartToken); - PartitionPosition subRangeStartPP = isFirstSubrange ? startPP : subRangeStartToken.maxKeyBound(); - PartitionPosition subRangeEndPP = endPP.getToken().equals(subRangeEndToken) ? endPP : subRangeEndToken.maxKeyBound(); - // Need to preserve the fact it is a bounds for paging to work, a range is not left inclusive and will not start from where we left off - AbstractBounds subRange = isFirstSubrange ? bounds.withNewRight(subRangeEndPP) : new org.apache.cassandra.dht.Range(subRangeStartPP, subRangeEndPP); - return command.forSubRangeWithNowInSeconds(nowInSeconds, subRange, startTokenKey.equals(r.start())); - } - - private AsyncChain performLocalRead(PartitionRangeReadCommand command, Range r, long nowInSeconds) - { - Callable readCallable = () -> - { - PartitionRangeReadCommand read = commandForSubrange(command, r, nowInSeconds); - - try (ReadExecutionController controller = read.executionController(); - UnfilteredPartitionIterator partition = read.executeLocally(controller); - PartitionIterator iterator = UnfilteredPartitionIterators.filter(partition, read.nowInSec())) - { - TxnData result = new TxnData(); - TxnDataRangeValue value = new TxnDataRangeValue(); - while(iterator.hasNext()) - { - try (RowIterator rows = iterator.next()) - { - FilteredPartition filtered = FilteredPartition.create(rows); - if (filtered.hasRows() || read.selectsFullPartition()) - { - value.add(filtered); - } - } - } - result.put(TxnData.txnDataName(TxnDataNameKind.USER), value); - return result; - } - }; - - return AsyncChains.ofCallable(Stage.READ.executor(), readCallable, (callable, receiver) -> - new DebuggableTask.RunnableDebuggableTask() - { - private final long approxCreationTimeNanos = MonotonicClock.Global.approxTime.now(); - private volatile long approxStartTimeNanos; - - @Override - public void run() - { - approxStartTimeNanos = MonotonicClock.Global.approxTime.now(); - - try - { - Data call = callable.call(); - receiver.accept(call, null); - } - catch (Throwable t) - { - logger.debug("AsyncChain Callable threw an Exception", t); - receiver.accept(null, t); - } - } - - @Override - public long creationTimeNanos() - { - return approxCreationTimeNanos; - } - - @Override - public long startTimeNanos() - { - return approxStartTimeNanos; - } - - @Override - public String description() - { - return command.toCQLString(); - } - } - ); - } - - @Override - public Read slice(Ranges ranges) - { - return new TxnRangeRead(bytes(), cassandraConsistencyLevel, covering.slice(ranges, Slice.Minimal)); - } - - @Override - public Read intersecting(Participants participants) - { - checkArgument(participants.domain() == Domain.Range, "Can only intersect with ranges"); - return new TxnRangeRead(bytes(), cassandraConsistencyLevel, covering.intersecting(participants, Slice.Minimal)); - } - - @Override - public Read merge(Read other) - { - Ranges newCovering = ((TxnRangeRead)other).covering.union(UnionMode.MERGE_OVERLAPPING, covering); - return new TxnRangeRead(bytes(), cassandraConsistencyLevel, newCovering); - } - - @Override - public Kind kind() - { - return Kind.range; - } - - @Override - public long estimatedSizeOnHeap() - { - long size = EMPTY_SIZE; - size += ByteBufferUtil.estimatedSizeOnHeap(bytes()); - size += AccordObjectSizes.ranges(covering); - return size; - } - - public PartitionRangeReadCommand command() - { - return (PartitionRangeReadCommand) get(); - } - - public static final TxnReadSerializer serializer = new TxnReadSerializer() - { - @Override - public void serialize(TxnRangeRead read, DataOutputPlus out, int version) throws IOException - { - serializeNullable(read.cassandraConsistencyLevel, out, version, consistencyLevelSerializer); - KeySerializers.ranges.serialize(read.covering, out, version); - writeWithVIntLength(read.bytes(), out); - } - - @Override - public TxnRangeRead deserialize(DataInputPlus in, int version) throws IOException - { - ConsistencyLevel consistencyLevel = deserializeNullable(in, version, consistencyLevelSerializer); - Ranges covering = KeySerializers.ranges.deserialize(in, version); - ByteBuffer commandBytes = readWithVIntLength(in); - return new TxnRangeRead(commandBytes, consistencyLevel, covering); - } - - @Override - public long serializedSize(TxnRangeRead read, int version) - { - long size = 0; - size += KeySerializers.ranges.serializedSize(read.covering, version); - size += serializedNullableSize(read.cassandraConsistencyLevel, version, consistencyLevelSerializer); - size += serializedSizeWithVIntLength(read.bytes()); - return size; - } - }; - - @Override - protected IVersionedSerializer serializer() - { - return PartitionRangeReadCommand.serializer; - } - - @Override - public ConsistencyLevel cassandraConsistencyLevel() - { - return cassandraConsistencyLevel; - } -} diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnRead.java b/src/java/org/apache/cassandra/service/accord/txn/TxnRead.java index b908bdd2b5..d52cea531c 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnRead.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnRead.java @@ -19,75 +19,321 @@ package org.apache.cassandra.service.accord.txn; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import com.google.common.collect.ImmutableList; + +import accord.api.Data; +import accord.api.DataStore; +import accord.api.Key; import accord.api.Read; +import accord.local.SafeCommandStore; +import accord.primitives.Keys; +import accord.primitives.Participants; +import accord.primitives.Range; +import accord.primitives.Ranges; +import accord.primitives.Routable.Domain; +import accord.primitives.Seekable; +import accord.primitives.Seekables; +import accord.primitives.Timestamp; +import accord.utils.async.AsyncChain; +import accord.utils.async.AsyncChains; import org.apache.cassandra.db.ConsistencyLevel; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.io.IVersionedSerializer; import org.apache.cassandra.io.util.DataInputPlus; import org.apache.cassandra.io.util.DataOutputPlus; +import org.apache.cassandra.service.accord.api.PartitionKey; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.utils.ObjectSizes; -import static org.apache.cassandra.db.TypeSizes.sizeof; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static org.apache.cassandra.service.accord.AccordSerializers.consistencyLevelSerializer; +import static org.apache.cassandra.service.accord.IAccordService.SUPPORTED_READ_CONSISTENCY_LEVELS; +import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.CAS_READ; +import static org.apache.cassandra.service.accord.txn.TxnData.TxnDataNameKind.USER; +import static org.apache.cassandra.service.accord.txn.TxnData.txnDataName; +import static org.apache.cassandra.utils.ArraySerializers.deserializeArray; +import static org.apache.cassandra.utils.ArraySerializers.serializeArray; +import static org.apache.cassandra.utils.ArraySerializers.serializedArraySize; +import static org.apache.cassandra.utils.NullableSerializer.deserializeNullable; +import static org.apache.cassandra.utils.NullableSerializer.serializeNullable; +import static org.apache.cassandra.utils.NullableSerializer.serializedNullableSize; -public interface TxnRead extends Read +public class TxnRead extends AbstractKeySorted implements Read { - ConsistencyLevel cassandraConsistencyLevel(); + private static final TxnRead EMPTY_KEY = new TxnRead(Domain.Key); + private static final TxnRead EMPTY_RANGE = new TxnRead(Domain.Range); + private static final long EMPTY_SIZE = ObjectSizes.measure(EMPTY_KEY); + private static final Comparator TXN_NAMED_READ_KEY_COMPARATOR = Comparator.comparing(a -> ((PartitionKey) a.key())); + private static final byte TYPE_EMPTY_KEY = 0; + private static final byte TYPE_EMPTY_RANGE = 1; + private static final byte TYPE_NOT_EMPTY = 2; - public interface TxnReadSerializer extends IVersionedSerializer {} - - enum Kind + public static TxnRead empty(Domain domain) { - key(0), - range(1); - - int id; - - Kind(int id) + switch (domain) { - this.id = id; - } - - public TxnReadSerializer serializer() - { - switch (this) - { - case key: - return TxnKeyRead.serializer; - case range: - return TxnRangeRead.serializer; - default: - throw new IllegalStateException("Unrecognized kind " + this); - } + default: + throw new IllegalStateException("Unhandled domain " + domain); + case Key: + return EMPTY_KEY; + case Range: + return EMPTY_RANGE; } } - long estimatedSizeOnHeap(); + // Cassandra's consistency level used by Accord to safely read data written outside of Accord + @Nullable + private final ConsistencyLevel cassandraConsistencyLevel; - Kind kind(); + // Specifies the domain in case the TxnRead is empty and it can't be inferred + private final Domain domain; - void unmemoize(); - - IVersionedSerializer serializer = new IVersionedSerializer() + private TxnRead(Domain domain) { - @SuppressWarnings("unchecked") - @Override - public void serialize(TxnRead txnRead, DataOutputPlus out, int version) throws IOException + super(new TxnNamedRead[0], domain); + this.domain = domain; + this.cassandraConsistencyLevel = null; + } + + private TxnRead(@Nonnull TxnNamedRead[] items, @Nullable ConsistencyLevel cassandraConsistencyLevel) + { + super(items, items[0].key().domain()); + checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read: %s", cassandraConsistencyLevel); + this.cassandraConsistencyLevel = cassandraConsistencyLevel; + this.domain = items[0].key().domain(); + } + + private TxnRead(@Nonnull List items, @Nullable ConsistencyLevel cassandraConsistencyLevel) + { + super(items, items.get(0).key().domain()); + checkArgument(cassandraConsistencyLevel == null || SUPPORTED_READ_CONSISTENCY_LEVELS.contains(cassandraConsistencyLevel), "Unsupported consistency level for read: %s", cassandraConsistencyLevel); + this.cassandraConsistencyLevel = cassandraConsistencyLevel; + this.domain = items.get(0).key().domain(); + } + + private static void sortReads(List reads) + { + if (reads.size() == 0) + return; + reads.sort(TXN_NAMED_READ_KEY_COMPARATOR); + } + + public static TxnRead createTxnRead(@Nonnull List items, @Nullable ConsistencyLevel consistencyLevel, Domain domain) + { + if (items.isEmpty()) + return empty(domain); + sortReads(items); + return new TxnRead(items, consistencyLevel); + } + + public static TxnRead createSerialRead(List readCommands, ConsistencyLevel consistencyLevel) + { + List reads = new ArrayList<>(readCommands.size()); + for (int i = 0; i < readCommands.size(); i++) + reads.add(new TxnNamedRead(txnDataName(USER, i), readCommands.get(i))); + sortReads(reads); + return new TxnRead(reads, consistencyLevel); + } + + public static TxnRead createCasRead(SinglePartitionReadCommand readCommand, ConsistencyLevel consistencyLevel) + { + TxnNamedRead read = new TxnNamedRead(txnDataName(CAS_READ), readCommand); + return new TxnRead(ImmutableList.of(read), consistencyLevel); + } + + // A read that declares it will read from keys but doesn't actually read any data so dependent transactions will + // still be applied first + public static TxnRead createNoOpRead(Keys keys) + { + List reads = new ArrayList<>(keys.size()); + for (int i = 0; i < keys.size(); i++) + reads.add(new TxnNamedRead(txnDataName(USER, i), keys.get(i), null)); + return new TxnRead(reads, null); + } + + public static TxnRead createRangeRead(PartitionRangeReadCommand command, AbstractBounds range, ConsistencyLevel consistencyLevel) + { + return new TxnRead(ImmutableList.of(new TxnNamedRead(txnDataName(USER), range, command)), consistencyLevel); + } + + public long estimatedSizeOnHeap() + { + long size = EMPTY_SIZE; + for (TxnNamedRead read : items) + size += read.estimatedSizeOnHeap(); + return size; + } + + @Override + int compareNonKeyFields(TxnNamedRead left, TxnNamedRead right) + { + return Integer.compare(left.txnDataName(), right.txnDataName()); + } + + @Override + Seekable getKey(TxnNamedRead read) + { + return read.key(); + } + + @Override + TxnNamedRead[] newArray(int size) + { + return new TxnNamedRead[size]; + } + + @Override + public Seekables keys() + { + return itemKeys; + } + + @Override + public Domain domain() + { + return domain; + } + + public ConsistencyLevel cassandraConsistencyLevel() + { + return cassandraConsistencyLevel; + } + + @Override + public Read slice(Ranges ranges) + { + return intersecting(itemKeys.slice(ranges)); + } + + @Override + public Read intersecting(Participants participants) + { + return intersecting(itemKeys.intersecting(participants)); + } + + private Read intersecting(Seekables select) + { + // TODO (review): Why construct this keys at all and not just check against select? + Seekables keys = (Seekables)itemKeys.intersecting(select); + List reads = new ArrayList<>(keys.size()); + + switch (keys.domain()) { - out.writeByte(txnRead.kind().ordinal()); - txnRead.kind().serializer().serialize(txnRead, out, version); + case Key: + for (TxnNamedRead read : items) + if (keys.contains((Key)read.key())) + reads.add(read); + break; + case Range: + for (TxnNamedRead read : items) + if (keys.intersects((Range)read.key())) + reads.add(read); + break; + default: + throw new IllegalStateException("Unhandled domain " + keys.domain()); + } + + return createTxnRead(reads, cassandraConsistencyLevel, keys.domain()); + } + + @Override + public Read merge(Read read) + { + TxnRead txnRead = (TxnRead)read; + List reads = new ArrayList<>(items.length); + Collections.addAll(reads, items); + + for (TxnNamedRead namedRead : txnRead) + if (!reads.contains(namedRead)) + reads.add(namedRead); + + return createTxnRead(reads, cassandraConsistencyLevel, txnRead.domain); + } + + public void unmemoize() + { + for (TxnNamedRead read : items) + read.unmemoize(); + } + + @Override + public AsyncChain read(Seekable key, SafeCommandStore safeStore, Timestamp executeAt, DataStore store) + { + // Set to null since we don't need it and interop can pass in null + safeStore = null; + ClusterMetadata cm = ClusterMetadata.current(); + checkState(cm.epoch.getEpoch() >= executeAt.epoch(), "TCM epoch %d is < executeAt epoch %d", cm.epoch.getEpoch(), executeAt.epoch()); + + List> results = new ArrayList<>(); + forEachWithKey(key, read -> results.add(read.read(cassandraConsistencyLevel, key, executeAt))); + + if (results.isEmpty()) + // Result type must match everywhere + return AsyncChains.success(new TxnData()); + + if (results.size() == 1) + return results.get(0); + + return AsyncChains.reduce(results, Data::merge); + } + + public static final IVersionedSerializer serializer = new IVersionedSerializer() + { + @Override + public void serialize(TxnRead read, DataOutputPlus out, int version) throws IOException + { + if (read.items.length > 0) + { + out.write(TYPE_NOT_EMPTY); + serializeArray(read.items, out, version, TxnNamedRead.serializer); + serializeNullable(read.cassandraConsistencyLevel, out, version, consistencyLevelSerializer); + } + else + { + out.write(read.domain == Domain.Key ? TYPE_EMPTY_KEY : TYPE_EMPTY_RANGE); + } } @Override public TxnRead deserialize(DataInputPlus in, int version) throws IOException { - TxnRead.Kind kind = TxnRead.Kind.values()[in.readByte()]; - return (TxnRead)kind.serializer().deserialize(in, version); + byte type = in.readByte(); + switch (type) + { + default: + throw new IllegalStateException("Unhandled type " + type); + case TYPE_EMPTY_KEY: + return EMPTY_KEY; + case TYPE_EMPTY_RANGE: + return EMPTY_RANGE; + case TYPE_NOT_EMPTY: + TxnNamedRead[] items = deserializeArray(in, version, TxnNamedRead.serializer, TxnNamedRead[]::new); + ConsistencyLevel consistencyLevel = deserializeNullable(in, version, consistencyLevelSerializer); + return new TxnRead(items, consistencyLevel); + } } - @SuppressWarnings("unchecked") @Override - public long serializedSize(TxnRead txnRead, int version) + public long serializedSize(TxnRead read, int version) { - return sizeof((byte)txnRead.kind().ordinal()) + txnRead.kind().serializer().serializedSize(txnRead, version); + long size = 1; // type + if (read.items.length > 0) + { + size += serializedArraySize(read.items, version, TxnNamedRead.serializer); + size += serializedNullableSize(read.cassandraConsistencyLevel, version, consistencyLevelSerializer); + } + return size; } }; } diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnUpdate.java b/src/java/org/apache/cassandra/service/accord/txn/TxnUpdate.java index d2fb6aa307..b7930c84f7 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnUpdate.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnUpdate.java @@ -49,12 +49,14 @@ import org.apache.cassandra.service.accord.AccordObjectSizes; import org.apache.cassandra.service.accord.AccordSerializers; import org.apache.cassandra.service.accord.IAccordService; import org.apache.cassandra.service.accord.serializers.KeySerializers; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ObjectSizes; import static accord.utils.Invariants.requireArgument; import static accord.utils.SortedArrays.Search.CEIL; +import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.service.accord.AccordSerializers.consistencyLevelSerializer; import static org.apache.cassandra.service.accord.AccordSerializers.serialize; import static org.apache.cassandra.utils.ArraySerializers.deserializeArray; @@ -214,6 +216,8 @@ public class TxnUpdate extends AccordUpdate @Override public TxnWrite apply(Timestamp executeAt, Data data) { + ClusterMetadata cm = ClusterMetadata.current(); + checkState(cm.epoch.getEpoch() >= executeAt.epoch(), "TCM epoch %d is < executeAt epoch %d", cm.epoch.getEpoch(), executeAt.epoch()); if (!checkCondition(data)) return TxnWrite.EMPTY_CONDITION_FAILED; diff --git a/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java b/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java index 85c26e629d..bc8d5d2bb3 100644 --- a/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java +++ b/src/java/org/apache/cassandra/service/accord/txn/TxnWrite.java @@ -26,9 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.concurrent.TimeUnit; -import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +35,7 @@ import accord.api.DataStore; import accord.api.Write; import accord.local.SafeCommandStore; import accord.primitives.PartialTxn; +import accord.primitives.Routable.Domain; import accord.primitives.RoutableKey; import accord.primitives.Seekable; import accord.primitives.Timestamp; @@ -50,6 +49,7 @@ import org.apache.cassandra.db.Clustering; import org.apache.cassandra.db.Columns; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.RegularAndStaticColumns; import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.db.partitions.PartitionUpdate; @@ -58,11 +58,14 @@ 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.ColumnMetadata; +import org.apache.cassandra.service.accord.AccordObjectSizes; import org.apache.cassandra.service.accord.api.PartitionKey; +import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.BooleanSerializer; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ObjectSizes; +import static com.google.common.base.Preconditions.checkState; import static org.apache.cassandra.service.accord.AccordSerializers.partitionUpdateSerializer; import static org.apache.cassandra.utils.ArraySerializers.deserializeArray; import static org.apache.cassandra.utils.ArraySerializers.serializeArray; @@ -100,7 +103,7 @@ public class TxnWrite extends AbstractKeySorted implements Writ long estimatedSizeOnHeap() { return EMPTY_SIZE - + key.estimatedSizeOnHeap() + + AccordObjectSizes.key(key) + ByteBufferUtil.estimatedSizeOnHeap(bytes()); } @@ -135,7 +138,7 @@ public class TxnWrite extends AbstractKeySorted implements Writ PartitionUpdate update = get(); if (!preserveTimestamps) update = new PartitionUpdate.Builder(get(), 0).updateAllTimestamp(timestamp).build(); - Mutation mutation = new Mutation(update, true); + Mutation mutation = new Mutation(update, PotentialTxnConflicts.ALLOW); return AsyncChains.ofRunnable(Stage.MUTATION.executor(), mutation::applyUnsafe); } @@ -274,7 +277,7 @@ public class TxnWrite extends AbstractKeySorted implements Writ private static RegularAndStaticColumns columns(PartitionUpdate update, TxnReferenceOperations referenceOps) { - Preconditions.checkState(!referenceOps.isEmpty()); + checkState(!referenceOps.isEmpty()); RegularAndStaticColumns current = update.columns(); return new RegularAndStaticColumns(columns(current.statics, referenceOps.statics), columns(current.regulars, referenceOps.regulars)); @@ -287,7 +290,7 @@ public class TxnWrite extends AbstractKeySorted implements Writ if (existing != null && !existing.isEmpty()) { - Preconditions.checkState(existing.clustering().equals(clustering)); + checkState(existing.clustering().equals(clustering)); up.addRow(existing); } else @@ -335,13 +338,13 @@ public class TxnWrite extends AbstractKeySorted implements Writ private TxnWrite(Update[] items, boolean isConditionMet) { - super(items); + super(items, Domain.Key); this.isConditionMet = isConditionMet; } public TxnWrite(List items, boolean isConditionMet) { - super(items); + super(items, Domain.Key); this.isConditionMet = isConditionMet; } @@ -352,11 +355,17 @@ public class TxnWrite extends AbstractKeySorted implements Writ } @Override - PartitionKey getKey(Update item) + Seekable getKey(Update item) { return item.key; } + @Override + Domain domain() + { + return Domain.Key; + } + @Override Update[] newArray(int size) { @@ -372,6 +381,8 @@ public class TxnWrite extends AbstractKeySorted implements Writ @Override public AsyncChain apply(Seekable key, SafeCommandStore safeStore, TxnId txnId, Timestamp executeAt, DataStore store, PartialTxn txn) { + ClusterMetadata cm = ClusterMetadata.current(); + checkState(cm.epoch.getEpoch() >= executeAt.epoch(), "TCM epoch %d is < executeAt epoch %d", cm.epoch.getEpoch(), executeAt.epoch()); // UnrecoverableRepairUpdate will deserialize as null at other nodes // Accord should skip the Update for a read transaction, but handle it here anyways TxnUpdate txnUpdate = ((TxnUpdate)txn.update()); @@ -379,7 +390,6 @@ public class TxnWrite extends AbstractKeySorted implements Writ return Writes.SUCCESS; long timestamp = executeAt.uniqueHlc(); - int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestamp); // TODO (expected): optimise for the common single update case; lots of lists allocated List> results = new ArrayList<>(); diff --git a/src/java/org/apache/cassandra/service/consensus/TransactionalMode.java b/src/java/org/apache/cassandra/service/consensus/TransactionalMode.java index b76d5ccb2d..3c38b8b4e1 100644 --- a/src/java/org/apache/cassandra/service/consensus/TransactionalMode.java +++ b/src/java/org/apache/cassandra/service/consensus/TransactionalMode.java @@ -18,11 +18,9 @@ package org.apache.cassandra.service.consensus; -import com.google.common.collect.ImmutableList; - import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.dht.NormalizedRanges; -import org.apache.cassandra.dht.Range; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Token; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.accord.IAccordService; @@ -32,7 +30,6 @@ import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.utils.LocalizeString; import static com.google.common.base.Preconditions.checkState; -import static org.apache.cassandra.dht.NormalizedRanges.normalizedRanges; /* * Configure the transactional behavior of a table. Enables accord on a table and defines how it mixes with non-serial writes @@ -70,33 +67,7 @@ import static org.apache.cassandra.dht.NormalizedRanges.normalizedRanges; public enum TransactionalMode { // Running on Paxos V1 or V2 with Accord disabled - off(false, false, false, false, false), - - // TODO (maybe): These unsafe modes don't have Accord do async commit and single replica reads so how useful are they besides preserving non-SERIAL performance? - // These modes are unsafe when Accord and non-SERIAL reads and writes interact with the same data - // They don't guarantee that non-SERIAL reads or writes will see the latest Accord writes or that - // Accord transactions will recover correctly - - /* - * Enables Accord but does not allow non-SERIAL reads and writes to occur safely to data read/written by Accord - * - * Execute non-SERIAL writes through Cassandra via StorageProxy's normal write path. This can lead Accord to compute - * multiple outcomes for a transaction that depend on data written by non-SERIAL writes. - * - * SERIAL reads and CAS will run on Accord. Accord will honor provided consistency levels and do synchronous commit - * so the results can be read correctly with non-SERIAL CLs, but read repair could interfere with Accord. - */ - unsafe(true, false, false, false, false), - - /* - * Enables Accord and makes it safe to perform non-SERIAL reads of Accord data without guaranteeing that they will - * see the latest Accord writes. non-SERIAL writes to data read by Accord will make Accord txn recovery non-deterministic - * - * Allow mixing of non-SERIAL writes and Accord, but still force BRR through Accord. - * This mode makes it safe to perform non-SERIAL or SERIAL reads of Accord data, but unsafe - * to write data that Accord may attempt to read. - */ - unsafe_writes(true, false, false, false, true), + off(false, false, false, false), // These modes always provide correct execution with mixed_reads allow non-transaction non-SERIAL operations // at the expense of slower Accord transactions, and full allowing faster transaction execution, but forcing @@ -107,37 +78,68 @@ public enum TransactionalMode * writes at the provided consistency level so they can be read via non-SERIAL consistency levels. * This mode makes it safe to read/write data that Accord will read/write. */ - mixed_reads(true, false, true, false, true), + mixed_reads(true, true, false, true), /* * Execute writes through Accord skipping StorageProxy's normal write path. Ignores the provided consistency level * which makes Accord commit writes at ANY similar to Paxos with commit consistency level ANY. */ - full(true, true, true, true, true), + full(true, true, true, true), + + // TODO (maybe): These unsafe modes don't have Accord do async commit and single replica reads so how useful are they besides preserving non-SERIAL performance? + // These modes are unsafe when Accord and non-SERIAL reads and writes interact with the same data + // They don't guarantee that non-SERIAL reads or writes will see the latest Accord writes or that + // Accord transactions will recover correctly + + /* + * Enables Accord and makes it safe to perform non-SERIAL reads of Accord data without guaranteeing that they will + * see the latest Accord writes. non-SERIAL writes to data read by Accord will make Accord txn recovery non-deterministic + * + * Allow mixing of non-SERIAL writes and Accord, but still force BRR through Accord. + * This mode makes it safe to perform non-SERIAL or SERIAL reads of Accord data, but unsafe + * to write data that Accord may attempt to read. + */ + test_unsafe_writes(true, false, false, true), + + /* + * Enables Accord but does not allow non-SERIAL reads and writes to occur safely to data read/written by Accord + * + * Execute non-SERIAL writes through Cassandra via StorageProxy's normal write path. This can lead Accord to compute + * multiple outcomes for a transaction that depend on data written by non-SERIAL writes. + * + * SERIAL reads and CAS will run on Accord. Accord will honor provided consistency levels and do synchronous commit + * so the results can be read correctly with non-SERIAL CLs, but read repair could interfere with Accord. + **/ + test_unsafe(true, false, false, false), // For tests, Accord will read and be forced to do interop reads - test_interop_read(true, false, false, true, true); + test_interop_read(true, false, true, true); public final boolean accordIsEnabled; - public final boolean ignoresSuppliedCommitCL; public final boolean nonSerialWritesThroughAccord; public final boolean nonSerialReadsThroughAccord; public final boolean blockingReadRepairThroughAccord; private final String cqlParam; - TransactionalMode(boolean accordIsEnabled, boolean ignoreSuppliedCommitCL, boolean nonSerialWritesThroughAccord, boolean nonSerialReadsThroughAccord, boolean blockingReadRepairThroughAccord) + TransactionalMode(boolean accordIsEnabled, boolean nonSerialWritesThroughAccord, boolean nonSerialReadsThroughAccord, boolean blockingReadRepairThroughAccord) { this.accordIsEnabled = accordIsEnabled; - this.ignoresSuppliedCommitCL = ignoreSuppliedCommitCL; this.nonSerialWritesThroughAccord = nonSerialWritesThroughAccord; this.nonSerialReadsThroughAccord = nonSerialReadsThroughAccord; this.blockingReadRepairThroughAccord = blockingReadRepairThroughAccord; this.cqlParam = String.format("transactional_mode = '%s'", LocalizeString.toLowerCaseLocalized(this.name())); + checkState(this.name().startsWith("test_") || (nonSerialReadsThroughAccord && nonSerialWritesThroughAccord) || !nonSerialReadsThroughAccord, "Doesn't make sense to do non-SERIAL reads through Accord without also doing non-SERIAL writes through Accord"); } - public ConsistencyLevel commitCLForStrategy(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, Token token) + // This can be inferred from whether non-SERIAL reads are done through Accord + public boolean ignoresSuppliedCommitCL() { - if (ignoresSuppliedCommitCL) + return nonSerialReadsThroughAccord; + } + + public ConsistencyLevel commitCLForMode(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, Token token) + { + if (ignoresSuppliedCommitCL()) { TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tableId); checkState(tms != null || fromMode == TransactionalMigrationFromMode.none); @@ -160,13 +162,12 @@ public enum TransactionalMode * Infer whether Accord can ignore the read CL and bias towards correctness by reading from a quorum * if it's needed due to how non-SERIAL writes are done */ - private boolean ignoresSuppliedReadCL() + public boolean ignoresSuppliedReadCL() { return nonSerialWritesThroughAccord && blockingReadRepairThroughAccord; } - - public ConsistencyLevel readCLForStrategy(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, Token token) + public ConsistencyLevel readCLForMode(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, Token token) { if (ignoresSuppliedReadCL()) { @@ -187,19 +188,18 @@ public enum TransactionalMode return consistencyLevel; } - public ConsistencyLevel readCLForStrategy(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, Range range) + public ConsistencyLevel readCLForMode(TransactionalMigrationFromMode fromMode, ConsistencyLevel consistencyLevel, ClusterMetadata cm, TableId tableId, AbstractBounds range) { if (ignoresSuppliedReadCL()) { TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tableId); checkState(tms != null || fromMode == TransactionalMigrationFromMode.none); - NormalizedRanges ranges = normalizedRanges(ImmutableList.of(range)); // Only ignore the supplied consistency level if none of the range is migrating // otherwise honor it because we might read through Accord for non-SERIAL reads before repair is run // this is OK to do because BRR still works and Accord isn't computing a write so recovery // determinism isn't an issue - if (tms == null || tms.migratedRanges.intersection(ranges).equals(ranges)) + if (tms == null || range.intersects(tms.migratedRangesAsPartitionPosition())) return null; } @@ -232,4 +232,9 @@ public enum TransactionalMode { return valueOf(LocalizeString.toLowerCaseLocalized(name)); } + + public boolean isTestMode() + { + return name().startsWith("test_"); + } } diff --git a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusKeyMigrationState.java b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusKeyMigrationState.java index e980e0422b..c6dc28c4a8 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusKeyMigrationState.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusKeyMigrationState.java @@ -20,17 +20,21 @@ package org.apache.cassandra.service.consensus.migration; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; +import java.util.SortedSet; import java.util.UUID; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import accord.api.BarrierType; +import accord.primitives.Keys; import accord.primitives.Seekables; import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; @@ -66,6 +70,7 @@ import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.Collectors3; import org.apache.cassandra.utils.ObjectSizes; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.UUIDSerializer; @@ -176,7 +181,7 @@ public abstract class ConsensusKeyMigrationState repairKeyAccord(key, tms.tableId, tms.minMigrationEpoch(key.getToken()).getEpoch(), Dispatcher.RequestTime.forImmediateExecution(), false, isForWrite); } - private boolean paxosReadSatisfiedByKeyMigration() + boolean paxosReadSatisfiedByKeyMigration() { // No migration in progress, it's safe if (tableMigrationState == null) @@ -230,12 +235,6 @@ public abstract class ConsensusKeyMigrationState saveConsensusKeyMigrationLocally(key, tableUUID, migratedAt); } - /* - * Should be called where we know we replicate the key so that the system table contains useful information - * about whether the migration already occurred. - * - * This is a more expensive check that might read from the system table to determine if migration occurred. - */ public static KeyMigrationState getKeyMigrationState(TableId tableId, DecoratedKey key) { ClusterMetadata cm = ClusterMetadata.current(); @@ -243,10 +242,20 @@ public abstract class ConsensusKeyMigrationState // No state means no migration for this table if (tms == null) return KeyMigrationState.MIGRATION_NOT_NEEDED; + return getKeyMigrationState(cm, tms, key); + } + /* + * Should be called where we know we replicate the key so that the system table contains useful information + * about whether the migration already occurred. + * + * This is a more expensive check that might read from the system table to determine if migration occurred. + */ + static KeyMigrationState getKeyMigrationState(ClusterMetadata cm, TableMigrationState tms, DecoratedKey key) + { if (tms.migratingRanges.intersects(key.getToken())) { - ConsensusMigratedAt consensusMigratedAt = getConsensusMigratedAt(tableId, key); + ConsensusMigratedAt consensusMigratedAt = getConsensusMigratedAt(tms.tableId, key); if (consensusMigratedAt == null) return new KeyMigrationState(null, cm.epoch, tms, key); return new KeyMigrationState(consensusMigratedAt, cm.epoch, tms, key); @@ -269,6 +278,16 @@ public abstract class ConsensusKeyMigrationState Dispatcher.RequestTime requestTime, boolean global, boolean isForWrite) + { + repairKeysAccord(ImmutableList.of(key), tableId, minEpoch, requestTime, global, isForWrite); + } + + static void repairKeysAccord(List keys, + TableId tableId, + long minEpoch, + Dispatcher.RequestTime requestTime, + boolean global, + boolean isForWrite) { ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId); if (isForWrite) @@ -282,7 +301,8 @@ 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; - Seekables keysOrRanges = AccordService.instance().barrier(Seekables.of(new PartitionKey(tableId, key)), minEpoch, requestTime, DatabaseDescriptor.getTransactionTimeout(TimeUnit.NANOSECONDS), barrierType, isForWrite); + SortedSet partitionKeys = keys.stream().map(key -> new PartitionKey(tableId, key)).collect(Collectors3.toSortedSet()); + Seekables keysOrRanges = AccordService.instance().barrier(new Keys(partitionKeys), 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. diff --git a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationMutationHelper.java b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationMutationHelper.java index 2ae6f8622e..81c29b7d09 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationMutationHelper.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationMutationHelper.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import accord.coordinate.CoordinationFailed; import accord.primitives.Keys; +import accord.primitives.Routable.Domain; import accord.primitives.Txn; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; @@ -54,8 +55,8 @@ 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.TxnKeyRead; 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; @@ -115,10 +116,10 @@ public class ConsensusMigrationMutationHelper TableParams tableParams = getTableMetadata(cm, tableId).params; TransactionalMode mode = tableParams.transactionalMode; TransactionalMigrationFromMode migrationFromMode = tableParams.transactionalMigrationFrom; - // commitCLForStrategy should return either null or the supplied consistency level + // commitCLForMode 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(migrationFromMode, consistencyLevel, cm, tableId, mutation.key().getToken()); + ConsistencyLevel commitCL = mode.commitCLForMode(migrationFromMode, consistencyLevel, cm, tableId, mutation.key().getToken()); if (commitCL != null) return commitCL; } @@ -217,11 +218,11 @@ public class ConsensusMigrationMutationHelper public SplitMutation splitMutationIntoAccordAndNormal(T mutation, ClusterMetadata cm) { - if (mutation.allowsPotentialTransactionConflicts()) + if (mutation.potentialTxnConflicts().allowed) return new SplitMutation<>(null, mutation); Token token = mutation.key().getToken(); - Predicate isAccordUpdate = tableId -> tokenShouldBeWrittenThroughAccord(cm, tableId, token, TransactionalMode::nonSerialWritesThroughAccord, TransactionalMigrationFromMode::nonSerialReadsThroughAccord); + Predicate isAccordUpdate = tableId -> tokenShouldBeWrittenThroughAccord(cm, tableId, token, TransactionalMode::nonSerialWritesThroughAccord, TransactionalMigrationFromMode::nonSerialWritesThroughAccord); T accordMutation = (T)mutation.filter(isAccordUpdate); T normalMutation = (T)mutation.filter(not(isAccordUpdate)); @@ -258,15 +259,15 @@ public class ConsensusMigrationMutationHelper // 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), TxnKeyRead.EMPTY, TxnQuery.NONE, update); + Txn.InMemory txn = new Txn.InMemory(Keys.of(partitionKeys), TxnRead.empty(Domain.Key), TxnQuery.NONE, update); IAccordService accordService = AccordService.instance(); try { - return accordService.coordinateAsync(minEpoch, txn, consistencyLevel, requestTime); + return accordService.coordinateAsync(minEpoch, txn, clForCommit, requestTime); } catch (CoordinationFailed coordinationFailed) { - AsyncTxnResult failure = new AsyncTxnResult(coordinationFailed.txnId()); + AsyncTxnResult failure = new AsyncTxnResult(coordinationFailed.txnId(), minEpoch, clForCommit, true, requestTime); failure.setFailure(coordinationFailed.wrap()); return failure; } @@ -274,15 +275,16 @@ public class ConsensusMigrationMutationHelper public static void validateSafeToExecuteNonTransactionally(IMutation mutation) throws RetryOnDifferentSystemException { - if (mutation.allowsPotentialTransactionConflicts()) + if (mutation.potentialTxnConflicts().allowed) return; + String keyspace = mutation.getKeyspaceName(); // System keyspaces are never managed by Accord - if (SchemaConstants.isSystemKeyspace(mutation.getKeyspaceName())) + if (SchemaConstants.isSystemKeyspace(keyspace)) return; // Local keyspaces are never managed by Accord - if (Schema.instance.localKeyspaces().containsKeyspace(mutation.getKeyspaceName())) + if (Schema.instance.localKeyspaces().containsKeyspace(keyspace)) return; ClusterMetadata cm = ClusterMetadata.current(); @@ -297,7 +299,7 @@ public class ConsensusMigrationMutationHelper { TableId tableId = pu.metadata().id; ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId); - if (tokenShouldBeWrittenThroughAccord(cm, tableId, dk.getToken(), TransactionalMode::nonSerialWritesThroughAccord, TransactionalMigrationFromMode::nonSerialReadsThroughAccord)) + if (tokenShouldBeWrittenThroughAccord(cm, tableId, dk.getToken(), TransactionalMode::nonSerialWritesThroughAccord, TransactionalMigrationFromMode::nonSerialWritesThroughAccord)) { throwRetryOnDifferentSystem = true; if (markedColumnFamilies == null) @@ -350,21 +352,18 @@ public class ConsensusMigrationMutationHelper // 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 + // Accord needs to do synchronous commit and respect the consistency level so non-SERIAL reads can read Accord's + // writes. if (transactionalModeWritesThroughAccord) + { return tms.migratingAndMigratedRanges.intersects(token); + } - // 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 we are migrating from a mode that used to write to Accord then any range that isn't migrated + // should continue to write through Accord. Accord might still be executing txns pre-migration so continue + // to route writes through Accord until migration is completed. if (migrationFromWritesThroughAccord) - return !tms.migratingAndMigratedRanges.intersects(token); + return !tms.migratedRanges.intersects(token); } return false; } diff --git a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationState.java b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationState.java index e01a796aff..67d43ab77b 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationState.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusMigrationState.java @@ -33,7 +33,6 @@ import javax.annotation.Nullable; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -147,10 +146,10 @@ public class ConsensusMigrationState implements MetadataValue>> migratingRangesByEpoch = ImmutableMap.of(); if (!ranges.isEmpty()) - ImmutableMap.of(Epoch.EMPTY, ranges); + migratingRangesByEpoch = ImmutableMap.of(Epoch.EMPTY, ranges); if (overwrite) - tableState = new TableMigrationState(metadata.keyspace, metadata.name, metadata.id, target, ImmutableSet.of(), initialRepairPendingRanges(target, ranges), migratingRangesByEpoch); + tableState = new TableMigrationState(metadata.keyspace, metadata.name, metadata.id, target, ImmutableList.of(), initialRepairPendingRanges(target, ranges), migratingRangesByEpoch); else tableState = tableState.withRangesMigrating(ranges, target); @@ -168,6 +167,7 @@ public class ConsensusMigrationState implements MetadataValue current, ImmutableMap.Builder next, Collection changed) { Set changedIds = changed.stream().map(TableMetadata::id).collect(Collectors.toSet()); + putUnchanged(current, next, changedIds); } @@ -183,12 +183,6 @@ public class ConsensusMigrationState implements MetadataValue updated = ImmutableMap.builder(); putUnchanged(tableStates, updated, new HashSet<>(completed)); - for (Map.Entry entry : tableStates.entrySet()) - { - if (completed.contains(entry.getKey())) - continue; - updated.put(entry); - } return new ConsensusMigrationState(lastModified, updated.build()); } @@ -208,7 +202,6 @@ public class ConsensusMigrationState implements MetadataValue removed) diff --git a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusRequestRouter.java b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusRequestRouter.java index 168be2145f..83ea27cb2a 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusRequestRouter.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusRequestRouter.java @@ -18,10 +18,15 @@ package org.apache.cassandra.service.consensus.migration; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.function.BiPredicate; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import accord.primitives.Routable.Domain; import accord.primitives.Seekables; @@ -31,25 +36,42 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; +import org.apache.cassandra.db.PartitionRangeReadCommand; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.SinglePartitionReadCommand; +import org.apache.cassandra.dht.AbstractBounds; +import org.apache.cassandra.dht.Bounds; +import org.apache.cassandra.dht.NormalizedRanges; +import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.exceptions.InvalidRequestException; +import org.apache.cassandra.exceptions.RetryOnDifferentSystemException; 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.SchemaConstants; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.schema.TableParams; +import org.apache.cassandra.service.accord.TokenRange; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.MinTokenKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.SentinelKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.consensus.TransactionalMode; +import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState.KeyMigrationState; import org.apache.cassandra.service.paxos.Paxos; +import org.apache.cassandra.service.reads.ReadCoordinator; import org.apache.cassandra.tcm.ClusterMetadata; -import org.apache.cassandra.tcm.ClusterMetadataService; -import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static org.apache.cassandra.dht.Range.compareRightToken; 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; @@ -59,6 +81,10 @@ import static org.apache.cassandra.service.consensus.migration.ConsensusRequestR /** * Helper class to decide where to route a request that requires consensus, migrating a key if necessary * before rerouting. + * + * This router has to be used for all SERIAL reads and writes to ensure the correct operation of Paxos/Acocrd during migration + * and for all non-SERIAL reads because non-SERIAL reads may end up being routed to Accord and Accord needs CRR to manage + * any key migrations that need to be performed */ public class ConsensusRequestRouter { @@ -226,10 +252,18 @@ public class ConsensusRequestRouter // Accord -> Paxos - Paxos will ask Accord to migrate in the read at each replica if necessary // Paxos -> Accord - Paxos needs to be repaired before Accord runs so do it here if (tms.targetProtocol == paxos) + // TODO (important): Why are these two cases paxosV2 instead of `pickPaxos`? + // Because we only supported PaxosV2 for migration? + // Eventually we want to support both so just use pickPaxos and error out on migration from paxosV1 elsewhere? return paxosV2; else - // Should exit exceptionally if the repair is not done - ConsensusKeyMigrationState.repairKeyPaxos(naturalReplicas, cm.epoch, key, cfs, consistencyLevel, requestTime, timeoutNanos, isLocallyReplicated, isForWrite); + { + if (tms.accordSafeToReadRanges.intersects(key.getToken())) + // Should exit exceptionally if the repair is not done + ConsensusKeyMigrationState.repairKeyPaxos(naturalReplicas, cm.epoch, key, cfs, consistencyLevel, requestTime, timeoutNanos, isLocallyReplicated, isForWrite); + else + return pickPaxos(); + } return pickMigrated(tms.targetProtocol); } @@ -274,31 +308,169 @@ public class ConsensusRequestRouter return false; } - public boolean isKeyInMigratingOrMigratedRangeFromAccord(Epoch epoch, TableId tableId, DecoratedKey key) + public boolean isRangeManagedByAccordForReadAndWrite(ClusterMetadata cm, TableId tableId, TokenRange range) { - ClusterMetadata cm = ClusterMetadataService.instance().fetchLogFromCMS(epoch); + TableMetadata metadata = getTableMetadata(cm, tableId); + TransactionalMode transactionalMode = metadata.params.transactionalMode; + TransactionalMigrationFromMode transactionalMigrationFromMode = metadata.params.transactionalMigrationFrom; TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tableId); - return isKeyInMigratingOrMigratedRangeFromAccord(getTableMetadata(cm, tableId), tms, key); + if (tms == null) + { + checkState(transactionalMigrationFromMode == TransactionalMigrationFromMode.none, "TableMigrationState shouldn't be null during migration"); + return transactionalMode.nonSerialReadsThroughAccord; + } + + // = token ends up as a min and max key bound in C* parlance and min and max token key in Accord parlance + // and the conversion to a C* range results in the unintentional creation of a wrap around range. + // Instead treat it like a key and do that check. + if (range.start().getClass() == MinTokenKey.class + && range.end() instanceof TokenKey + && range.start().token().equals(range.end().token())) + { + checkState(range.end().getClass() != MinTokenKey.class, "Unexpected empty range"); + return isTokenManagedByAccordForReadAndWrite(metadata, tms, range.start().token()); + } + else if (range.start().getClass() == MinTokenKey.class) + { + // Start is particularly problematic because we use min MinTokenKey to make start inclusive and this is something + // that isn't possible to mimic at all with Range, for end it's less problematic because just the token + // is sufficient for Accord to route the query and select the correct shards even if it might accidentally run + // on an extra shard, the filtering will take care of it. There is nothing to do here but convert to a bounds + // and use the bounds check + PartitionPosition startPP = range.start().token().minKeyBound(); + PartitionPosition endPP; + if (range.end().getClass() == SentinelKey.class) + endPP = DatabaseDescriptor.getPartitioner().getMinimumToken().maxKeyBound(); + else if (range.end().getClass() == MinTokenKey.class) + endPP = range.end().token().minKeyBound(); + else + endPP = range.end().token().maxKeyBound(); + Bounds bounds = new Bounds<>(startPP, endPP); + return isBoundsExclusivelyManagedByAccordForRead(transactionalMode, transactionalMigrationFromMode, tms, bounds); + } + else + { + return isRangeManagedByAccordForReadAndWrite(metadata, + cm.consensusMigrationState.tableStates.get(tableId), + range.toKeyspaceRange()); + } + } + + /* + * A lightweight check against cluster metadata that doesn't check if the range has already been migrated + * using local system table state. It just assumes that the key migration has already been done. + * + * This version is for is full read write transactions + */ + public boolean isRangeManagedByAccordForReadAndWrite(TableMetadata metadata, TableMigrationState tms, Range range) + { + checkState(!range.isTrulyWrapAround(), "Accidentally created a wrap around range"); + TransactionalMode transactionalMode = metadata.params.transactionalMode; + TransactionalMigrationFromMode migrationFrom = metadata.params.transactionalMigrationFrom; + + if (migrationFrom.isMigrating()) + checkState(tms != null, "Can't have migration in progress without tms"); + + if (transactionalMode.accordIsEnabled) + { + if (!migrationFrom.isMigrating()) + return true; + if (migrationFrom.migratingFromAccord()) + return true; + // Accord can only read/write the key if it is in a safe to read (repaired) range + if (Range.intersects(tms.accordSafeToReadRanges, ImmutableList.of(range))) + return true; + } + else + { + // Once the migration starts only barriers are allowed to run for the key in Accord + if (migrationFrom.migratingFromAccord() && !Range.intersects(tms.migratingAndMigratedRanges, ImmutableList.of(range))) + return true; + } + + return false; + } + + public boolean isKeyManagedByAccordForReadAndWrite(ClusterMetadata cm, TableId tableId, DecoratedKey key) + { + return isTokenManagedByAccordForReadAndWrite(getTableMetadata(cm, tableId), + cm.consensusMigrationState.tableStates.get(tableId), + key.getToken()); } /* * A lightweight check against cluster metadata that doesn't check if the key has already been migrated - * using local system table state. + * using local system table state. It just assumes that the key migration has already been done. + * + * This version is for is full read write transactions */ - public boolean isKeyInMigratingOrMigratedRangeFromAccord(TableMetadata metadata, TableMigrationState tms, DecoratedKey key) + public boolean isTokenManagedByAccordForReadAndWrite(TableMetadata metadata, TableMigrationState tms, Token token) { - if (!metadata.params.transactionalMigrationFrom.isMigrating()) - return false; + TransactionalMode transactionalMode = metadata.params.transactionalMode; + TransactionalMigrationFromMode migrationFrom = metadata.params.transactionalMigrationFrom; - // No state means no migration for this table - if (tms == null) - return false; + if (migrationFrom.isMigrating()) + checkState(tms != null, "Can't have migration in progress without tms"); - if (tms.targetProtocol == ConsensusMigrationTarget.accord) - return false; + if (transactionalMode.accordIsEnabled) + { + if (!migrationFrom.isMigrating()) + return true; + if (migrationFrom.migratingFromAccord()) + return true; + // Accord can only read/write the key if it is in a safe to read (repaired) range + if (tms.accordSafeToReadRanges.intersects(token)) + return true; + } + else + { + // Once the migration starts only barriers are allowed to run for the key in Accord + if (migrationFrom.migratingFromAccord() && !tms.migratingAndMigratedRanges.intersects(token)) + return true; + } - if (tms.migratingAndMigratedRanges.intersects(key.getToken())) - return true; + return false; + } + + public boolean isKeyManagedByAccordForWrite(ClusterMetadata cm, TableId tableId, DecoratedKey key) + { + return isKeyManagedByAccordForWrite(getTableMetadata(cm, tableId), + cm.consensusMigrationState.tableStates.get(tableId), + key); + } + + /* + * A lightweight check against cluster metadata that doesn't check if the key has already been migrated + * using local system table state. It just assumes that the key migration has already been done. + * + * This version is for writes through Accord before Accord is able to safely read. + */ + public boolean isKeyManagedByAccordForWrite(TableMetadata metadata, TableMigrationState tms, DecoratedKey key) + { + TransactionalMode transactionalMode = metadata.params.transactionalMode; + TransactionalMigrationFromMode migrationFrom = metadata.params.transactionalMigrationFrom; + Token token = key.getToken(); + + if (migrationFrom.isMigrating()) + checkState(tms != null, "Can't have migration in progress without tms"); + + if (transactionalMode.accordIsEnabled) + { + if (!migrationFrom.isMigrating()) + return true; + if (migrationFrom.migratingFromAccord()) + return true; + // Accord can blind write to the key even if it isn't safe to read from it so use migratingAndMigratedRanges + if (tms.migratingAndMigratedRanges.intersects(token)) + return true; + } + else + { + // We can always allow writes through Accord and it's necessary to do that so that + // andy premigration txns aren't exposed to non-transactional writes + if (migrationFrom.nonSerialWritesThroughAccord() && !tms.migratedRanges.intersects(token)) + return true; + } return false; } @@ -340,4 +512,409 @@ public class ConsensusRequestRouter { return Paxos.useV2() ? paxosV2 : paxosV1; } + + public static void validateSafeToReadNonTransactionally(ReadCommand command) + { + if (command.potentialTxnConflicts().allowed) + return; + + String keyspace = command.metadata().keyspace; + // System keyspaces are never managed by Accord + if (SchemaConstants.isSystemKeyspace(keyspace)) + return; + + // Local keyspaces are never managed by Accord + if (Schema.instance.localKeyspaces().containsKeyspace(keyspace)) + return; + + ClusterMetadata cm = ClusterMetadata.current(); + TableId tableId = command.metadata().id; + TableMetadata tableMetadata = getTableMetadata(cm, tableId); + // Null for local or dropped tables + if (tableMetadata == null) + return; + + TransactionalMode transactionalMode = tableMetadata.params.transactionalMode; + TransactionalMigrationFromMode transactionalMigrationFromMode = tableMetadata.params.transactionalMigrationFrom; + if (!transactionalMode.nonSerialReadsThroughAccord && !transactionalMigrationFromMode.nonSerialReadsThroughAccord()) + return; + + TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tableId); + + // Null with a transaction mode that reads through Accord indicates a completed migration or table created + // to use Accord initially + if (tms == null) + { + checkState(transactionalMigrationFromMode == TransactionalMigrationFromMode.none); + if (transactionalMode.nonSerialReadsThroughAccord) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId); + if (cfs != null) + cfs.metric.readsRejectedOnWrongSystem.mark(); + throw new RetryOnDifferentSystemException(); + } + } + + boolean isExclusivelyReadableFromAccord; + if (command.isRangeRequest()) + isExclusivelyReadableFromAccord = isBoundsExclusivelyManagedByAccordForRead(transactionalMode, transactionalMigrationFromMode, tms, command.dataRange().keyRange()); + else + isExclusivelyReadableFromAccord = isTokenExclusivelyManagedByAccordForRead(transactionalMode, transactionalMigrationFromMode, tms, ((SinglePartitionReadCommand)command).partitionKey().getToken()); + + if (isExclusivelyReadableFromAccord) + { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(tableId); + if (cfs != null) + cfs.metric.readsRejectedOnWrongSystem.mark(); + throw new RetryOnDifferentSystemException(); + } + } + + private static boolean isTokenExclusivelyManagedByAccordForRead(@Nonnull TransactionalMode transactionalMode, + @Nonnull TransactionalMigrationFromMode migrationFrom, + @Nonnull TableMigrationState tms, + @Nonnull Token token) + { + checkNotNull(transactionalMode, "transactionalMode is null"); + checkNotNull(migrationFrom, "migrationFrom is null"); + checkNotNull(tms, "tms (TableMigrationState) is null"); + checkNotNull(token, "bounds is null"); + + if (transactionalMode.accordIsEnabled) + { + if (!migrationFrom.isMigrating()) + return true; + if (migrationFrom.migratingFromAccord()) + return true; + + // Accord is exclusive once the range is fully migrated to Accord, but possible to read from safely + // when accordSafeToReadRanges covers the entire bound + if (tms.migratedRanges.intersects(token)) + return true; + } + else + { + // Once the migration starts only barriers are allowed to run for the key in Accord + if (migrationFrom.migratingFromAccord() && !tms.migratingAndMigratedRanges.intersects(token)) + return true; + } + + return false; + } + + // Returns true if any part of the bound + private static boolean isBoundsExclusivelyManagedByAccordForRead(@Nonnull TransactionalMode transactionalMode, + @Nonnull TransactionalMigrationFromMode migrationFrom, + @Nonnull TableMigrationState tms, + @Nonnull AbstractBounds bounds) + { + checkNotNull(transactionalMode, "transactionalMode is null"); + checkNotNull(migrationFrom, "migrationFrom is null"); + checkNotNull(tms, "tms (TableMigrationState) is null"); + checkNotNull(bounds, "bounds is null"); + + BiPredicate, NormalizedRanges> intersects = (testBounds, testRanges) -> { + // TODO (nicetohave): Efficiency of this intersection + for (org.apache.cassandra.dht.Range range : testRanges) + { + Pair, AbstractBounds> intersectionAndRemainder = Range.intersectionAndRemainder(testBounds, range); + return intersectionAndRemainder.left != null; + } + return false; + }; + + if (bounds.left.getToken().equals(bounds.right.getToken()) && !bounds.inclusiveLeft() && bounds.inclusiveRight()) + { + return isTokenExclusivelyManagedByAccordForRead(transactionalMode, migrationFrom, tms, bounds.left.getToken()); + } + + if (transactionalMode.accordIsEnabled) + { + if (!migrationFrom.isMigrating()) + return true; + if (migrationFrom.migratingFromAccord()) + return true; + + // Accord is exclusive once the range is fully migrated to Accord, but possible to read from safely + // when accordSafeToReadRanges covers the entire bound + if (intersects.test(bounds, tms.migratedRanges)) + return true; + } + else + { + // Once the migration starts only barriers are allowed to run for the key in Accord + if (migrationFrom.migratingFromAccord() && !intersects.test(bounds, tms.migratingAndMigratedRanges)) + return true; + } + + return false; + } + + public enum RangeReadTarget + { + accord, + normal + } + + public static class RangeReadWithTarget + { + public final PartitionRangeReadCommand read; + public final RangeReadTarget target; + + private RangeReadWithTarget(PartitionRangeReadCommand read, RangeReadTarget target) + { + this.read = read; + this.target = target; + } + + @Override + public String toString() + { + return "RangeReadWithTarget{" + + "read=" + read + + ", target=" + target + + '}'; + } + } + + /** + * While it's possible to map the Accord read to a single txn it doesn't seem worth it since it's a pretty unusual + * scenario where we do this during migration and have a lot of different read commands. + */ + public static List splitReadIntoAccordAndNormal(ClusterMetadata cm, PartitionRangeReadCommand read, ReadCoordinator readCoordinator, Dispatcher.RequestTime requestTime) + { + if (!readCoordinator.isEventuallyConsistent()) + return ImmutableList.of(new RangeReadWithTarget(read, RangeReadTarget.normal)); + TableMetadata tm = getTableMetadata(cm, read.metadata().id); + if (tm == null || (!tm.params.transactionalMode.nonSerialReadsThroughAccord && !tm.params.transactionalMigrationFrom.nonSerialReadsThroughAccord())) + return ImmutableList.of(new RangeReadWithTarget(read, RangeReadTarget.normal)); + + List result = null; + TransactionalMode transactionalMode = tm.params.transactionalMode; + TransactionalMigrationFromMode transactionalMigrationFromMode = tm.params.transactionalMigrationFrom; + boolean transactionalModeReadsThroughAccord = transactionalMode.nonSerialReadsThroughAccord; + RangeReadTarget migrationToTarget = transactionalModeReadsThroughAccord ? RangeReadTarget.accord : RangeReadTarget.normal; + boolean migrationFromReadsThroughAccord = transactionalMigrationFromMode.nonSerialReadsThroughAccord(); + RangeReadTarget migrationFromTarget = migrationFromReadsThroughAccord ? RangeReadTarget.accord : RangeReadTarget.normal; + 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 ImmutableList.of(new RangeReadWithTarget(read, transactionalModeReadsThroughAccord ? RangeReadTarget.accord : RangeReadTarget.normal)); + 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 ImmutableList.of(new RangeReadWithTarget(read, migrationFromReadsThroughAccord ? RangeReadTarget.accord : RangeReadTarget.normal)); + } + + + // AbstractBounds can potentially be left/right inclusive while Range used to track migration is only right inclusive + // The right way to tackle this seems to be to find the tokens that intersect the key range and then split until + // until nothing intersects + AbstractBounds keyRange = read.dataRange().keyRange(); + AbstractBounds remainder = keyRange; + + // Migrating to Accord we only read through Accord when the range is fully migrated, but migrating back + // we stop reading from Accord as soon as the range is marked migrating and do key migration on read + NormalizedRanges migratedRanges = transactionalModeReadsThroughAccord ? tms.migratedRanges : tms.migratingAndMigratedRanges; + + // Add the preceding range if any + if (!migratedRanges.isEmpty()) + { + Token firstMigratingToken = migratedRanges.get(0).left.getToken(); + int leftCmp = keyRange.left.getToken().compareTo(firstMigratingToken); + int rightCmp = compareRightToken(keyRange.right.getToken(), firstMigratingToken); + if (leftCmp <= 0) + { + if (rightCmp <= 0) + return ImmutableList.of(new RangeReadWithTarget(read, migrationFromTarget)); + result = new ArrayList<>(); + AbstractBounds precedingRange = keyRange.withNewRight(rightCmp <= 0 ? keyRange.right : firstMigratingToken.maxKeyBound()); + // Could be an empty bound, it's fine to let a min KeyBound and max KeyBound through as that isn't empty + if (!precedingRange.left.equals(precedingRange.right)) + result.add(new RangeReadWithTarget(read.forSubRange(precedingRange, false), migrationFromTarget)); + } + } + + boolean hadAccordReads = false; + for (Range r : migratedRanges) + { + Pair, AbstractBounds> intersectionAndRemainder = Range.intersectionAndRemainder(remainder, r); + if (intersectionAndRemainder.left != null) + { + if (result == null) + result = new ArrayList<>(); + PartitionRangeReadCommand subRead = read.forSubRange(intersectionAndRemainder.left, result.isEmpty() ? true : false); + result.add(new RangeReadWithTarget(subRead, migrationToTarget)); + hadAccordReads = true; + } + remainder = intersectionAndRemainder.right; + if (remainder == null) + break; + } + + if (remainder != null) + { + if (result != null) + result.add(new RangeReadWithTarget(read.forSubRange(remainder, true), migrationFromTarget)); + else + return ImmutableList.of(new RangeReadWithTarget(read.forSubRange(remainder, false), migrationFromTarget)); + } + + checkState(result != null && !result.isEmpty(), "Shouldn't have null or empty result"); + checkState(result.get(0).read.dataRange().startKey().equals(read.dataRange().startKey()), "Split reads should encompass entire range"); + checkState(result.get(result.size() - 1).read.dataRange().stopKey().equals(read.dataRange().stopKey()), "Split reads should encompass entire range"); + if (result.size() > 1) + { + for (int i = 0; i < result.size() - 1; i++) + { + checkState(result.get(i).read.dataRange().stopKey().equals(result.get(i + 1).read.dataRange().startKey()), "Split reads should all be adjacent"); + checkState(result.get(i).target != result.get(i + 1).target, "Split reads should be for different targets"); + } + } + + //TODO (later): https://issues.apache.org/jira/browse/CASSANDRA-20211 Range reads could use a barrier + if (hadAccordReads) + { + // do barrier + } + + return result; + } + + /** + * Result of splitting mutations across Accord and non-transactional boundaries + */ + public static class SplitReads + { + @Nullable + public final SinglePartitionReadCommand.Group accordReads; + + @Nullable + public final SinglePartitionReadCommand.Group normalReads; + + private SplitReads(SinglePartitionReadCommand.Group accordReads, SinglePartitionReadCommand.Group normalReads) + { + this.accordReads = accordReads; + this.normalReads = normalReads; + } + } + + public static SplitReads splitReadsIntoAccordAndNormal(ClusterMetadata cm, SinglePartitionReadCommand.Group reads, ReadCoordinator coordinator, Dispatcher.RequestTime requestTime) + { + if (!coordinator.isEventuallyConsistent()) + return new SplitReads(null, reads); + List accordReads = null; + List normalReads = null; + + TableMetadata tm = getTableMetadata(cm, reads.queries.get(0).metadata().id); + if (tm == null || (!tm.params.transactionalMode.nonSerialReadsThroughAccord && !tm.params.transactionalMigrationFrom.nonSerialReadsThroughAccord())) + return new SplitReads(null, reads); + + TransactionalMode transactionalMode = tm.params.transactionalMode; + TransactionalMigrationFromMode transactionalMigrationFromMode = tm.params.transactionalMigrationFrom; + TableMigrationState tms = cm.consensusMigrationState.tableStates.get(tm.id); + + for (SinglePartitionReadCommand command : reads.queries) + { + if (tokenShouldBeReadThroughAccord(tms, command.partitionKey().getToken(), transactionalMode, transactionalMigrationFromMode)) + { + if (accordReads == null) + accordReads = new ArrayList<>(reads.queries.size()); + accordReads.add(command); + } + else + { + if (normalReads == null) + normalReads = new ArrayList<>(reads.queries.size()); + normalReads.add(command); + } + } + + // When migrating from Accord -> Paxos we need to do the Accord barrier to have acknowledged Accord writes + // be visible to non-SERIAL reads, but from Paxos -> Accord we don't need to because read only transactions + // don't have recovery determinism issues and Accord will honor read consistency levels and match the behavior + // of non-serially reading Paxos transactions. Since it's a non-SERIAL read there is no guarantee of seeing + // in-flight Paxos operations, for that you would need to read at SERIAL. + // If the migration direction is from a mode that used to read through Accord then Accord would be + // doing async commit so we need barriers if this mode is no longer reading through Accord. + if (transactionalMigrationFromMode.isMigrating() && transactionalMigrationFromMode.nonSerialReadsThroughAccord() && !transactionalMode.nonSerialReadsThroughAccord && normalReads != null) + { + checkState(!normalReads.isEmpty()); + List keysNeedingBarrier = null; + long maxRequiredEpoch = Long.MIN_VALUE; + for (SinglePartitionReadCommand readCommand : normalReads) + { + DecoratedKey key = readCommand.partitionKey(); + KeyMigrationState kms = ConsensusKeyMigrationState.getKeyMigrationState(cm, tms, key); + if (!kms.paxosReadSatisfiedByKeyMigration()) + { + if (keysNeedingBarrier == null) + keysNeedingBarrier = new ArrayList<>(normalReads.size()); + keysNeedingBarrier.add(key); + maxRequiredEpoch = Math.max(tms.minMigrationEpoch(key.getToken()).getEpoch(), maxRequiredEpoch); + } + } + + if (keysNeedingBarrier != null) + { + checkState(!keysNeedingBarrier.isEmpty()); + checkState(maxRequiredEpoch != Long.MIN_VALUE); + // Local barriers don't support multiple keys so create a global one unless there is a single key + // See BarrierType enum for explanation of global vs local + boolean global = keysNeedingBarrier.size() > 1 ? true : false; + ConsensusKeyMigrationState.repairKeysAccord(keysNeedingBarrier, tm.id, maxRequiredEpoch, requestTime, global, false); + } + } + + SinglePartitionReadCommand.Group accordGroup = accordReads != null ? SinglePartitionReadCommand.Group.create(accordReads, reads.limits()) : null; + SinglePartitionReadCommand.Group normalGroup = normalReads != null ? SinglePartitionReadCommand.Group.create(normalReads, reads.limits()) : null; + return new SplitReads(accordGroup, normalGroup); + } + + private static boolean tokenShouldBeReadThroughAccord(TableMigrationState tms, + @Nonnull Token token, + @Nonnull TransactionalMode transactionalMode, + TransactionalMigrationFromMode transactionalMigrationFromMode) + { + boolean transactionalModeReadsThroughAccord = transactionalMode.nonSerialReadsThroughAccord; + boolean migrationFromReadsThroughAccord = transactionalMigrationFromMode.nonSerialReadsThroughAccord(); + + if (transactionalModeReadsThroughAccord && migrationFromReadsThroughAccord) + return true; + + // Could be migrating or could be completely migrated, if it's migrating check if the key for this mutation + if (transactionalModeReadsThroughAccord || migrationFromReadsThroughAccord) + { + 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 transactionalModeReadsThroughAccord; + 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 migrationFromReadsThroughAccord; + } + + // In theory we can start reading from Accord immediately because we know these transactions are 100% + // read only but then that impacts performance more so wait for the range to be completely migrated + // when it can potentially do single replica reads + if (transactionalModeReadsThroughAccord) + return tms.migratedRanges.intersects(token); + + // 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 (migrationFromReadsThroughAccord) + return !tms.migratingAndMigratedRanges.intersects(token); + } + return false; + } } diff --git a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusTableMigration.java b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusTableMigration.java index e4ba971ffe..00ac2589a3 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/ConsensusTableMigration.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/ConsensusTableMigration.java @@ -52,7 +52,6 @@ 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; import org.apache.cassandra.tcm.ClusterMetadata; @@ -171,31 +170,26 @@ public abstract class ConsensusTableMigration return cm.consensusMigrationState.tableStates.get(tableId); } - public static void startMigrationToConsensusProtocol(@Nonnull String targetProtocolName, - @Nullable List keyspaceNames, + public static void startMigrationToConsensusProtocol(@Nullable List keyspaceNames, @Nonnull Optional> maybeTables, @Nonnull Optional maybeRangesStr) { checkArgument(!maybeTables.isPresent() || !maybeTables.get().isEmpty(), "Must provide at least 1 table if Optional is not empty"); - ConsensusMigrationTarget targetProtocol = ConsensusMigrationTarget.fromString(targetProtocolName); + ClusterMetadata cm = ClusterMetadata.current(); if (keyspaceNames == null || keyspaceNames.isEmpty()) { keyspaceNames = ImmutableList.copyOf(StorageService.instance.getNonLocalStrategyKeyspaces()); } checkState(keyspaceNames.size() == 1 || !maybeTables.isPresent(), "Can't specify tables with multiple keyspaces"); - List ids = keyspacesAndTablesToTableIds(keyspaceNames, maybeTables); + List ids = keyspacesAndTablesToTableIds(cm, keyspaceNames, maybeTables); - // TODO (review): should this perform the schema change to make these tables accord tables? List tableIds = new ArrayList<>(); for (TableId tableId : ids) { - TableMetadata metadata = Schema.instance.getTableMetadata(tableId); + TableMetadata metadata = cm.schema.getTableMetadata(tableId); if (metadata == null || !metadata.params.transactionalMigrationFrom.isMigrating()) continue; - TransactionalMode transactionalMode = metadata.params.transactionalMode; - if (!transactionalMode.nonSerialWritesThroughAccord && transactionalMode != TransactionalMode.unsafe_writes) - throw new IllegalStateException("non-SERIAL writes need to be routed through Accord before attempting migration, or enable mixed mode"); tableIds.add(tableId); } @@ -207,41 +201,41 @@ public abstract class ConsensusTableMigration Token minToken = partitioner.getMinimumToken(); NormalizedRanges ranges = normalizedRanges(maybeParsedRanges.orElse(ImmutableList.of(new Range(minToken, minToken)))); - - ClusterMetadataService.instance().commit(new BeginConsensusMigrationForTableAndRange(targetProtocol, ranges, tableIds)); + ClusterMetadataService.instance().commit(new BeginConsensusMigrationForTableAndRange(ranges, tableIds)); } public static Integer finishMigrationToConsensusProtocol(@Nonnull String keyspace, - @Nonnull Optional> maybeTables, - @Nonnull Optional maybeRangesStr, - ConsensusMigrationTarget target) + @Nonnull Optional> maybeTables, + @Nonnull Optional maybeRangesStr, + @Nonnull ConsensusMigrationTarget target) { checkArgument(!maybeTables.isPresent() || !maybeTables.get().isEmpty(), "Must provide at least 1 table if Optional is not empty"); checkNotNull(target); + ClusterMetadata cm = ClusterMetadata.current(); Optional>> localKeyspaceRanges = Optional.of(ImmutableList.copyOf(StorageService.instance.getLocalReplicas(keyspace).onlyFull().ranges())); List> ranges = maybeRangesToRanges(maybeRangesStr, localKeyspaceRanges); Map allTableMigrationStates = ClusterMetadata.current().consensusMigrationState.tableStates; - List tableIds = keyspacesAndTablesToTableIds(ImmutableList.of(keyspace), maybeTables, Optional.of(allTableMigrationStates::containsKey)); + List tableIds = keyspacesAndTablesToTableIds(cm, ImmutableList.of(keyspace), maybeTables, Optional.of(allTableMigrationStates::containsKey)); checkState(tableIds.stream().allMatch(allTableMigrationStates::containsKey), "All tables need to be migrating"); List tableMigrationStates = new ArrayList<>(); tableIds.forEach(table -> { - ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(table); - if (cfs == null) + TableMetadata tm = cm.schema.getTableMetadata(table); + if (tm == null) { - logger.warn("Table {} does not exist or was dropped", cfs); + logger.warn("Table {} does not exist or was dropped", table); return; } TableMigrationState tms = allTableMigrationStates.get(table); if (tms == null) { - logger.warn("Table {} does not have any migration state", cfs.name); + logger.warn("Table {} does not have any migration state", tm.name); return; } - if(!Range.intersects(ranges, tms.migratingRanges)) + if (!Range.intersects(ranges, tms.migratingRanges)) { - logger.warn("Table {} with migrating ranges {} does not intersect with any requested ranges {}", cfs.name, tms.migratingRanges, ranges); + logger.warn("Table {} with migrating ranges {} does not intersect with any requested ranges {}", tm.name, tms.migratingRanges, ranges); return; } tableMigrationStates.add(tms); @@ -341,12 +335,12 @@ public abstract class ConsensusTableMigration } - private static List keyspacesAndTablesToTableIds(@Nonnull List keyspaceNames, @Nonnull Optional> maybeTables) + private static List keyspacesAndTablesToTableIds(@Nonnull ClusterMetadata cm, @Nonnull List keyspaceNames, @Nonnull Optional> maybeTables) { - return keyspacesAndTablesToTableIds(keyspaceNames, maybeTables, Optional.empty()); + return keyspacesAndTablesToTableIds(cm, keyspaceNames, maybeTables, Optional.empty()); } - private static List keyspacesAndTablesToTableIds(@Nonnull List keyspaceNames, @Nonnull Optional> maybeTables, @Nonnull Optional> includeTable) + private static List keyspacesAndTablesToTableIds(@Nonnull ClusterMetadata cm, @Nonnull List keyspaceNames, @Nonnull Optional> maybeTables, @Nonnull Optional> includeTable) { List tableIds = new ArrayList<>(); for (String keyspaceName : keyspaceNames) @@ -355,7 +349,7 @@ public abstract class ConsensusTableMigration tableNames .stream() .map(tableName -> { - TableMetadata tm = Schema.instance.getTableMetadata(keyspaceName, tableName); + TableMetadata tm = cm.schema.getTableMetadata(keyspaceName, tableName); if (tm == null) throw new IllegalArgumentException(format("Unknown table %s.%s", keyspaceName, tableName)); return tm.id; @@ -363,7 +357,7 @@ public abstract class ConsensusTableMigration .collect(toImmutableList())); tableIds.addAll( maybeTableIds.orElseGet(() -> - Schema.instance.getKeyspaceInstance(keyspaceName).getColumnFamilyStores() + cm.schema.getKeyspace(keyspaceName).getColumnFamilyStores() .stream() .map(ColumnFamilyStore::getTableId) .filter(includeTable.orElse(Predicates.alwaysTrue())) // Filter out non-migrating so they don't generate an error diff --git a/src/java/org/apache/cassandra/service/consensus/migration/TableMigrationState.java b/src/java/org/apache/cassandra/service/consensus/migration/TableMigrationState.java index 636fdcb64e..7fb435b2b2 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/TableMigrationState.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/TableMigrationState.java @@ -37,12 +37,14 @@ import javax.annotation.Nullable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; +import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.NormalizedRanges; import org.apache.cassandra.dht.Range; @@ -58,8 +60,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static org.apache.cassandra.db.TypeSizes.sizeof; -import static org.apache.cassandra.dht.Range.normalize; import static org.apache.cassandra.dht.NormalizedRanges.normalizedRanges; +import static org.apache.cassandra.dht.Range.normalize; import static org.apache.cassandra.dht.Range.subtract; import static org.apache.cassandra.utils.CollectionSerializers.deserializeMap; import static org.apache.cassandra.utils.CollectionSerializers.deserializeSet; @@ -119,11 +121,19 @@ public class TableMigrationState public final NormalizedRanges repairPendingRanges; /** - * Ranges that are migrating could be in either phase when migrating to Accord. PAxos only has one phase. + * Ranges that are migrating could be in either phase when migrating to Accord. Paxos only has one phase. */ @Nonnull public final NormalizedRanges migratingAndMigratedRanges; + /** + * Same as migratingAndMigratedRanges if migrating to Paxos, otherwise migratingAndMigratedRanges.subtract(repairPendingRanges) + * + * Not included in equals or hashCode because it is inferred from other fields + */ + @Nonnull + public final NormalizedRanges accordSafeToReadRanges; + public TableMigrationState(@Nonnull String keyspaceName, @Nonnull String tableName, @Nonnull TableId tableId, @@ -145,6 +155,7 @@ public class TableMigrationState .collect(Collectors.toList())); this.migratingRanges = normalizedRanges(migratingRangesByEpoch.values().stream().flatMap(Collection::stream).collect(Collectors.toList())); this.migratingAndMigratedRanges = normalizedRanges(ImmutableList.>builder().addAll(migratedRanges).addAll(migratingRanges).build()); + this.accordSafeToReadRanges = !repairPendingRanges.isEmpty() ? migratingAndMigratedRanges.subtract(this.repairPendingRanges) : migratingAndMigratedRanges; } static List> initialRepairPendingRanges(ConsensusMigrationTarget target, List> initialMigratingRanges) @@ -415,6 +426,11 @@ public class TableMigrationState } }; + public Iterable> migratedRangesAsPartitionPosition() + { + return Iterables.transform(migratedRanges, range -> new Range<>(range.left.maxKeyBound(), range.right.maxKeyBound())); + } + @Override public String toString() { diff --git a/src/java/org/apache/cassandra/service/consensus/migration/TransactionalMigrationFromMode.java b/src/java/org/apache/cassandra/service/consensus/migration/TransactionalMigrationFromMode.java index 90eeee567a..fe1d9a028c 100644 --- a/src/java/org/apache/cassandra/service/consensus/migration/TransactionalMigrationFromMode.java +++ b/src/java/org/apache/cassandra/service/consensus/migration/TransactionalMigrationFromMode.java @@ -30,10 +30,10 @@ public enum TransactionalMigrationFromMode { none(null), // No migration is in progress. The currently active transaction system could be either Accord or Paxos. off(TransactionalMode.off), - unsafe(TransactionalMode.unsafe), - unsafe_writes(TransactionalMode.unsafe_writes), mixed_reads(TransactionalMode.mixed_reads), full(TransactionalMode.full), + test_unsafe(TransactionalMode.test_unsafe), + test_unsafe_writes(TransactionalMode.test_unsafe_writes), test_interop_read(TransactionalMode.test_interop_read); public final TransactionalMode from; @@ -52,11 +52,11 @@ public enum TransactionalMigrationFromMode { default: throw new IllegalArgumentException(); case off: return off; - case unsafe: return unsafe; - case unsafe_writes: return unsafe_writes; case mixed_reads: return mixed_reads; case full: return full; case test_interop_read: return test_interop_read; + case test_unsafe: return test_unsafe; + case test_unsafe_writes: return test_unsafe_writes; } } diff --git a/src/java/org/apache/cassandra/service/paxos/Commit.java b/src/java/org/apache/cassandra/service/paxos/Commit.java index 12c3606f74..c6a9916f79 100644 --- a/src/java/org/apache/cassandra/service/paxos/Commit.java +++ b/src/java/org/apache/cassandra/service/paxos/Commit.java @@ -29,6 +29,7 @@ import com.google.common.base.Objects; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Mutation; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.db.rows.DeserializationHelper; import org.apache.cassandra.io.IVersionedSerializer; @@ -314,7 +315,7 @@ public class Commit public Mutation makeMutation() { - return new Mutation(update, true); + return new Mutation(update, PotentialTxnConflicts.ALLOW); } @Override diff --git a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java index 1e2d06cfb0..ae76c02d38 100644 --- a/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java +++ b/src/java/org/apache/cassandra/service/reads/AbstractReadExecutor.java @@ -138,7 +138,7 @@ public abstract class AbstractReadExecutor { boolean hasLocalEndpoint = false; Message message = null; - readCommand = coordinator.maybeAllowOutOfRangeReads(readCommand); + readCommand = coordinator.maybeAllowOutOfRangeReads(readCommand, replicaPlan().consistencyLevel()); for (Replica replica: replicas) { diff --git a/src/java/org/apache/cassandra/service/reads/ReadCallback.java b/src/java/org/apache/cassandra/service/reads/ReadCallback.java index e266de0616..3b36332b2f 100644 --- a/src/java/org/apache/cassandra/service/reads/ReadCallback.java +++ b/src/java/org/apache/cassandra/service/reads/ReadCallback.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import com.google.common.collect.ImmutableMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,10 +33,12 @@ import org.apache.cassandra.db.MessageParams; import org.apache.cassandra.db.PartitionRangeReadCommand; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.ReadResponse; +import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.ReadFailureException; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.exceptions.RequestFailure; import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.exceptions.RetryOnDifferentSystemException; import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.ReplicaPlan; @@ -54,6 +57,8 @@ import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater; +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.tracing.Tracing.isTracing; import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; @@ -173,6 +178,34 @@ public class ReadCallback, P extends ReplicaPlan.ForRead< if (snapshot != null) snapshot.maybeAbort(command, replicaPlan().consistencyLevel(), received, replicaPlan().readQuorum(), resolver.isDataPresent(), failureReasonByEndpoint); + // 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 failureReasonByEndpoint = ImmutableMap.copyOf(this.failureReasonByEndpoint); + 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; + + // TODO (nicetohave): This could be smarter and check if retrying would succeed instead of pessimistically + // failing unless all errors are retriable + if (!timedout && totalRetriableFailures > 0 && totalRetriableFailures == failureReasonByEndpoint.size()) + { + // 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(); + if (coordinatorBehindErrors > 0) + throw new CoordinatorBehindException("Read request failed due to coordinator behind"); + } + + // Same as for writes, see AbstractWriteResponseHandler throw !timedout ? new ReadFailureException(replicaPlan().consistencyLevel(), received, replicaPlan().readQuorum(), resolver.isDataPresent(), failureReasonByEndpoint) diff --git a/src/java/org/apache/cassandra/service/reads/ReadCoordinator.java b/src/java/org/apache/cassandra/service/reads/ReadCoordinator.java index 90d11f887f..ba777418d5 100644 --- a/src/java/org/apache/cassandra/service/reads/ReadCoordinator.java +++ b/src/java/org/apache/cassandra/service/reads/ReadCoordinator.java @@ -18,8 +18,10 @@ package org.apache.cassandra.service.reads; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.ReadResponse; import org.apache.cassandra.dht.Token; import org.apache.cassandra.locator.EndpointsForToken; @@ -64,16 +66,16 @@ public interface ReadCoordinator boolean localReadSupported(); EndpointsForToken forNonLocalStrategyTokenRead(ClusterMetadata metadata, KeyspaceMetadata keyspace, TableId tableId, Token token); - default ReadCommand maybeAllowOutOfRangeReads(ReadCommand command) + default ReadCommand maybeAllowOutOfRangeReads(ReadCommand command, ConsistencyLevel cl) { return command; } void sendReadCommand(Message message, InetAddressAndPort to, RequestCallback callback); default void notifyOfInitialContacts(EndpointsForToken fullDataRequests, EndpointsForToken transientRequests, EndpointsForToken digestRequests) {} void sendReadRepairMutation(Message message, InetAddressAndPort to, RequestCallback callback); - default boolean allowsPotentialTransactionConflicts() + default PotentialTxnConflicts potentialTxnConflicts() { - return !isEventuallyConsistent(); + return isEventuallyConsistent() ? PotentialTxnConflicts.DISALLOW : PotentialTxnConflicts.ALLOW; } boolean isEventuallyConsistent(); } diff --git a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java index 2744e8d4be..fae560089b 100644 --- a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java @@ -156,7 +156,7 @@ public class ReplicaFilteringProtection> ReadCallback handler = new ReadCallback<>(resolver, cmd, replicaPlan, requestTime); - if (source.isSelf()) + if (source.isSelf() && coordinator.localReadSupported()) { Stage.READ.maybeExecuteImmediately(new StorageProxy.LocalReadRunnable(cmd, handler, requestTime)); } @@ -164,6 +164,7 @@ public class ReplicaFilteringProtection> { if (source.isTransient()) cmd = cmd.copyAsTransientQuery(source); + cmd = coordinator.maybeAllowOutOfRangeReads(cmd, consistency); MessagingService.instance().sendWithCallback(cmd.createMessage(false, requestTime), source.endpoint(), handler); } diff --git a/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java b/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java index 102042758e..d1562b1b4d 100644 --- a/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ShortReadPartitionsProtection.java @@ -182,7 +182,7 @@ public class ShortReadPartitionsProtection extends Transformation, P extends ReplicaPlan.ForRead> UnfilteredPartitionIterator executeReadCommand(ReadCommand cmd, ReplicaPlan.Shared replicaPlan) { - cmd = coordinator.maybeAllowOutOfRangeReads(cmd); + cmd = coordinator.maybeAllowOutOfRangeReads(cmd, replicaPlan.get().consistencyLevel()); DataResolver resolver = new DataResolver<>(coordinator, cmd, replicaPlan, (NoopReadRepair)NoopReadRepair.instance, requestTime); ReadCallback handler = new ReadCallback<>(resolver, cmd, replicaPlan, requestTime); diff --git a/src/java/org/apache/cassandra/service/reads/range/AccordRangeResponse.java b/src/java/org/apache/cassandra/service/reads/range/AccordRangeResponse.java index 75dfb4b3b2..91a4ffc2c5 100644 --- a/src/java/org/apache/cassandra/service/reads/range/AccordRangeResponse.java +++ b/src/java/org/apache/cassandra/service/reads/range/AccordRangeResponse.java @@ -23,15 +23,14 @@ import java.util.function.IntPredicate; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.RetryOnDifferentSystemException; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageProxy.ConsensusAttemptResult; import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult; import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.AbstractIterator; -import static com.google.common.base.Preconditions.checkState; - -public class AccordRangeResponse extends AbstractIterator implements IRangeResponse +public class AccordRangeResponse extends AbstractIterator implements PartitionIterator { private final AsyncTxnResult asyncTxnResult; // Range queries don't support reverse, but dutifully threading it through anyways @@ -55,8 +54,9 @@ public class AccordRangeResponse extends AbstractIterator implement IntPredicate alwaysTrue = ignored -> true; IntPredicate alwaysFalse = ignored -> false; // TODO (required): Handle retry on different system - ConsensusAttemptResult consensusAttemptResult = StorageProxy.getConsensusAttemptResultFromAsyncTxnResult(asyncTxnResult, 1, reversed ? alwaysTrue : alwaysFalse, cl, requestTime); - checkState(!consensusAttemptResult.shouldRetryOnNewConsensusProtocol, "Live migration is not supported yet"); + ConsensusAttemptResult consensusAttemptResult = StorageProxy.getConsensusAttemptResultFromAsyncTxnResult(asyncTxnResult, 1, reversed ? alwaysTrue : alwaysFalse); + if (consensusAttemptResult.shouldRetryOnNewConsensusProtocol) + throw new RetryOnDifferentSystemException(); result = consensusAttemptResult.serialReadResult; } diff --git a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java index 6eb3ee47b4..11d9539777 100644 --- a/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/RangeCommandIterator.java @@ -21,37 +21,40 @@ package org.apache.cassandra.service.reads.range; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.concurrent.Stage; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.PartitionRangeReadCommand; import org.apache.cassandra.db.ReadCommand; import org.apache.cassandra.db.filter.DataLimits; import org.apache.cassandra.db.partitions.PartitionIterator; +import org.apache.cassandra.db.partitions.PartitionIterators; import org.apache.cassandra.db.rows.RowIterator; +import org.apache.cassandra.exceptions.CoordinatorBehindException; import org.apache.cassandra.exceptions.ReadAbortException; import org.apache.cassandra.exceptions.ReadFailureException; import org.apache.cassandra.exceptions.ReadTimeoutException; +import org.apache.cassandra.exceptions.RetryOnDifferentSystemException; import org.apache.cassandra.exceptions.UnavailableException; import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.metrics.ClientRangeRequestMetrics; import org.apache.cassandra.net.Message; -import org.apache.cassandra.net.MessagingService; -import org.apache.cassandra.schema.TableMetadata; -import org.apache.cassandra.schema.TableParams; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.accord.IAccordService.AsyncTxnResult; -import org.apache.cassandra.service.consensus.TransactionalMode; -import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode; +import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; +import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.RangeReadTarget; +import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter.RangeReadWithTarget; import org.apache.cassandra.service.reads.DataResolver; import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.service.reads.ReadCoordinator; @@ -62,6 +65,9 @@ import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.CloseableIterator; +import static com.google.common.base.Preconditions.checkState; +import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetrics; +import static org.apache.cassandra.metrics.ClientRequestsMetricsHolder.readMetricsForLevel; import static org.apache.cassandra.utils.Clock.Global.nanoTime; @VisibleForTesting @@ -187,6 +193,47 @@ public class RangeCommandIterator extends AbstractIterator implemen return concurrencyFactor; } + private PartitionIterator executeAccord(ClusterMetadata cm, PartitionRangeReadCommand rangeCommand, ConsistencyLevel cl) + { + //TODO (nicetohave): https://issues.apache.org/jira/browse/CASSANDRA-20210 More efficient reads across command stores + AsyncTxnResult result = StorageProxy.readWithAccord(cm, rangeCommand, rangeCommand.dataRange().keyRange(), cl, requestTime); + return new AccordRangeResponse(result, rangeCommand.isReversed(), cl, requestTime); + } + + private SingleRangeResponse executeNormal(ReplicaPlan.ForRangeRead replicaPlan, PartitionRangeReadCommand rangeCommand, ReadCoordinator readCoordinator) + { + rangeCommand = (PartitionRangeReadCommand) readCoordinator.maybeAllowOutOfRangeReads(rangeCommand, replicaPlan.consistencyLevel()); + // If enabled, request repaired data tracking info from full replicas, but + // only if there are multiple full replicas to compare results from. + boolean trackRepairedStatus = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled() + && replicaPlan.contacts().filter(Replica::isFull).size() > 1; + + ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); + ReadRepair readRepair = + ReadRepair.create(readCoordinator, command, sharedReplicaPlan, requestTime); + DataResolver resolver = + new DataResolver<>(readCoordinator, rangeCommand, sharedReplicaPlan, readRepair, requestTime, trackRepairedStatus); + ReadCallback handler = + new ReadCallback<>(resolver, rangeCommand, sharedReplicaPlan, requestTime); + + if (replicaPlan.contacts().size() == 1 && replicaPlan.contacts().get(0).isSelf() && readCoordinator.localReadSupported()) + { + Stage.READ.execute(new StorageProxy.LocalReadRunnable(rangeCommand, handler, requestTime, trackRepairedStatus)); + } + else + { + for (Replica replica : replicaPlan.contacts()) + { + Tracing.trace("Enqueuing request to {}", replica); + ReadCommand command = replica.isFull() ? rangeCommand : rangeCommand.copyAsTransientQuery(replica); + Message message = command.createMessage(trackRepairedStatus && replica.isFull(), requestTime); + readCoordinator.sendReadCommand(message, replica.endpoint(), handler); + } + } + return new SingleRangeResponse(resolver, handler, readRepair); + } + + /** * Queries the provided sub-range. * @@ -196,56 +243,88 @@ public class RangeCommandIterator extends AbstractIterator implemen * {@code DataLimits}) may have "state" information and that state may only be valid for the first query (in * that it's the query that "continues" whatever we're previously queried). */ - private IRangeResponse query(ReplicaPlan.ForRangeRead replicaPlan, ReadCoordinator readCoordinator, boolean isFirst) + private PartitionIterator query(ClusterMetadata cm, ReplicaPlan.ForRangeRead replicaPlan, ReadCoordinator readCoordinator, List> readRepairs, boolean isFirst) { PartitionRangeReadCommand rangeCommand = command.forSubRange(replicaPlan.range(), isFirst); - - // If enabled, request repaired data tracking info from full replicas, but - // only if there are multiple full replicas to compare results from. - boolean trackRepairedStatus = DatabaseDescriptor.getRepairedDataTrackingForRangeReadsEnabled() - && replicaPlan.contacts().filter(Replica::isFull).size() > 1; - ClusterMetadata cm = ClusterMetadata.current(); - TableMetadata metadata = command.metadata(); - TableParams tableParams = metadata.params; - TransactionalMode transactionalMode = tableParams.transactionalMode; - TransactionalMigrationFromMode transactionalMigrationFromMode = tableParams.transactionalMigrationFrom; - if (transactionalMigrationFromMode != TransactionalMigrationFromMode.none && transactionalMode.nonSerialReadsThroughAccord && transactionalMigrationFromMode.nonSerialWritesThroughAccord() && transactionalMigrationFromMode.nonSerialReadsThroughAccord()) - throw new UnsupportedOperationException("Live migration is not supported, can't safely read when migrating from " + transactionalMigrationFromMode + " to " + transactionalMode); - if (transactionalMode.nonSerialReadsThroughAccord && readCoordinator.isEventuallyConsistent()) + // Accord interop execution should always be coordinated through the C* plumbing + if (!readCoordinator.isEventuallyConsistent()) { - //TODO (nicetohave): This is very inefficient because it will not map the the command store owned ranges - // so every command store will return results and most will be discarded due to the limit - // Really we want to split the ranges by command stores owned ranges and then query one at a time - AsyncTxnResult result = StorageProxy.readWithAccord(cm, rangeCommand, ImmutableList.of(rangeCommand.dataRange().keyRange()), replicaPlan.consistencyLevel(), requestTime); - return new AccordRangeResponse(result, rangeCommand.isReversed(), replicaPlan.consistencyLevel(), requestTime); + SingleRangeResponse response = executeNormal(replicaPlan, rangeCommand, readCoordinator); + readRepairs.add(response.getReadRepair()); + return response; } - else - { - ReplicaPlan.SharedForRangeRead sharedReplicaPlan = ReplicaPlan.shared(replicaPlan); - ReadRepair readRepair = - ReadRepair.create(ReadCoordinator.DEFAULT, command, sharedReplicaPlan, requestTime); - DataResolver resolver = - new DataResolver<>(ReadCoordinator.DEFAULT, rangeCommand, sharedReplicaPlan, readRepair, requestTime, trackRepairedStatus); - ReadCallback handler = - new ReadCallback<>(resolver, rangeCommand, sharedReplicaPlan, requestTime); - if (replicaPlan.contacts().size() == 1 && replicaPlan.contacts().get(0).isSelf()) + List reads = ConsensusRequestRouter.splitReadIntoAccordAndNormal(cm, rangeCommand, readCoordinator, requestTime); + // Special case returning directly to avoid wrapping the iterator and applying the limits an extra time + if (reads.size() == 1) + { + RangeReadWithTarget rangeReadWithTarget = reads.get(0); + checkState(rangeReadWithTarget.read.dataRange().keyRange().equals(rangeCommand.dataRange().keyRange())); + if (rangeReadWithTarget.target == RangeReadTarget.accord && readCoordinator.isEventuallyConsistent()) { - Stage.READ.execute(new StorageProxy.LocalReadRunnable(rangeCommand, handler, requestTime, trackRepairedStatus)); + return executeAccord(cm, + rangeReadWithTarget.read, + replicaPlan.consistencyLevel()); } else { - for (Replica replica : replicaPlan.contacts()) - { - Tracing.trace("Enqueuing request to {}", replica); - ReadCommand command = replica.isFull() ? rangeCommand : rangeCommand.copyAsTransientQuery(replica); - Message message = command.createMessage(trackRepairedStatus && replica.isFull(), requestTime); - MessagingService.instance().sendWithCallback(message, replica.endpoint(), handler); - } + SingleRangeResponse response = executeNormal(replicaPlan, rangeReadWithTarget.read, readCoordinator); + readRepairs.add(response.getReadRepair()); + return response; } - return new CassandraRangeResponse(resolver, handler, readRepair); } + + // TODO (review): Should this be reworked to execute the queries serially from the iterator? It would respect + // any provided limits better but the number of queries created will generally be low (2-3) + List responses = new ArrayList<>(reads.size() + 1); + // Dummy iterator that checks all the responses for retry on different system hasNext so we don't read + // from the first iterator when the second needs to be retried because the split was wrong + responses.add(new PartitionIterator() + { + @Override + public void close() + { + + } + + @Override + public boolean hasNext() + { + for (int i = 1; i < responses.size(); i++) + responses.get(i).hasNext(); + return false; + } + + @Override + public RowIterator next() + { + throw new NoSuchElementException(); + } + }); + + for (RangeReadWithTarget rangeReadWithTarget : reads) + { + if (rangeReadWithTarget.target == RangeReadTarget.accord && readCoordinator.isEventuallyConsistent()) + responses.add(executeAccord(cm, rangeReadWithTarget.read, replicaPlan.consistencyLevel())); + else + { + SingleRangeResponse response = executeNormal(replicaPlan, rangeReadWithTarget.read, readCoordinator); + responses.add(response); + readRepairs.add(response.getReadRepair()); + } + } + + /* + * We have to apply limits here if the query spans different systems because each subquery we created + * could have gaps in the results since the limit is pushed down independently to each subquery. + * So if we don't meet the limit in the first subquery, it's not safe to go to the next one unless + * we fully exhausted the data the first subquery might have reached + */ + return command.limits().filter(PartitionIterators.concat(responses), + 0, + command.selectsFullPartition(), + command.metadata().enforceStrictLiveness()); } PartitionIterator sendNextRequests() @@ -253,15 +332,26 @@ public class RangeCommandIterator extends AbstractIterator implemen List concurrentQueries = new ArrayList<>(concurrencyFactor); List> readRepairs = new ArrayList<>(concurrencyFactor); + ClusterMetadata cm = ClusterMetadata.current(); try { for (int i = 0; i < concurrencyFactor && replicaPlans.hasNext(); ) { ReplicaPlan.ForRangeRead replicaPlan = replicaPlans.next(); - - IRangeResponse response = query(replicaPlan, readCoordinator, i == 0); + boolean isFirst = i == 0; + PartitionIterator response; + // Only add the retry wrapper to reroute for the top level coordinator execution + // not Accord's interop execution + if (readCoordinator.isEventuallyConsistent()) + { + Function querySupplier = clusterMetadata -> query(clusterMetadata, replicaPlan, readCoordinator, readRepairs, isFirst); + response = retryingPartitionIterator(querySupplier, replicaPlan.consistencyLevel()); + } + else + { + response = query(cm, replicaPlan, readCoordinator, readRepairs, isFirst); + } concurrentQueries.add(response); - readRepairs.add(response.getReadRepair()); // due to RangeMerger, coordinator may fetch more ranges than required by concurrency factor. rangesQueried += replicaPlan.vnodeCount(); i += replicaPlan.vnodeCount(); @@ -283,6 +373,57 @@ public class RangeCommandIterator extends AbstractIterator implemen return counter.applyTo(StorageProxy.concatAndBlockOnRepair(concurrentQueries, readRepairs)); } + // Wrap the iterator to retry if request routing is incorrect + private PartitionIterator retryingPartitionIterator(Function attempt, ConsistencyLevel cl) + { + return new PartitionIterator() + { + private ClusterMetadata lastClusterMetadata = ClusterMetadata.current(); + private PartitionIterator delegate = attempt.apply(lastClusterMetadata); + + @Override + public void close() + { + delegate.close(); + } + + @Override + public boolean hasNext() + { + while (true) + { + try + { + return delegate.hasNext(); + } + catch (RetryOnDifferentSystemException e) + { + readMetrics.retryDifferentSystem.mark(); + readMetricsForLevel(cl).retryDifferentSystem.mark(); + logger.debug("Retrying range read on different system because some reads were misrouted according to Accord"); + Tracing.trace("Got {} from range reads, will retry", e); + } + catch (CoordinatorBehindException e) + { + readMetrics.retryCoordinatorBehind.mark(); + readMetricsForLevel(cl).retryCoordinatorBehind.mark(); + logger.debug("Retrying range read now that coordinator has caught up to cluster metadata"); + Tracing.trace("Got {} from range reads, will retry", e); + } + // Fetch the next epoch to retry + lastClusterMetadata = ClusterMetadata.current(); + delegate = attempt.apply(lastClusterMetadata); + } + } + + @Override + public RowIterator next() + { + return delegate.next(); + } + }; + } + @Override public void close() { diff --git a/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java b/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java index e2a877048f..8ebc6a6697 100644 --- a/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java +++ b/src/java/org/apache/cassandra/service/reads/range/ScanAllRangesCommandIterator.java @@ -112,6 +112,6 @@ public class ScanAllRangesCommandIterator extends RangeCommandIterator Tracing.trace("Submitted scanning all ranges requests to {} nodes", nodes); // skip read-repair for top-k query because data mismatch may be caused by top-k algorithm instead of actual inconsistency. - return new CassandraRangeResponse(resolver, handler, NoopReadRepair.instance); + return new SingleRangeResponse(resolver, handler, NoopReadRepair.instance); } } diff --git a/src/java/org/apache/cassandra/service/reads/range/CassandraRangeResponse.java b/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java similarity index 81% rename from src/java/org/apache/cassandra/service/reads/range/CassandraRangeResponse.java rename to src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java index 1d383b8017..0274058030 100644 --- a/src/java/org/apache/cassandra/service/reads/range/CassandraRangeResponse.java +++ b/src/java/org/apache/cassandra/service/reads/range/SingleRangeResponse.java @@ -23,12 +23,13 @@ import org.apache.cassandra.db.rows.RowIterator; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.locator.EndpointsForRange; import org.apache.cassandra.locator.ReplicaPlan; +import org.apache.cassandra.locator.ReplicaPlan.ForRangeRead; import org.apache.cassandra.service.reads.DataResolver; import org.apache.cassandra.service.reads.ReadCallback; import org.apache.cassandra.service.reads.repair.ReadRepair; import org.apache.cassandra.utils.AbstractIterator; -class CassandraRangeResponse extends AbstractIterator implements IRangeResponse +class SingleRangeResponse extends AbstractIterator implements PartitionIterator { private final DataResolver resolver; private final ReadCallback handler; @@ -36,17 +37,16 @@ class CassandraRangeResponse extends AbstractIterator implements IR private PartitionIterator result; - CassandraRangeResponse(DataResolver resolver, - ReadCallback handler, - ReadRepair readRepair) + SingleRangeResponse(DataResolver resolver, + ReadCallback handler, + ReadRepair readRepair) { this.resolver = resolver; this.handler = handler; this.readRepair = readRepair; } - @Override - public ReadRepair getReadRepair() + public ReadRepair getReadRepair() { return readRepair; } diff --git a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java index 7ef6d2fe04..418deb4aa2 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/AbstractReadRepair.java @@ -94,7 +94,7 @@ public abstract class AbstractReadRepair, P extends Repli void sendReadCommand(Replica to, ReadCallback readCallback, boolean speculative, boolean trackRepairedStatus) { - ReadCommand command = coordinator.maybeAllowOutOfRangeReads(this.command); + ReadCommand command = coordinator.maybeAllowOutOfRangeReads(this.command, replicaPlan().consistencyLevel()); if (to.isSelf() && coordinator.localReadSupported()) { diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java index 309bbeeb51..c3c61181c9 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingPartitionRepair.java @@ -53,10 +53,10 @@ import org.apache.cassandra.utils.concurrent.CountDownLatch; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.all; 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; -import static com.google.common.collect.Iterables.all; public class BlockingPartitionRepair extends AsyncFuture implements RequestCallback, PendingPartitionRepair @@ -155,7 +155,7 @@ public class BlockingPartitionRepair @VisibleForTesting protected void sendRR(Message message, InetAddressAndPort endpoint) { - checkArgument(message.payload.allowsPotentialTransactionConflicts() == coordinator.allowsPotentialTransactionConflicts(), "Mutation allowing transaction conflicts should match coordinator"); + checkArgument(message.payload.potentialTxnConflicts() == coordinator.potentialTxnConflicts(), "Mutation allowing transaction conflicts should match coordinator"); coordinator.sendReadRepairMutation(message, endpoint, this); } @@ -241,7 +241,7 @@ public class BlockingPartitionRepair if (mutation == null) { - mutation = BlockingReadRepairs.createRepairMutation(update, repairPlan.consistencyLevel(), replica.endpoint(), true, coordinator.allowsPotentialTransactionConflicts()); + mutation = BlockingReadRepairs.createRepairMutation(update, repairPlan.consistencyLevel(), replica.endpoint(), true, coordinator.potentialTxnConflicts()); versionedMutations[versionIdx] = mutation; } diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java index d01583a3ca..9e82583aaf 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepair.java @@ -38,6 +38,7 @@ 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.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.locator.Endpoints; @@ -47,8 +48,8 @@ import org.apache.cassandra.locator.ReplicaPlan.ForWrite; import org.apache.cassandra.metrics.ReadRepairMetrics; import org.apache.cassandra.service.accord.AccordService; import org.apache.cassandra.service.accord.api.PartitionKey; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; 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.TransactionalMode; @@ -241,7 +242,7 @@ public class BlockingReadRepair, P extends ReplicaPlan.Fo * since overlapping non-transactional writes with transactional reads will never be deterministic, but it combines * the two things into the same mechanism and we can't tell the origin of the writes needing read repair anyways. */ - Txn txn = new Txn.InMemory(Txn.Kind.Read, key, TxnKeyRead.createNoOpRead(key), TxnQuery.NONE, repairUpdate); + Txn txn = new Txn.InMemory(Txn.Kind.Read, key, TxnRead.createNoOpRead(key), TxnQuery.NONE, repairUpdate); Future repairFuture = Stage.ACCORD_MIGRATION.submit(() -> AccordService.instance().coordinate(command.metadata().epoch.getEpoch(), txn, ConsistencyLevel.ANY, requestTime)); repairs.add(new PendingPartitionRepair() @@ -294,8 +295,8 @@ public class BlockingReadRepair, P extends ReplicaPlan.Fo } @Override - public boolean coordinatorAllowsPotentialTransactionConflicts() + public PotentialTxnConflicts coordinatorPotentialTxnConflicts() { - return coordinator.allowsPotentialTransactionConflicts(); + return coordinator.potentialTxnConflicts(); } } diff --git a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java index c16fba3674..3986a42bf4 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java +++ b/src/java/org/apache/cassandra/service/reads/repair/BlockingReadRepairs.java @@ -26,6 +26,7 @@ import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.MutationExceededMaxSizeException; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionUpdate; import org.apache.cassandra.exceptions.ReadTimeoutException; import org.apache.cassandra.locator.InetAddressAndPort; @@ -46,13 +47,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, boolean allowPotentialTransactionConflicts) + public static Mutation createRepairMutation(PartitionUpdate update, ConsistencyLevel consistency, InetAddressAndPort destination, boolean suppressException, PotentialTxnConflicts potentialTxnConflicts) { if (update == null) return null; DecoratedKey key = update.partitionKey(); - Mutation mutation = new Mutation(update, allowPotentialTransactionConflicts); + Mutation mutation = new Mutation(update, potentialTxnConflicts); int messagingVersion = MessagingService.instance().versions.get(destination); try diff --git a/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java b/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java index 66703bb56e..b21765203e 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java +++ b/src/java/org/apache/cassandra/service/reads/repair/ReadRepair.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadCommand.PotentialTxnConflicts; import org.apache.cassandra.db.partitions.PartitionIterator; import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators; import org.apache.cassandra.exceptions.ReadTimeoutException; @@ -31,8 +32,8 @@ import org.apache.cassandra.locator.Endpoints; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.service.reads.DigestResolver; -import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.service.reads.ReadCoordinator; +import org.apache.cassandra.transport.Dispatcher; public interface ReadRepair, P extends ReplicaPlan.ForRead> { @@ -110,8 +111,8 @@ public interface ReadRepair, P extends ReplicaPlan.ForRea */ void repairPartitionDirectly(ReadCoordinator coordinator, DecoratedKey partitionKey, Map mutations, ReplicaPlan.ForWrite writePlan); - default boolean coordinatorAllowsPotentialTransactionConflicts() + default PotentialTxnConflicts coordinatorPotentialTxnConflicts() { - return false; + return PotentialTxnConflicts.DISALLOW; } } diff --git a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java index f279a290d0..47bf0efeb1 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java +++ b/src/java/org/apache/cassandra/service/reads/repair/RowIteratorMergeListener.java @@ -391,7 +391,7 @@ public class RowIteratorMergeListener> else if (repairs[i] != null) update = repairs[i].build(); - Mutation mutation = BlockingReadRepairs.createRepairMutation(update, readPlan.consistencyLevel(), replica.endpoint(), false, readRepair.coordinatorAllowsPotentialTransactionConflicts()); + Mutation mutation = BlockingReadRepairs.createRepairMutation(update, readPlan.consistencyLevel(), replica.endpoint(), false, readRepair.coordinatorPotentialTxnConflicts()); if (mutation == null) continue; diff --git a/src/java/org/apache/cassandra/tcm/log/LogState.java b/src/java/org/apache/cassandra/tcm/log/LogState.java index 96514a740c..7e050e0805 100644 --- a/src/java/org/apache/cassandra/tcm/log/LogState.java +++ b/src/java/org/apache/cassandra/tcm/log/LogState.java @@ -23,7 +23,6 @@ import java.util.Objects; import java.util.Optional; import com.google.common.collect.ImmutableList; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/java/org/apache/cassandra/tcm/transformations/BeginConsensusMigrationForTableAndRange.java b/src/java/org/apache/cassandra/tcm/transformations/BeginConsensusMigrationForTableAndRange.java index c266f41901..869a5b5189 100644 --- a/src/java/org/apache/cassandra/tcm/transformations/BeginConsensusMigrationForTableAndRange.java +++ b/src/java/org/apache/cassandra/tcm/transformations/BeginConsensusMigrationForTableAndRange.java @@ -24,7 +24,6 @@ import java.util.List; import java.util.stream.Collectors; import javax.annotation.Nonnull; -import org.apache.cassandra.db.TypeSizes; import org.apache.cassandra.dht.NormalizedRanges; import org.apache.cassandra.dht.Token; import org.apache.cassandra.io.util.DataInputPlus; @@ -32,7 +31,6 @@ import org.apache.cassandra.io.util.DataOutputPlus; import org.apache.cassandra.schema.TableId; import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.consensus.migration.ConsensusMigrationState; -import org.apache.cassandra.service.consensus.migration.ConsensusMigrationTarget; import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Transformation; @@ -51,25 +49,19 @@ public class BeginConsensusMigrationForTableAndRange implements Transformation { public static Serializer serializer = new Serializer(); - @Nonnull - public final ConsensusMigrationTarget targetProtocol; - @Nonnull public final NormalizedRanges ranges; @Nonnull public final List tables; - public BeginConsensusMigrationForTableAndRange(@Nonnull ConsensusMigrationTarget targetProtocol, - @Nonnull NormalizedRanges ranges, + public BeginConsensusMigrationForTableAndRange(@Nonnull NormalizedRanges ranges, @Nonnull List tables) { - checkNotNull(targetProtocol, "targetProtocol should not be null"); checkNotNull(ranges, "ranges should not be null"); checkArgument(!ranges.isEmpty(), "ranges should not be empty"); checkNotNull(tables, "tables should not be null"); checkArgument(!tables.isEmpty(), "tables should not be empty"); - this.targetProtocol = targetProtocol; this.ranges = ranges; this.tables = tables; } @@ -93,24 +85,21 @@ public class BeginConsensusMigrationForTableAndRange implements Transformation public void serialize(Transformation t, DataOutputPlus out, Version version) throws IOException { BeginConsensusMigrationForTableAndRange v = (BeginConsensusMigrationForTableAndRange)t; - out.writeUTF(v.targetProtocol.toString()); ConsensusTableMigration.rangesSerializer.serialize(v.ranges, out, version); serializeCollection(v.tables, out, version, TableId.metadataSerializer); } public BeginConsensusMigrationForTableAndRange deserialize(DataInputPlus in, Version version) throws IOException { - ConsensusMigrationTarget targetProtocol = ConsensusMigrationTarget.fromString(in.readUTF()); NormalizedRanges ranges = ConsensusTableMigration.rangesSerializer.deserialize(in, version); List tables = deserializeList(in, version, TableId.metadataSerializer); - return new BeginConsensusMigrationForTableAndRange(targetProtocol, ranges, tables); + return new BeginConsensusMigrationForTableAndRange(ranges, tables); } public long serializedSize(Transformation t, Version version) { BeginConsensusMigrationForTableAndRange v = (BeginConsensusMigrationForTableAndRange) t; - return TypeSizes.sizeof(v.targetProtocol.toString()) - + ConsensusTableMigration.rangesSerializer.serializedSize(v.ranges, version) + return ConsensusTableMigration.rangesSerializer.serializedSize(v.ranges, version) + serializedCollectionSize(v.tables, version, TableId.metadataSerializer); } } diff --git a/src/java/org/apache/cassandra/tools/nodetool/ConsensusMigrationAdmin.java b/src/java/org/apache/cassandra/tools/nodetool/ConsensusMigrationAdmin.java index 124a45610c..6553ddf9fd 100644 --- a/src/java/org/apache/cassandra/tools/nodetool/ConsensusMigrationAdmin.java +++ b/src/java/org/apache/cassandra/tools/nodetool/ConsensusMigrationAdmin.java @@ -69,9 +69,6 @@ public abstract class ConsensusMigrationAdmin extends NodeTool.NodeToolCmd @Option(title = "end_token", name = {"-et", "--end-token"}, description = "Use -et to specify a token at which repair range ends") private String endToken = null; - @Option(title = "target_protocol", name = {"-tp", "--target-protocol"}, description = "Use -tp to specify what consensus protocol should be migrated to", required=true) - private String targetProtocol = null; - @Arguments(usage = "[ ...]", description = "The keyspace followed by one or many tables") private List schemaArgs = new ArrayList<>(); @@ -81,7 +78,7 @@ public abstract class ConsensusMigrationAdmin extends NodeTool.NodeToolCmd String maybeRangesStr = startToken != null ? startToken + ":" + endToken : null; List keyspaceNames = parseOptionalKeyspace(schemaArgs, probe, KeyspaceSet.ACCORD_MANAGED); List maybeTableNames = schemaArgs.size() > 1 ? schemaArgs.subList(1, schemaArgs.size()) : null; - probe.getStorageService().migrateConsensusProtocol(targetProtocol, keyspaceNames, maybeTableNames, maybeRangesStr); + probe.getStorageService().migrateConsensusProtocol(keyspaceNames, maybeTableNames, maybeRangesStr); probe.output().out.println("Marked requested ranges as migrating. Repair needs to be run in order to complete the migration"); } } diff --git a/src/java/org/apache/cassandra/utils/Collectors3.java b/src/java/org/apache/cassandra/utils/Collectors3.java index c48f16062a..3d884fe9df 100644 --- a/src/java/org/apache/cassandra/utils/Collectors3.java +++ b/src/java/org/apache/cassandra/utils/Collectors3.java @@ -19,6 +19,8 @@ package org.apache.cassandra.utils; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Function; import java.util.stream.Collector; @@ -46,6 +48,8 @@ public class Collectors3 private static final Collector.Characteristics[] SET_CHARACTERISTICS = new Collector.Characteristics[]{ Collector.Characteristics.UNORDERED }; + private static final Collector.Characteristics[] SORTED_SET_CHARACTERISTICS = new Collector.Characteristics[]{ Collector.Characteristics.UNORDERED }; + public static Collector> toImmutableSet() { return Collector.of(ImmutableSet.Builder::new, @@ -75,4 +79,15 @@ public class Collectors3 MAP_CHARACTERISTICS); } + public static Collector, SortedSet> toSortedSet() + { + return Collector.of(() -> new TreeSet(), + (set, value) -> set.add(value), + (set1, set2) -> { + set1.addAll(set2); + return set1; + }, + set -> set, + SORTED_SET_CHARACTERISTICS); + } } diff --git a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java index 225b446d76..8134931a34 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/InstanceConfig.java @@ -30,8 +30,8 @@ import java.util.UUID; import java.util.function.Function; import com.vdurmont.semver4j.Semver; -import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.AccordSpec; +import org.apache.cassandra.config.CassandraRelevantProperties; import org.apache.cassandra.config.OptionaldPositiveInt; import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IInstanceConfig; diff --git a/test/distributed/org/apache/cassandra/distributed/impl/MessageImpl.java b/test/distributed/org/apache/cassandra/distributed/impl/MessageImpl.java index 937670b612..fc1dbcf0de 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/MessageImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/MessageImpl.java @@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.impl; import java.net.InetSocketAddress; import org.apache.cassandra.distributed.api.IMessage; +import org.apache.cassandra.net.Verb; import org.apache.cassandra.utils.ByteArrayUtil; // a container for simplifying the method signature for per-instance message handling/delivery @@ -78,7 +79,7 @@ public class MessageImpl implements IMessage public String toString() { return "MessageImpl{" + - "verb=" + verb + + "verb=" + Verb.fromId(verb) + ", bytes=" + ByteArrayUtil.bytesToHex(bytes) + ", id=" + id + ", version=" + version + diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java index 1279689fd9..654196a597 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java @@ -488,6 +488,11 @@ public class ClusterUtils instance.runOnInstance(() -> ClusterMetadataService.instance().log().clearFilters()); } + public static Callable pauseBeforeEnacting(IInvokableInstance instance, long epoch) + { + return pauseBeforeEnacting(instance, Epoch.create(epoch), 10, TimeUnit.SECONDS); + } + public static Callable pauseBeforeEnacting(IInvokableInstance instance, Epoch epoch) { return pauseBeforeEnacting(instance, epoch, 10, TimeUnit.SECONDS); @@ -501,7 +506,10 @@ public class ClusterUtils return instance.callOnInstance(() -> { TestChangeListener listener = TestChangeListener.instance; AsyncPromise promise = new AsyncPromise<>(); - listener.pauseBefore(epoch, () -> promise.setSuccess(null)); + listener.pauseBefore(epoch, () -> { + logger.info("Notifying waiter of pausing for pauseBeforeEnacting epoch {}", epoch); + promise.setSuccess(null); + }); return () -> { try { @@ -532,7 +540,10 @@ public class ClusterUtils return instance.callOnInstance(() -> { TestChangeListener listener = TestChangeListener.instance; AsyncPromise promise = new AsyncPromise<>(); - listener.pauseAfter(epoch, () -> promise.setSuccess(null)); + listener.pauseAfter(epoch, () -> { + logger.info("Notifying waiter of pausing for pauseAfterEnacting epoch {}", epoch); + promise.setSuccess(null); + }); return () -> { try { diff --git a/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java index 0a4c575b5b..65a1ef8237 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/QueriesTableTest.java @@ -18,20 +18,23 @@ package org.apache.cassandra.distributed.test; import java.io.IOException; +import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; -import accord.impl.progresslog.DefaultProgressLogs; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.implementation.bind.annotation.SuperCall; -import org.awaitility.Awaitility; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import accord.impl.progresslog.DefaultProgressLogs; +import com.datastax.driver.core.ConsistencyLevel; import com.datastax.driver.core.Session; +import com.datastax.driver.core.SimpleStatement; +import com.datastax.driver.core.Statement; +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.db.Keyspace; import org.apache.cassandra.db.Mutation; import org.apache.cassandra.db.ReadCommand; @@ -42,8 +45,11 @@ import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.Row; import org.apache.cassandra.distributed.api.SimpleQueryResult; +import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.utils.Throwables; +import org.awaitility.Awaitility; +import static com.google.common.base.Preconditions.checkState; import static java.util.concurrent.TimeUnit.SECONDS; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @@ -171,7 +177,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='mixed_reads'"); + SHARED_CLUSTER.schemaChange("CREATE TABLE " + KEYSPACE + ".accord_tbl (k int primary key, v int) WITH " + TransactionalMode.mixed_reads.asCqlParam()); // Disable recovery to make sure only one local read occurs: for (IInvokableInstance instance : SHARED_CLUSTER) @@ -184,8 +190,10 @@ public class QueriesTableTest extends TestBaseImpl " UPDATE " + KEYSPACE + ".accord_tbl SET v = 10 WHERE k = 0;\n" + " END IF\n" + "COMMIT TRANSACTION"; - - SESSION.executeAsync(update); + + Statement statement = new SimpleStatement(update); + statement.setConsistencyLevel(ConsistencyLevel.QUORUM); + SESSION.executeAsync(statement); // Wait until the coordinator update and local read required by the CAS operation are visible: Awaitility.await() @@ -240,12 +248,14 @@ public class QueriesTableTest extends TestBaseImpl static void install(ClassLoader cl, int nodeNumber) { + checkState(Arrays.stream(Mutation.class.getDeclaredMethods()).anyMatch(method -> method.getName().equals("apply") && method.getParameterCount() == 3)); new ByteBuddy().rebase(Mutation.class) .method(named("apply").and(takesArguments(3))) .intercept(MethodDelegation.to(QueryDelayHelper.class)) .make() .load(cl, ClassLoadingStrategy.Default.INJECTION); + checkState(Arrays.stream(ReadCommand.class.getDeclaredMethods()).anyMatch(method -> method.getName().equals("executeLocally") && method.getParameterCount() == 1)); new ByteBuddy().rebase(ReadCommand.class) .method(named("executeLocally").and(takesArguments(1))) .intercept(MethodDelegation.to(QueryDelayHelper.class)) diff --git a/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java index 2bcaad35d9..e9db6a9235 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ReadRepairTest.java @@ -27,9 +27,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.util.concurrent.FutureCallback; -import org.apache.cassandra.distributed.api.*; -import org.apache.cassandra.distributed.test.accord.AccordTestBase; -import org.junit.*; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import net.bytebuddy.ByteBuddy; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; @@ -45,8 +46,14 @@ import org.apache.cassandra.db.partitions.PartitionUpdate; 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.Feature; +import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IMessageFilters.Filter; +import org.apache.cassandra.distributed.api.TokenSupplier; import org.apache.cassandra.distributed.shared.NetworkTopology; +import org.apache.cassandra.distributed.test.accord.AccordTestBase; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.ReplicaPlan; import org.apache.cassandra.service.consensus.TransactionalMode; @@ -57,7 +64,6 @@ import org.apache.cassandra.utils.concurrent.Future; import org.checkerframework.checker.nullness.qual.Nullable; import static net.bytebuddy.matcher.ElementMatchers.named; - import static org.apache.cassandra.config.CassandraRelevantProperties.ALLOW_ALTER_RF_DURING_RANGE_MOVEMENT; import static org.apache.cassandra.db.Keyspace.open; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; @@ -116,7 +122,7 @@ public class ReadRepairTest extends TestBaseImpl private void testReadRepair(ReadRepairStrategy strategy, boolean brrThroughAccord) throws Throwable { try (Cluster cluster = init(Cluster.create(3, c -> c.with(Feature.GOSSIP, Feature.NETWORK)))) { - TransactionalMode transactionalMode = brrThroughAccord ? TransactionalMode.unsafe_writes : TransactionalMode.off; + TransactionalMode transactionalMode = brrThroughAccord ? TransactionalMode.test_unsafe_writes : TransactionalMode.off; cluster.schemaChange(withKeyspace("CREATE TABLE %s." + tableName + " (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode='" + transactionalMode.toString().toLowerCase() + '\'' + String.format(" AND read_repair='%s'", strategy))); AccordTestBase.ensureTableIsAccordManaged(cluster, KEYSPACE, "t"); diff --git a/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java index c9ab6b30cc..98b92aa06b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ShortReadProtectionTest.java @@ -91,7 +91,7 @@ public class ShortReadProtectionTest extends TestBaseImpl public static Collection data() { List result = new ArrayList<>(); - for (TransactionalMode mode : ImmutableList.of(TransactionalMode.mixed_reads, TransactionalMode.off)) + for (TransactionalMode mode : ImmutableList.of(TransactionalMode.test_interop_read, TransactionalMode.off)) for (ConsistencyLevel readConsistencyLevel : Arrays.asList(ALL, QUORUM, SERIAL)) for (boolean flush : BOOLEANS) for (boolean paging : BOOLEANS) @@ -446,13 +446,14 @@ public class ShortReadProtectionTest extends TestBaseImpl private Tester createTable(String query) { - cluster.schemaChange(format(query) + " WITH read_repair='NONE'"); + String formattedQuery = format(query) + " WITH read_repair='NONE'"; if (transactionalMode != TransactionalMode.off) { - // For test purposes we create the table and require migration otherwise Accord - // won't bother to do interop reads with short read protection - cluster.schemaChange(format("ALTER TABLE %s WITH transactional_mode='" + transactionalMode + "\' AND transactional_migration_from = \'off\'")); + // For test purposes we create the table and in an interop mode that forces interop reads so + // testing short reads is trivial + formattedQuery = formattedQuery + " AND " + transactionalMode.asCqlParam(); } + cluster.schemaChange(formattedQuery); return this; } diff --git a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java index 9107848f74..4a0b54c386 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/TestBaseImpl.java @@ -324,6 +324,11 @@ public class TestBaseImpl extends DistributedTestBase return nodetool(coordinator.instance(), commandAndArgs); } + public static ListenableFuture nodetoolAsync(IInstance instance, String... commandAndArgs) + { + return nodetoolAsync(instance.coordinator(), commandAndArgs); + } + public static ListenableFuture nodetoolAsync(ICoordinator coordinator, String... commandAndArgs) { ListenableFutureTask task = ListenableFutureTask.create(() -> nodetool(coordinator, commandAndArgs)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordCQLTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordCQLTestBase.java index 39c26d8e62..92abe1bb83 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordCQLTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordCQLTestBase.java @@ -45,8 +45,10 @@ import org.slf4j.LoggerFactory; import accord.primitives.Unseekables; import accord.topology.Topologies; +import org.apache.cassandra.config.Config.PaxosVariant; import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.cql3.functions.types.utils.Bytes; +import org.apache.cassandra.cql3.statements.TransactionStatement; import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.ListType; import org.apache.cassandra.db.marshal.MapType; @@ -59,6 +61,7 @@ import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.distributed.api.QueryResults; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.shared.AssertUtils; +import org.apache.cassandra.distributed.test.sai.SAIUtil; import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.service.accord.AccordService; import org.apache.cassandra.service.accord.AccordTestUtils; @@ -75,6 +78,9 @@ import static org.apache.cassandra.cql3.CQLTester.row; import static org.apache.cassandra.cql3.statements.schema.AlterTableStatement.ACCORD_COUNTER_COLUMN_UNSUPPORTED; import static org.apache.cassandra.cql3.statements.schema.AlterTableStatement.ACCORD_COUNTER_TABLES_UNSUPPORTED; import static org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -99,10 +105,89 @@ public abstract class AccordCQLTestBase extends AccordTestBase @BeforeClass public static void setupClass() throws IOException { - AccordTestBase.setupCluster(builder -> builder, 2); + AccordTestBase.setupCluster(builder -> builder.appendConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL) + .set("paxos_variant", PaxosVariant.v2.name())), 2); SHARED_CLUSTER.schemaChange("CREATE TYPE " + KEYSPACE + ".person (height int, age int)"); } + @Test + public void testRejectTransactionStatement() throws Throwable + { + test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, primary key (k, c))", cluster -> { + ICoordinator coordinator = cluster.coordinator(1); + String readQuery = "BEGIN TRANSACTION\n" + + " SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 0;\n" + + "COMMIT TRANSACTION;"; + String writeQuery = "BEGIN TRANSACTION\n" + + " INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 43, 44);\n" + + "COMMIT TRANSACTION;"; + + // Not enabled on table or migrating/migrated + try + { + coordinator.execute(readQuery, ConsistencyLevel.ALL); + fail("Expected exception"); + } + catch (Throwable t) + { + assertEquals(InvalidRequestException.class.getName(), t.getClass().getName()); + assertEquals(t.getMessage(), format(TransactionStatement.TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "SELECT", "at [2:3]")); + } + + try + { + coordinator.execute(writeQuery, ConsistencyLevel.ALL); + fail("Expected exception"); + } + catch (Throwable t) + { + assertEquals(InvalidRequestException.class.getName(), t.getClass().getName()); + assertEquals(t.getMessage(), format(TransactionStatement.TRANSACTIONS_DISABLED_ON_TABLE_MESSAGE, "INSERT", "at [2:3]")); + } + + // Enabled on table but not migrating/migrated + coordinator.execute("ALTER TABLE " + qualifiedAccordTableName + " WITH transactional_mode = '" + transactionalMode.name() + "';", ConsistencyLevel.ALL); + try + { + cluster.coordinator(1).execute(readQuery, ConsistencyLevel.ALL); + fail("Expected exception"); + } + catch (Throwable t) + { + assertEquals(InvalidRequestException.class.getName(), t.getClass().getName()); + assertEquals(t.getMessage(), TransactionStatement.UNSUPPORTED_MIGRATION); + } + + // Blind writes are allowed because Accord does know how to execute them correctly via interop + coordinator.execute(writeQuery, ConsistencyLevel.ALL); + + // Enabled on table but migrating + nodetool(coordinator, "consensus_admin", "begin-migration", KEYSPACE, accordTableName); + try + { + coordinator.execute(readQuery, ConsistencyLevel.ALL); + fail("Expected exception"); + } + catch (Throwable t) + { + assertEquals(InvalidRequestException.class.getName(), t.getClass().getName()); + assertEquals(t.getMessage(), TransactionStatement.UNSUPPORTED_MIGRATION); + } + + // Write query should succeed even if Accord can't read + coordinator.execute(writeQuery, ConsistencyLevel.ALL); + // Should also work as a non-SERIAL insert + coordinator.execute("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 43, 44);", ConsistencyLevel.ALL); + // And CAS + coordinator.execute("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 43, 44) IF NOT EXISTS;", ConsistencyLevel.SERIAL, ConsistencyLevel.ALL); + + // Enabled on table and migration has done data repair + nodetool(coordinator, "repair", "-skip-accord", "-skip-paxos", KEYSPACE, accordTableName); + coordinator.execute(readQuery, ConsistencyLevel.ALL); + coordinator.execute(writeQuery, ConsistencyLevel.ALL); + }); + } + @Test public void testCounterCreateTableTransactionalModeFails() throws Exception { @@ -233,6 +318,7 @@ public abstract class AccordCQLTestBase extends AccordTestBase { test(cluster -> { cluster.schemaChange("CREATE INDEX ON " + qualifiedAccordTableName + "(v) USING 'sai';"); + SAIUtil.waitForIndexQueryable(cluster, KEYSPACE); for (int i = 0; i < 3; i++) cluster.coordinator(1).execute(wrapInTxn("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (?, ?, ?)"), ConsistencyLevel.ALL, 42, 43 + i, 44 + i); @@ -253,6 +339,7 @@ public abstract class AccordCQLTestBase extends AccordTestBase { test(cluster -> { cluster.schemaChange("CREATE INDEX ON " + qualifiedAccordTableName + "(v) USING 'org.apache.cassandra.index.sasi.SASIIndex';"); + SAIUtil.waitForIndexQueryable(cluster, KEYSPACE); for (int i = 0; i < 3; i++) cluster.coordinator(1).execute(wrapInTxn("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (?, ?, ?)"), ConsistencyLevel.ALL, 42, 43 + i, 44 + i); @@ -272,6 +359,7 @@ public abstract class AccordCQLTestBase extends AccordTestBase { test(cluster -> { cluster.schemaChange("CREATE INDEX ON " + qualifiedAccordTableName + "(v);"); + SAIUtil.waitForIndexQueryable(cluster, KEYSPACE); for (int i = 0; i < 3; i++) cluster.coordinator(1).execute(wrapInTxn("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (?, ?, ?)"), ConsistencyLevel.ALL, 42, 43 + i, 44 + i); @@ -391,12 +479,13 @@ public abstract class AccordCQLTestBase extends AccordTestBase ICoordinator node = cluster.coordinator(1); cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadSingleToken (pk0 int, ck0 int, static0 int, regular0 int, PRIMARY KEY (pk0, ck0)) WITH " + transactionalMode.asCqlParam() + " AND CLUSTERING ORDER BY (ck0 ASC);")); cluster.schemaChange(withKeyspace("CREATE INDEX ck0_sai_idx ON %s.testRangeReadSingleToken (ck0) USING 'sai';")); + SAIUtil.waitForIndexQueryable(cluster, KEYSPACE); node.executeWithResult(withKeyspace("INSERT INTO %s.testRangeReadSingleToken (pk0, ck0, static0, regular0) VALUES (?, ?, ?, ?)"), QUORUM, 42, 43, 44, 45); assertThat(node.executeWithResult(withKeyspace("SELECT pk0, ck0, static0, regular0 FROM %s.testRangeReadSingleToken WHERE pk0 = ? AND ck0 = ? AND static0 <= ? AND regular0 >= ? ALLOW FILTERING;"), ConsistencyLevel.ALL, 42, 43, 44, 45)) .isEqualTo(42, 43, 44, 45); // This one is a little more explicit about trying to force a range read of a single token - cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadSingleToken2 (pk blob primary key) WITH " + TransactionalMode.full.asCqlParam())); + cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadSingleToken2 (pk blob primary key) WITH " + transactionalMode.asCqlParam())); long token = 42; ByteBuffer keyForToken = Murmur3Partitioner.LongToken.keyForToken(token); node.executeWithResult(withKeyspace("INSERT INTO %s.testRangeReadSingleToken2 (pk) VALUES (?)"), QUORUM, keyForToken); @@ -419,7 +508,7 @@ public abstract class AccordCQLTestBase extends AccordTestBase test(cluster -> { ICoordinator node = cluster.coordinator(1); - cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadRightMin (pk blob primary key) WITH " + TransactionalMode.full.asCqlParam())); + cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadRightMin (pk blob primary key) WITH " + transactionalMode.asCqlParam())); long token = Long.MIN_VALUE; ByteBuffer keyForToken = Murmur3Partitioner.LongToken.keyForToken(token); node.executeWithResult(withKeyspace("INSERT INTO %s.testRangeReadRightMin (pk) VALUES (?)"), QUORUM, keyForToken); @@ -440,6 +529,21 @@ public abstract class AccordCQLTestBase extends AccordTestBase }); } + @Test + public void testRangeReadAllowFiltering() throws Throwable + { + test(cluster -> + { + ICoordinator node = cluster.coordinator(1); + cluster.schemaChange(withKeyspace("CREATE TABLE %s.testRangeReadAllowFiltering (pk int primary key, foo text) WITH " + transactionalMode.asCqlParam())); + long token = Long.MIN_VALUE; + ByteBuffer keyForToken = Murmur3Partitioner.LongToken.keyForToken(token); + node.executeWithResult(withKeyspace("INSERT INTO %s.testRangeReadAllowFiltering (pk, foo) VALUES (?, ?)"), QUORUM, 42, "ba"); + assertThat(node.executeWithResult(withKeyspace("SELECT * FROM %s.testRangeReadAllowFiltering WHERE foo < 'bar' ALLOW FILTERING"), QUORUM)) + .isEqualTo(42, "ba"); + }); + } + @Test public void testIN() throws Exception { @@ -772,7 +876,7 @@ public abstract class AccordCQLTestBase extends AccordTestBase { accordRead = wrapInTxn(accordRead); Object[][] simpleReadResult; - if (transactionalMode.ignoresSuppliedCommitCL) + if (transactionalMode.ignoresSuppliedCommitCL()) // With accord non-SERIAL write strategy the commit CL is effectively ANY so we need to read at SERIAL simpleReadResult = cluster.coordinator(1).execute(simpleRead, ConsistencyLevel.SERIAL, key); else diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropKeyspaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropKeyspaceTest.java index 353980f55f..fd9b653a84 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropKeyspaceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropKeyspaceTest.java @@ -23,9 +23,12 @@ import java.io.IOException; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.schema.TableId; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + public class AccordDropKeyspaceTest extends AccordDropTableBase { @Test @@ -35,7 +38,7 @@ public class AccordDropKeyspaceTest extends AccordDropTableBase int steps = 5; try (Cluster cluster = Cluster.build(3) .withoutVNodes() - .withConfig(c -> c.with(Feature.values()) + .withConfig(c -> c.with(GOSSIP, NETWORK, NATIVE_PROTOCOL) .set("auto_snapshot", false)) .start()) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropTableTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropTableTest.java index b13bcb05ce..da2669dd5f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropTableTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordDropTableTest.java @@ -23,9 +23,12 @@ import java.io.IOException; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; -import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.schema.TableId; +import static org.apache.cassandra.distributed.api.Feature.GOSSIP; +import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; +import static org.apache.cassandra.distributed.api.Feature.NETWORK; + public class AccordDropTableTest extends AccordDropTableBase { @Test @@ -35,7 +38,7 @@ public class AccordDropTableTest extends AccordDropTableBase int steps = 5; try (Cluster cluster = Cluster.build(3) .withoutVNodes() - .withConfig(c -> c.with(Feature.values()) + .withConfig(c -> c.with(GOSSIP, NETWORK, NATIVE_PROTOCOL) .set("auto_snapshot", false)) .start()) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordIncrementalRepairTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordIncrementalRepairTest.java index bccbe293fe..72db50303a 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordIncrementalRepairTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordIncrementalRepairTest.java @@ -378,7 +378,7 @@ public class AccordIncrementalRepairTest extends AccordTestBase @Test public void unsafeRepairTest() { - testSingleNodeWrite(TransactionalMode.unsafe); + testSingleNodeWrite(TransactionalMode.test_unsafe); } /** diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteropReadTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteropReadTest.java index 72409dc871..fb32c0a15f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteropReadTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteropReadTest.java @@ -32,6 +32,7 @@ import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.distributed.util.QueryResultUtil; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.QueryState; +import org.apache.cassandra.service.consensus.TransactionalMode; import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.NETWORK; @@ -63,7 +64,7 @@ public class AccordInteropReadTest extends TestBaseImpl .withConfig(config -> config.with(GOSSIP).with(NETWORK)).start()) { cluster.schemaChange("CREATE KEYSPACE ks WITH REPLICATION={'class':'SimpleStrategy', 'replication_factor':3}"); - cluster.schemaChange("CREATE TABLE ks.tbl (k int, c int, v int, PRIMARY KEY (k, c)) WITH transactional_mode='unsafe_writes'"); + cluster.schemaChange("CREATE TABLE ks.tbl (k int, c int, v int, PRIMARY KEY (k, c)) WITH " + TransactionalMode.test_unsafe_writes.asCqlParam()); cluster.get(1).runOnInstance(() -> localWrite("INSERT INTO ks.tbl (k, c, v) VALUES (1, 1, 1)")); cluster.get(2).runOnInstance(() -> localWrite("INSERT INTO ks.tbl (k, c, v) VALUES (1, 1, 2)")); diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteroperabilityTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteroperabilityTest.java index f485d4d089..8f072a9782 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteroperabilityTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordInteroperabilityTest.java @@ -19,8 +19,6 @@ package org.apache.cassandra.distributed.test.accord; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import java.util.function.Function; import org.junit.After; @@ -29,27 +27,13 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.cassandra.db.ColumnFamilyStore; import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.db.DataRange; -import org.apache.cassandra.db.filter.ColumnFilter; -import org.apache.cassandra.db.memtable.Memtable; -import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; -import org.apache.cassandra.db.rows.Row; -import org.apache.cassandra.db.rows.UnfilteredRowIterator; import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.distributed.shared.AssertUtils; import org.apache.cassandra.exceptions.InvalidRequestException; -import org.apache.cassandra.io.sstable.SSTableReadsListener; -import org.apache.cassandra.net.Verb; import org.apache.cassandra.service.accord.IAccordService; -import static com.google.common.base.Throwables.getStackTraceAsString; -import static org.apache.cassandra.Util.dk; -import static org.apache.commons.collections.ListUtils.synchronizedList; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class AccordInteroperabilityTest extends AccordTestBase @@ -90,73 +74,6 @@ public class AccordInteroperabilityTest extends AccordTestBase ); } - @Test - public void testApplyIsInteropApply() throws Throwable - { - test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY(k, c)) WITH transactional_mode='mixed_reads'", - cluster -> { - MessageCountingSink messageCountingSink = new MessageCountingSink(SHARED_CLUSTER); - List failures = synchronizedList(new ArrayList<>()); - // Verify that the apply response is only sent after the row has been inserted - // TODO (required): Need to delay mutation stage/mutation to ensure this has time to catch it - SHARED_CLUSTER.setMessageSink((to, message) -> { - try - { - if (message.verb() == Verb.ACCORD_APPLY_RSP.id) - { - String currentThread = Thread.currentThread().getName(); - char nodeIndexChar = currentThread.charAt(4); - int nodeIndex = Integer.parseInt(String.valueOf(nodeIndexChar)); - try - { - String keyspace = KEYSPACE; - String tableName = accordTableName; - String fail = SHARED_CLUSTER.get(nodeIndex).callOnInstance(() -> { - ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspace, tableName); - Memtable memtable = cfs.getCurrentMemtable(); - assertEquals(1, memtable.partitionCount()); - UnfilteredPartitionIterator partitions = memtable.partitionIterator(ColumnFilter.all(cfs.metadata()), DataRange.allData(cfs.getPartitioner()), SSTableReadsListener.NOOP_LISTENER); - assertTrue(partitions.hasNext()); - UnfilteredRowIterator rows = partitions.next(); - assertEquals(dk(42), rows.partitionKey()); - assertFalse(partitions.hasNext()); - assertTrue(rows.hasNext()); - Row row = (Row)rows.next(); - assertFalse(rows.hasNext()); - return null; - }); - if (fail != null) - failures.add(fail); - } - catch (Exception e) - { - failures.add(getStackTraceAsString(e)); - } - } - } - finally - { - messageCountingSink.accept(to, message); - } - }); - cluster.coordinator(1).execute("INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 2, 3)", org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM); - assertEquals(3, messageCounts.get(Verb.ACCORD_INTEROP_APPLY_REQ).get()); - assertTrue(failures.toString(), failures.isEmpty()); - }); - } - - @Test - public void testReadIsAtQuorum() throws Throwable - { - test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY(k, c)) WITH transactional_mode='unsafe_writes'", - cluster -> { - SHARED_CLUSTER.setMessageSink(new MessageCountingSink(SHARED_CLUSTER)); - cluster.coordinator(1).execute("SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 0", org.apache.cassandra.distributed.api.ConsistencyLevel.SERIAL); - assertEquals(2, messageCounts.get(Verb.ACCORD_INTEROP_STABLE_THEN_READ_REQ).get()); - assertEquals(2, messageCounts.get(Verb.ACCORD_INTEROP_READ_RSP).get()); - }); - } - private static Object[][] assertTargetAccordRead(Function query, int coordinatorIndex, int key, int expectedAccordReadCount) { int startingReadCount = getAccordReadCount(coordinatorIndex); diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMetricsTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMetricsTest.java index 0ee6f72e43..0b90db1355 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMetricsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMetricsTest.java @@ -89,8 +89,9 @@ public class AccordMetricsTest extends AccordTestBase String readCql() { return "BEGIN TRANSACTION\n" + - " LET val = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=? AND c=?);\n" + - " SELECT val.v;\n" + + " LET val1 = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=? AND c=?);\n" + + " LET val2 = (SELECT v FROM " + qualifiedAccordTableName + " WHERE k=? AND c=?);\n" + + " SELECT val1.v, val2.v;\n" + "COMMIT TRANSACTION"; } @@ -104,7 +105,7 @@ public class AccordMetricsTest extends AccordTestBase } @Test - public void testRegularMetrics() throws Exception + public void testRegularMetrics() { countingMetrics0 = getMetrics(); assertCoordinatorMetrics(0, "rw", 0, 0, 0, 0, 0); @@ -116,7 +117,7 @@ public class AccordMetricsTest extends AccordTestBase assertZeroMetrics("ro"); countingMetrics0 = getMetrics(); - SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0); + SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0, 1, 1); assertCoordinatorMetrics(0, "ro", 1, 0, 0, 0, 0); assertCoordinatorMetrics(1, "ro", 0, 0, 0, 0, 0); assertReplicaMetrics(0, "ro", 1, 1, 0); @@ -136,46 +137,59 @@ public class AccordMetricsTest extends AccordTestBase .messagesMatching(delay) .drop(); + long originalAccordRecoverDelay = SHARED_CLUSTER.get(1).callOnInstance(() -> DatabaseDescriptor.getAccordRecoverDelay(TimeUnit.MILLISECONDS)); SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setAccordRecoverDelay(100L, TimeUnit.MILLISECONDS)); + long originalTransactionTimeoutMillis = SHARED_CLUSTER.get(1).callOnInstance(() -> DatabaseDescriptor.getTransactionTimeout(TimeUnit.MILLISECONDS)); + SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setTransactionTimeout(12_000)); + long originalWriteRpcTimeoutMillis = SHARED_CLUSTER.get(1).callOnInstance(() -> DatabaseDescriptor.getWriteRpcTimeout(TimeUnit.MILLISECONDS)); + SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setWriteRpcTimeout(12_000)); - countingMetrics0 = getMetrics(); try { - SHARED_CLUSTER.coordinator(1).executeWithResult(writeCql(), ConsistencyLevel.ALL, 0, 0, 0, 0); - fail("expected to fail"); + countingMetrics0 = getMetrics(); + try + { + SHARED_CLUSTER.coordinator(1).executeWithResult(writeCql(), ConsistencyLevel.ALL, 0, 0, 0, 0); + fail("expected to fail"); + } + catch (RuntimeException ex) + { + Assertions.assertThat(ex).is(AssertionUtils.rootCauseIs(WritePreemptedException.class)); + } + + assertCoordinatorMetrics(0, "rw", 0, 0, 1, 0, 0); + assertCoordinatorMetrics(1, "rw", 0, 0, 0, 0, 0); + assertReplicaMetrics(0, "rw", 0, 0, 0); + assertReplicaMetrics(1, "rw", 0, 0, 0); + + assertZeroMetrics("ro"); + + countingMetrics0 = getMetrics(); + try + { + SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0, 1, 1); + fail("expected to fail"); + } + catch (RuntimeException ex) + { + Assertions.assertThat(ex).is(AssertionUtils.rootCauseIs(ReadPreemptedException.class)); + } + + assertCoordinatorMetrics(0, "ro", 0, 0, 1, 0, 0); + assertCoordinatorMetrics(1, "ro", 0, 0, 0, 0, 0); + assertReplicaMetrics(0, "ro", 0, 0, 0); + assertReplicaMetrics(1, "ro", 0, 0, 0); + + assertZeroMetrics("rw"); } - catch (RuntimeException ex) + finally { - Assertions.assertThat(ex).is(AssertionUtils.rootCauseIs(WritePreemptedException.class)); + SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setAccordRecoverDelay(originalAccordRecoverDelay, TimeUnit.SECONDS)); + SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setWriteRpcTimeout(originalWriteRpcTimeoutMillis)); + SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setTransactionTimeout(originalTransactionTimeoutMillis)); + preacceptDelay.off(); + exec.shutdown(); } - - assertCoordinatorMetrics(0, "rw", 0, 0, 1, 0, 0); - assertCoordinatorMetrics(1, "rw", 0, 0, 0, 0, 0); - assertReplicaMetrics(0, "rw", 0, 0, 0); - assertReplicaMetrics(1, "rw", 0, 0, 0); - - assertZeroMetrics("ro"); - - countingMetrics0 = getMetrics(); - try - { - SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0); - fail("expected to fail"); - } - catch (RuntimeException ex) - { - Assertions.assertThat(ex).is(AssertionUtils.rootCauseIs(ReadPreemptedException.class)); - } - - assertCoordinatorMetrics(0, "ro", 0, 0, 1, 0, 0); - assertCoordinatorMetrics(1, "ro", 0, 0, 0, 0, 0); - assertReplicaMetrics(0, "ro", 0, 0, 0); - assertReplicaMetrics(1, "ro", 0, 0, 0); - - assertZeroMetrics("rw"); - SHARED_CLUSTER.forEach(() -> DatabaseDescriptor.setAccordRecoverDelay(10L, TimeUnit.SECONDS)); - preacceptDelay.off(); - exec.shutdown(); } @Test @@ -187,7 +201,7 @@ public class AccordMetricsTest extends AccordTestBase countingMetrics0 = getMetrics(); try { - SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0); + SHARED_CLUSTER.coordinator(1).executeWithResult(readCql(), ConsistencyLevel.ALL, 0, 0, 1, 1); fail("expected to fail"); } catch (RuntimeException ex) diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java new file mode 100644 index 0000000000..8e35715caa --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationReadRaceTestBase.java @@ -0,0 +1,645 @@ +/* + * 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.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; +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.collect.ImmutableList; +import com.google.common.collect.Sets; +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.primitives.Ranges; +import org.apache.cassandra.ServerTestUtils; +import org.apache.cassandra.Util; +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.config.Config.PaxosVariant; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.dht.IPartitioner; +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; +import org.apache.cassandra.distributed.Cluster; +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.SimpleQueryResult; +import org.apache.cassandra.distributed.shared.ClusterUtils; +import org.apache.cassandra.distributed.test.accord.InteropTokenRangeTest.TokenOperator; +import org.apache.cassandra.repair.RepairJobDesc; +import org.apache.cassandra.repair.RepairResult; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableId; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.service.accord.AccordService; +import org.apache.cassandra.service.accord.TokenRange; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; +import org.apache.cassandra.service.consensus.TransactionalMode; +import org.apache.cassandra.service.consensus.migration.ConsensusMigrationRepairResult; +import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration; +import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode; +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.Throwables.ThrowingRunnable; +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.util.ConcurrentHashSet; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.String.format; +import static org.apache.cassandra.Util.spinAssertEquals; +import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken.keyForToken; +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.pauseBeforeEnacting; +import static org.apache.cassandra.distributed.shared.ClusterUtils.unpauseEnactment; +import static org.apache.cassandra.distributed.test.accord.InteropTokenRangeTest.TokenOperator.gt; +import static org.apache.cassandra.distributed.test.accord.InteropTokenRangeTest.TokenOperator.gte; +import static org.apache.cassandra.distributed.test.accord.InteropTokenRangeTest.TokenOperator.lt; +import static org.apache.cassandra.distributed.test.accord.InteropTokenRangeTest.TokenOperator.lte; +import static org.apache.cassandra.distributed.util.QueryResultUtil.assertThat; +import static org.apache.cassandra.utils.ByteBufferUtil.bytesToHex; +import static org.junit.Assert.assertEquals; + +/* + * Test that non-transactional read operations migrating to/from a mode where Accord ignores commit consistency levels + * and does aysnc commit are routed correctly. Currently this is just TransactionalMode.full + */ +public abstract class AccordMigrationReadRaceTestBase extends AccordTestBase +{ + private static final Logger logger = LoggerFactory.getLogger(AccordMigrationReadRaceTestBase.class); + private static final int TEST_BOUNDS_CONCURRENCY = 32; + // Set BATCH_INDEX to the failing batch and this to true to find out the query index, then set QUERY_INDEX + private static final boolean EXECUTE_BATCH_QUERIES_SERIALLY = false; + // Specify only a single batch or query to run + private static final Integer BATCH_INDEX = null; + private static final Integer QUERY_INDEX = null; + private static final String TABLE_FMT = "CREATE TABLE %s (pk blob, c int, v int, PRIMARY KEY ((pk), c));"; + + private static IPartitioner partitioner; + + private static Range migratingRange; + + private static ICoordinator coordinator; + + private final static TestMessageSink messageSink = new TestMessageSink(); + private static class TestMessageSink implements IMessageSink + { + private final Queue> messages = new ConcurrentLinkedQueue<>(); + private final Set 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); + } + } + + private final boolean migrateAwayFromAccord; + + protected AccordMigrationReadRaceTestBase() + { + this.migrateAwayFromAccord = migratingAwayFromAccord(); + } + + protected abstract boolean migratingAwayFromAccord(); + + @Override + protected Logger logger() + { + return logger; + } + + @BeforeClass + public static void setupClass() throws IOException + { + 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("read_request_timeout", "2s") + .set("range_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(); + LongToken migrationStart = new LongToken(Long.valueOf(SHARED_CLUSTER.get(2).callOnInstance(() -> DatabaseDescriptor.getInitialTokens().iterator().next()))); + LongToken migrationEnd = new LongToken(Long.valueOf(SHARED_CLUSTER.get(3).callOnInstance(() -> DatabaseDescriptor.getInitialTokens().iterator().next()))); + migratingRange = new Range<>(migrationStart, migrationEnd); + coordinator = SHARED_CLUSTER.coordinator(1); + SHARED_CLUSTER.setMessageSink(messageSink); + buildData(); + } + + private static final int NUM_PARTITIONS = 1000; + private static final int ROWS_PER_PARTITION = 10; + private static final Object[][][] data = new Object[NUM_PARTITIONS][][]; + private static final Object[][] dataFlat = new Object[NUM_PARTITIONS * ROWS_PER_PARTITION][]; + private static ByteBuffer pkeyAccord; + private static int pkeyAccordDataIndex; + + private static void buildData() + { + Random r = new Random(0); + long[] tokens = new long[NUM_PARTITIONS]; + for (int i = 0; i < tokens.length; i++) + tokens[i] = r.nextLong(); + Arrays.sort(tokens); + + for (int i = 0; i < NUM_PARTITIONS; i++) + { + data[i] = new Object[ROWS_PER_PARTITION][]; + ByteBuffer pk = keyForToken(tokens[i]); + for (int j = 0; j < ROWS_PER_PARTITION; j++) + { + int clustering = r.nextInt(); + data[i][j] = new Object[] { pk, clustering, 42 }; + } + Arrays.sort(data[i], Comparator.comparing(row -> (Integer)row[1])); + } + for (int i = 0; i < NUM_PARTITIONS; i++) + { + for (int j = 0; j < ROWS_PER_PARTITION; j++) + { + int idx = i * ROWS_PER_PARTITION + j; + dataFlat[idx] = new Object[] { data[i][j][0], data[i][j][1], data[i][j][2] }; + if (migratingRange.contains(Murmur3Partitioner.instance.getToken((ByteBuffer)data[i][j][0]))) + { + pkeyAccord = (ByteBuffer)data[i][j][0]; + pkeyAccordDataIndex = i; + } + } + } + } + + @AfterClass + public static void tearDownClass() + { + StorageService.instance.resetPartitionerUnsafe(); + } + + @After + public void tearDown() throws Exception + { + super.tearDown(); + messageSink.reset(); + SHARED_CLUSTER.forEach(ClusterUtils::clearAndUnpause); + super.tearDown(); + } + + private void loadData() throws Exception + { + logger.info("Starting data load"); + Stopwatch sw = Stopwatch.createStarted(); + List> inserts = new ArrayList<>(); + for (int i = 0; i < NUM_PARTITIONS; i++) + { + for (int j = 0; j < ROWS_PER_PARTITION; j++) + inserts.add(coordinator.asyncExecuteWithResult(insertCQL(qualifiedAccordTableName, (ByteBuffer)data[i][j][0], (int)data[i][j][1], (int)data[i][j][2]), ALL)); + + if (i % 100 == 0) + { + for (java.util.concurrent.Future insert : inserts) + insert.get(); + inserts.clear(); + } + } + logger.info("Data load took %dms", sw.elapsed(TimeUnit.MILLISECONDS)); + } + + private NavigableSet boundsTokens() + { + long migratingRangeStart = migratingRange.left.getLongValue(); + long migratingRangeEnd = migratingRange.right.getLongValue(); + NavigableSet set = new TreeSet<>(); + set.add(migratingRangeStart - 1); + set.add(migratingRangeStart); + set.add(migratingRangeStart + 1); + set.add(migratingRangeEnd - 1); + set.add(migratingRangeEnd); + set.add(migratingRangeEnd + 1); + set.add(Long.MAX_VALUE); + set.add(Long.MIN_VALUE + 1); + set.add(0L); + return set; + } + + private void loadOverlapData() + { + for (long token : boundsTokens()) + coordinator.executeWithResult(insertCQL(qualifiedAccordTableName, keyForToken(token), 42, 43), ALL); + } + + @Test + public void testKeyRouting() throws Throwable + { + String readCQL = "SELECT * FROM " + qualifiedAccordTableName + " WHERE pk = 0x" + bytesToHex(pkeyAccord); + testSplitAndRetry(readCQL, this::loadData, result -> assertThat(result).isDeepEqualTo(data[pkeyAccordDataIndex])); + } + + @Test + public void testRangeRouting() throws Throwable + { + String cql = "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(pk) > " + Murmur3Partitioner.MINIMUM.token; + testSplitAndRetry(cql, this::loadData, result -> { + assertThat(result).isDeepEqualTo(dataFlat); + }); + } + + @Test + public void testBounds() throws Throwable + { + NavigableSet tokens = boundsTokens(); + Queue queries = new ArrayDeque<>(); + Queue> validations = new ArrayDeque<>(); + Queue retryExpectedQueries = new ArrayDeque<>(); + Queue> retryExpectedValidations = new ArrayDeque<>(); + for (long firstToken : tokens) + { + ByteBuffer pk = keyForToken(firstToken); + for (TokenOperator op : TokenOperator.values()) + { + String cql = "SELECT * FROM %s WHERE " + op.condition; + cql = cql.replace("?", "0x" + bytesToHex(pk)); + NavigableSet expectedTokens = op.expected(firstToken, tokens); + boolean expectRetry = op.intersects(firstToken, migratingRange); + Consumer validation = result -> { + Assertions.assertThat(InteropTokenRangeTest.tokens(result)) + .describedAs("Token %d with operator %s", firstToken, op.condition) + .isEqualTo(expectedTokens); + }; + if (expectRetry) + { + retryExpectedQueries.add(cql); + retryExpectedValidations.add(validation); + } + else + { + queries.add(cql); + validations.add(validation); + } + } + + for (long secondToken : tokens) + { + for (TokenOperator lt : Arrays.asList(lt, lte)) + { + for (TokenOperator gt : Arrays.asList(gt, gte)) + { + ByteBuffer gtPk = keyForToken(secondToken); + String cql = "SELECT * FROM %s WHERE " + lt.condition + " AND " + gt.condition; + cql = cql.replaceFirst("\\?", "0x" + bytesToHex(pk)); + cql = cql.replaceFirst("\\?", "0x" + bytesToHex(gtPk)); + NavigableSet expectedTokens = new TreeSet<>(Sets.intersection(lt.expected(firstToken, tokens), gt.expected(secondToken, tokens))); + Consumer validation = result -> { + Assertions.assertThat(InteropTokenRangeTest.tokens(result)) + .describedAs("LT Token %d GT Token %d with operators %s / %s", firstToken, secondToken, lt.condition, gt.condition) + .isEqualTo(expectedTokens); + }; + boolean expectRetry = lt.intersects(firstToken, migratingRange) && gt.intersects(secondToken, migratingRange); + // This evaluates to no rows without actually executing + if (firstToken == secondToken && (lt == TokenOperator.lt || gt == TokenOperator.gt)) + expectRetry = false; + if (firstToken < secondToken) + expectRetry = false; + if (expectRetry) + { + retryExpectedQueries.add(cql); + retryExpectedValidations.add(validation); + } + else + { + queries.add(cql); + validations.add(validation); + } + } + } + + ByteBuffer rhsPK = keyForToken(secondToken); + String cql = "SELECT * FROM %s WHERE token(pk) BETWEEN token(?) AND token(?)"; + cql = cql.replaceFirst("\\?", "0x" + bytesToHex(pk)); + cql = cql.replaceFirst("\\?", "0x" + bytesToHex(rhsPK)); + NavigableSet expectedTokens = new TreeSet<>(Sets.intersection(gte.expected(firstToken, tokens), lte.expected(secondToken, tokens))); + Consumer validation = result -> { + Assertions.assertThat(InteropTokenRangeTest.tokens(result)) + .describedAs("Between token %d and %d with operator token(pk) BETWEEN token(?) AND token(?)", firstToken, secondToken) + .isEqualTo(expectedTokens); + }; + // Cassandra straight up returns the wrong answer here so until it is fixed skip it + // https://issues.apache.org/jira/browse/CASSANDRA-20154 + if (firstToken > secondToken) + continue; + boolean expectRetry = gte.intersects(firstToken, migratingRange) && lte.intersects(secondToken, migratingRange); + if (expectRetry) + { + retryExpectedQueries.add(cql); + retryExpectedValidations.add(validation); + } + else + { + queries.add(cql); + validations.add(validation); + } + } + } + + testBoundsBatches(queries, validations, false); + testBoundsBatches(retryExpectedQueries, retryExpectedValidations, true); + } + + private void testBoundsBatches(Queue queries, Queue> validations, boolean expectRetry) throws Throwable + { + List queryBatch = new ArrayList<>(); + List> validationBatch = new ArrayList<>(); + int batchCount = 0; + while (!queries.isEmpty()) + { + queryBatch.add(queries.poll()); + validationBatch.add(validations.poll()); + if (queryBatch.size() == TEST_BOUNDS_CONCURRENCY) + { + if (BATCH_INDEX == null || BATCH_INDEX == batchCount) + { + logger.info("Executing batch {}", batchCount); + testBoundsBatch(queryBatch, validationBatch, expectRetry, batchCount); + } + else + { + logger.info("Skipping batch {}", batchCount); + } + batchCount++; + queryBatch.clear(); + validationBatch.clear(); + } + } + + if (!queryBatch.isEmpty()) + { + logger.info("Executing batch " + batchCount); + testBoundsBatch(queryBatch, validationBatch, expectRetry, batchCount); + } + } + + private void testBoundsBatch(List readCQL, List> validation, boolean expectRetry, int batchCount) throws Throwable + { + if (EXECUTE_BATCH_QUERIES_SERIALLY) + { + for (int i = 0; i < readCQL.size(); i++) + { + if (QUERY_INDEX == null || QUERY_INDEX == i) + { + logger.info("Executing query from batch {} query index {}", batchCount, i); + String cql = format(readCQL.get(i), qualifiedAccordTableName); + testSplitAndRetry(ImmutableList.of(cql), this::loadOverlapData, ImmutableList.of(validation.get(i)), expectRetry); + tearDown(); + setup(); + afterEach(); + } + else + { + logger.info("Skipping query from batch {} query index {}", batchCount, i); + } + } + } + else + { + readCQL = readCQL.stream().map(cql -> format(cql, qualifiedAccordTableName)).collect(toImmutableList()); + testSplitAndRetry(readCQL, this::loadOverlapData, validation, expectRetry); + tearDown(); + setup(); + afterEach(); + } + } + + private void testSplitAndRetry(String readCQL, ThrowingRunnable load, Consumer validation) throws Throwable + { + testSplitAndRetry(ImmutableList.of(readCQL), load, ImmutableList.of(validation),true); + } + + private void testSplitAndRetry(List readCQL, ThrowingRunnable load, List> validation, boolean expectRetry) throws Throwable + { + test(createTables(TABLE_FMT, qualifiedAccordTableName), + cluster -> { + load.run(); + // Node 3 is always the out of sync node + IInvokableInstance outOfSyncInstance = setUpOutOfSyncNode(cluster); + ICoordinator coordinator = outOfSyncInstance.coordinator(); + int startMigrationRejectCount = getAccordReadMigrationRejects(3); + int startRetryCount = getReadRetryOnDifferentSystemCount(outOfSyncInstance); + int startRejectedCount = getReadsRejectedOnWrongSystemCount(); + logger.info("Executing reads " + readCQL + " expect retry " + expectRetry); + List> results = readCQL.stream() + .map(read -> coordinator.asyncExecuteWithResult(read, ALL)) + .collect(toImmutableList()); + + if (migrateAwayFromAccord && expectRetry) + { + int expectedTransactions = readCQL.size(); + // 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(() -> { + logger.info("Coordinating {}", AccordService.instance().node().coordinating()); + return AccordService.instance().node().coordinating().size() == expectedTransactions; + })); + + logger.info("Accord node is now coordinating something, unpausing so it can continue to execute"); + } + + if (!migrateAwayFromAccord && expectRetry) + spinAssertEquals(readCQL.size() * 2, 10, () -> getReadsRejectedOnWrongSystemCount() - startRejectedCount); + + // Accord can't finish the transaction without unpausing + if (expectRetry || migrateAwayFromAccord) + { + logger.info("Unpausing out of sync instance before waiting on result"); + // Testing read coordination retry loop let coordinator get up to date and retry + unpauseEnactment(outOfSyncInstance); + } + + try + { + for (int i = 0; i < results.size(); i++) + { + SimpleQueryResult result = results.get(i).get(); + logger.info("Result for: " + readCQL.get(i)); + logger.info(result.toString()); + try + { + validation.get(i).accept(result); + } + catch (Throwable t) + { + logger.info("Query index {} failed", i); + throw t; + } + } + } + catch (ExecutionException e) + { + throw e; + } + + if (!expectRetry) + { + logger.info("Unpausing out of sync instance after waiting on result"); + // Testing read coordination retry loop let coordinator get up to date and retry + unpauseEnactment(outOfSyncInstance); + } + + int endRetryCount = getReadRetryOnDifferentSystemCount(outOfSyncInstance); + int endRejectedCount = getReadsRejectedOnWrongSystemCount(); + int endMigrationRejects = getAccordReadMigrationRejects(3); + if (expectRetry) + { + if (migrateAwayFromAccord) + { + assertEquals(readCQL.size(), endRetryCount - startRetryCount); + assertEquals(readCQL.size(), endMigrationRejects - startMigrationRejectCount); + } + else + { + assertEquals(1 * readCQL.size(), endRetryCount - startRetryCount); + // Expect only two nodes to reject since they enacted the new epoch + assertEquals(2 * readCQL.size(), endRejectedCount - startRejectedCount); + } + } + else + { + assertEquals(0, endRetryCount - startRetryCount); + assertEquals(0, endRejectedCount - startRejectedCount); + } + }); + } + + /* + * Set up 3 to be behind and unaware of the migration having progressed to the point where reads need to + * be on a different system 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); + + long afterAlter = getNextEpoch(i1).getEpoch(); + logger.info("Epoch after alter {}", afterAlter); + if (migrateAwayFromAccord) + alterTableTransactionalMode(TransactionalMode.off, TransactionalMigrationFromMode.full); + else + alterTableTransactionalMode(TransactionalMode.full); + Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterAlter)))), 10); + + long afterMigrationStart = getNextEpoch(i1).getEpoch(); + logger.info("Epoch after migration start {}", afterMigrationStart); + long waitFori1Andi2ToEnact = afterMigrationStart; + // Migrating away from Accord need i3 to pause before enacting + if (migrateAwayFromAccord) + pauseBeforeEnacting(i3, Epoch.create(afterMigrationStart)); + // Reads are allowed until Accord thinks it owns the range and can start doing async commit and ignoring consistency levels + nodetool(coordinator, "consensus_admin", "begin-migration", "-st", migratingRange.left.toString(), "-et", migratingRange.right.toString(), KEYSPACE, accordTableName); + + if (!migrateAwayFromAccord) + { + // Migration to Accord does not have Accord read until the migration has completed a data repair and then an Accord repair + Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterMigrationStart)))), 10); + + long afterRepair = getNextEpoch(i1).getEpoch(); + logger.info("Epoch after repair {}", afterRepair); + // First repair only does the data and allows Accord to read, but doesn't require reads to be done through Accord + nodetool(i2, "repair", "-skip-paxos", "-skip-accord", "-st", migratingRange.left.toString(), "-et", migratingRange.right.toString(), KEYSPACE, accordTableName); + Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterRepair)))), 10); + + long afterRepairCompletionHandler = getNextEpoch(i1).getEpoch(); + logger.info("Epoch after repair completion handler {}", afterRepairCompletionHandler); + waitFori1Andi2ToEnact = afterRepairCompletionHandler; + // Node 3 will coordinate the query and not be aware that the migration has begun + pauseBeforeEnacting(i3, Epoch.create(afterRepairCompletionHandler)); + + // Unfortunately can't run real repair because it can't complete with i3 not responding because it's stuck waiting + // on TCM so fake the completion of the repair by invoking the completion handler directly + String keyspace = KEYSPACE; + String table = accordTableName; + long migratingTokenStart = migratingRange.left.getLongValue(); + long migratingTokenEnd = migratingRange.right.getLongValue(); + Future result = SHARED_CLUSTER.get(1).asyncRunsOnInstance(() -> + { + Epoch startEpoch = ClusterMetadata.current().epoch; + TableId tableId = Schema.instance.getTableMetadata(keyspace, table).id; + List> ranges = ImmutableList.of(new Range<>(new LongToken(migratingTokenStart), new LongToken(migratingTokenEnd))); + RepairJobDesc desc = new RepairJobDesc(null, null, keyspace, table, ranges); + TokenRange range = TokenRange.create(new TokenKey(tableId, new LongToken(migratingTokenStart)), new TokenKey(tableId, new LongToken(migratingTokenEnd))); + Ranges accordRanges = Ranges.of(range); + ConsensusMigrationRepairResult repairResult = ConsensusMigrationRepairResult.fromRepair(startEpoch, accordRanges, true, true, true, false); + ConsensusTableMigration.completedRepairJobHandler.onSuccess(new RepairResult(desc, null, repairResult)); + }).call(); + result.get(); + } + + long waitFori1Andi2ToEnactFinal = waitFori1Andi2ToEnact; + // Make sure 1 and 2 are up to date + for (int i = 1; i < 3; i++) + { + int instanceIndex = i; + Util.spinUntilTrue(() -> cluster.get(instanceIndex).callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(waitFori1Andi2ToEnactFinal))), 10); + } + + return i3; + } + + private static String insertCQL(String qualifiedTableName, ByteBuffer pkey, int clustering, int value) + { + return format("INSERT INTO %s ( pk, c, v ) VALUES ( 0x%s, %d, %d )", qualifiedTableName, bytesToHex(pkey), clustering, value); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationTest.java index a7a2d406d4..ffb1c4546b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationTest.java @@ -104,8 +104,8 @@ import static org.apache.cassandra.Util.dk; import static org.apache.cassandra.Util.spinUntilSuccess; 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; import static org.apache.cassandra.dht.NormalizedRanges.normalizedRanges; +import static org.apache.cassandra.dht.Range.normalize; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ANY; import static org.apache.cassandra.distributed.api.ConsistencyLevel.SERIAL; @@ -330,14 +330,14 @@ public class AccordMigrationTest extends AccordTestBase int startingWriteCount = getAccordWriteCount(coordinatorIndex); int startingCasWriteCount = getCasWriteCount(coordinatorIndex); int startingKeyMigrationCount = getKeyMigrationCount(coordinatorIndex); - int startingMigrationRejectsCount = getAccordMigrationRejects(coordinatorIndex); + int startingMigrationRejectsCount = getAccordWriteMigrationRejects(coordinatorIndex); int startingSkippedReadsCount = getAccordMigrationSkippedReads(); query.accept(key); validateKeyMigrations(expectedKeyMigrations); assertEquals("Accord writes", expectedAccordWriteCount, getAccordWriteCount(coordinatorIndex) - startingWriteCount); assertEquals("CAS writes", expectedCasWriteCount, getCasWriteCount(coordinatorIndex) - startingCasWriteCount); assertEquals("Key Migrations", expectedKeyMigrationCount, getKeyMigrationCount(coordinatorIndex) - startingKeyMigrationCount); - assertEquals("Accord migration rejects", expectedMigrationRejects, getAccordMigrationRejects(coordinatorIndex) - startingMigrationRejectsCount); + assertEquals("Accord migration rejects", expectedMigrationRejects, getAccordWriteMigrationRejects(coordinatorIndex) - startingMigrationRejectsCount); assertEquals("Accord skipped reads", expectedSkippedReads, getAccordMigrationSkippedReads() - startingSkippedReadsCount); } @@ -421,7 +421,7 @@ public class AccordMigrationTest extends AccordTestBase assertTargetAccordWrite(runCasNoApply, 1, migratingKey, expectedKeyMigrations, 0, 1, 0, 0, 0); // Mark ranges migrating and check migration state is correct - nodetool(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), "-tp", "accord", KEYSPACE, tableName); + nodetool(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), KEYSPACE, tableName); assertMigrationState(tableName, ConsensusMigrationTarget.accord, emptyList(), migratingRanges, migratingRanges, 1); // Should be routed directly to Accord, and perform key migration, as well as key migration read in Accord @@ -476,7 +476,7 @@ public class AccordMigrationTest extends AccordTestBase // Pivot to testing repair with a subrange of the migrating range as well as key migration // Will use the unmigrated range between lowerMidToken and midToken - nodetool(coordinator, "consensus_admin", "begin-migration", "-st", lowerMidToken.toString(), "-et", midToken.toString(), "-tp", "accord", KEYSPACE, tableName); + nodetool(coordinator, "consensus_admin", "begin-migration", "-st", lowerMidToken.toString(), "-et", midToken.toString(), KEYSPACE, tableName); // Generate several keys to test with instead of resetting key state Iterator testingKeys = getKeysBetweenTokens(lowerMidToken, midToken); @@ -502,7 +502,7 @@ public class AccordMigrationTest extends AccordTestBase Integer clusteringValue = CLUSTERING_VALUE; String mutationTableName = accordTableName; Consumer makeCASApply = key -> cluster.forEach(instance -> instance.runOnInstance(() -> { - SimpleBuilder mutationBuilder = Mutation.simpleBuilder(keyspace, dk(key)).allowPotentialTransactionConflicts(); + SimpleBuilder mutationBuilder = Mutation.simpleBuilder(keyspace, dk(key)).allowPotentialTxnConflicts(); mutationBuilder.update(mutationTableName).row(clusteringValue).add("v", 42); Mutation m = mutationBuilder.build(); m.applyUnsafe(); @@ -578,7 +578,7 @@ 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, accordTableName); + nodetool(coordinator, "consensus_admin", "begin-migration", "-st", String.valueOf(Long.MIN_VALUE + 1), "-et", String.valueOf(Long.MIN_VALUE), KEYSPACE, accordTableName); assertMigrationState(accordTableName, ConsensusMigrationTarget.accord, emptyList(), migratingRanges, migratingRanges, 1); // Need to repair so key migration can occur for (int i = 1; i <= 3; i++) @@ -625,7 +625,7 @@ public class AccordMigrationTest extends AccordTestBase assertTransactionalModes(TransactionalMode.mixed_reads, TransactionalMigrationFromMode.off); // Mark a subrange as migrating and finish migrating half of it - nodetool(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), "-tp", "accord", KEYSPACE, tableName); + nodetool(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), KEYSPACE, tableName); nodetool(coordinator, "consensus_admin", "finish-migration", "-st", midToken.toString(), "-et", "3074457345618258601"); nodetool(coordinator, "consensus_admin", "finish-migration", "-st", "3074457345618258601", "-et", upperMidToken.toString()); Range accordMigratedRange = new Range(midToken, upperMidToken); diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationRaceTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationWriteRaceTestBase.java similarity index 83% rename from test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationRaceTestBase.java rename to test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationWriteRaceTestBase.java index b864515bc0..593b1296ce 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationRaceTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordMigrationWriteRaceTestBase.java @@ -20,6 +20,8 @@ package org.apache.cassandra.distributed.test.accord; import java.io.IOException; import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.Callable; @@ -30,6 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import org.junit.After; @@ -40,10 +43,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import accord.api.RoutingKey; +import accord.coordinate.Outcome; import accord.messages.PreAccept; import accord.primitives.PartialKeyRoute; +import accord.primitives.Ranges; import accord.primitives.Routable.Domain; import accord.primitives.Route; +import accord.primitives.TxnId; +import accord.utils.async.AsyncResult; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.Util; import org.apache.cassandra.batchlog.BatchlogManager; @@ -53,9 +60,10 @@ 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.Murmur3Partitioner.LongToken; +import org.apache.cassandra.dht.Range; 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; @@ -76,12 +84,20 @@ 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.repair.RepairJobDesc; +import org.apache.cassandra.repair.RepairResult; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableId; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.accord.AccordService; +import org.apache.cassandra.service.accord.TokenRange; import org.apache.cassandra.service.accord.api.AccordRoutingKey; +import org.apache.cassandra.service.accord.api.AccordRoutingKey.TokenKey; import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.service.consensus.migration.ConsensusKeyMigrationState; +import org.apache.cassandra.service.consensus.migration.ConsensusMigrationRepairResult; import org.apache.cassandra.service.consensus.migration.ConsensusRequestRouter; +import org.apache.cassandra.service.consensus.migration.ConsensusTableMigration; import org.apache.cassandra.tcm.ClusterMetadata; import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.utils.FBUtilities; @@ -96,14 +112,13 @@ 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.test.accord.AccordMigrationWriteRaceTestBase.Scenario.BATCHLOG_FAILED_ROUTING_THEN_HINT; +import static org.apache.cassandra.distributed.test.accord.AccordMigrationWriteRaceTestBase.Scenario.BATCHLOG_FAILED_TIMEOUT_THEN_HINT; +import static org.apache.cassandra.distributed.test.accord.AccordMigrationWriteRaceTestBase.Scenario.BATCHLOG_SUCCESSFUL_ROUTING; +import static org.apache.cassandra.distributed.test.accord.AccordMigrationWriteRaceTestBase.Scenario.HINT; +import static org.apache.cassandra.distributed.test.accord.AccordMigrationWriteRaceTestBase.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; @@ -114,10 +129,12 @@ 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. + * TODO (required): Accord TopologyMismatch means we aren't testing routing failure checks in TxnQuery migrating away from Accord in some test scenarios + * but maybe this doesn't matter becuase we do check the routing */ -public abstract class AccordMigrationRaceTestBase extends AccordTestBase +public abstract class AccordMigrationWriteRaceTestBase extends AccordTestBase { - private static final Logger logger = LoggerFactory.getLogger(AccordMigrationRaceTestBase.class); + private static final Logger logger = LoggerFactory.getLogger(AccordMigrationWriteRaceTestBase.class); private static final int CLUSTERING_VALUE = 1; @@ -196,7 +213,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase private final boolean migrateAwayFromAccord; - protected AccordMigrationRaceTestBase() + protected AccordMigrationWriteRaceTestBase() { this.migrateAwayFromAccord = migratingAwayFromAccord(); } @@ -243,6 +260,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase @After public void tearDown() throws Exception { + super.tearDown(); messageSink.reset(); forEach(() -> { BatchlogManager.instance.resumeReplay(); @@ -440,12 +458,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase 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(); + IInvokableInstance outOfSyncInstance = setUpOutOfSyncNode(cluster, scenario); // Need to be able to block writing to the test keyspace forcing batchlog replay // without also failing writes to the batch log @@ -485,7 +498,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase int coordinatorIndex = scenario.initiallyEnableBatchlogReplay ? 2 : 3; IInvokableInstance instance = cluster.get(coordinatorIndex); ICoordinator coordinator = instance.coordinator(); - int startRetryCount = getRetryOnDifferentSystemCount(coordinatorIndex); + int startRetryCount = getWriteRetryOnDifferentSystemCount(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"); @@ -551,8 +564,15 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase // 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("Found transaction, unpausing"); + Util.spinUntilTrue(() -> { + Map> txns = AccordService.instance().node().coordinating(); + if (!txns.isEmpty()) + { + logger.info("Found txns {}", txns); + return true; + } + return false; + }, 20); TestChangeListener.instance.unpause(); unpaused.trySuccess(null); } @@ -585,8 +605,12 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase { // 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"); + Util.spinUntilTrue(() -> outOfSyncInstance.callOnInstance(() -> { + Map> coordinating = AccordService.instance().node().coordinating(); + if (!coordinating.isEmpty()) + logger.info("Accord coordinating: " + coordinating); + return !coordinating.isEmpty(); + }), 20); try { validation.accept(cluster); @@ -622,7 +646,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase if (!migrateAwayFromAccord) { - int endRetryCount = getRetryOnDifferentSystemCount(coordinatorIndex); + int endRetryCount = getWriteRetryOnDifferentSystemCount(coordinatorIndex); int endRejectedCount = getMutationsRejectedOnWrongSystemCount(); assertEquals(1, endRetryCount - startRetryCount); // Expect only two nodes to reject since they enacted the new epoch @@ -642,6 +666,7 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase long startingAccordPreempted = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.preempted.getCount()); long startingAccordMigrationRejects = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.accordMigrationRejects.getCount()); long startingHintTimeouts = outOfSyncInstance.callOnInstance(() -> HintsServiceMetrics.hintsTimedOut.getCount()); + long startingTopologyMismatches = outOfSyncInstance.callOnInstance(() -> ClientRequestsMetricsHolder.accordWriteMetrics.topologyMismatches.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 @@ -651,8 +676,8 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase { Callable test = () -> outOfSyncInstance.callOnInstance(() -> { HintsService.instance.flushAndFsyncBlockingly(); - 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; + logger.info("startingAccordTimeouts {}, startingAccordPreempts {}, startingAccordMigrationRejects {}, startingHintTimeouts {}, startingTopoloygMismatches {}, accord timeouts {}, accordPreempts {}, accordMigrationRejects {}, hint timeouts {}, topologyMismatches {}", startingAccordTimeouts, startingAccordPreempted, startingAccordMigrationRejects, startingHintTimeouts, startingTopologyMismatches, accordMetrics.timeouts.getCount(), accordMetrics.preempted.getCount(), accordMetrics.accordMigrationRejects.getCount(), HintsServiceMetrics.hintsTimedOut.getCount(), accordMetrics.topologyMismatches.getCount()); return accordMetrics.timeouts.getCount() >= (startingAccordTimeouts + 1) && HintsServiceMetrics.hintsTimedOut.getCount() >= (startingHintTimeouts + 1); }); Util.spinUntilTrue(test, 40); @@ -677,45 +702,50 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase }, 20); } // After this hints should deliver and the final validation should succeed - // if we don't unpause enactment + // if we unpause enactment unpauseEnactment(outOfSyncInstance); + long currentEpoch = SHARED_CLUSTER.get(1).callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch()); + logger.info("Spinning waiting for out of sync instance to catch up"); + Util.spinUntilTrue(() -> outOfSyncInstance.callOnInstance(() -> ClusterMetadata.current().epoch.getEpoch() == currentEpoch)); + logger.info("Out of sync instance caught up"); } // Accord commit is async and might take a while, but the data should end up as expected - Util.spinUntilSuccess(() -> validation.accept(cluster)); + Util.spinUntilSuccess(() -> validation.accept(cluster), 20); }); } /* * Set up 3 to be behind and unaware of the migration while 1 and 2 are aware */ - private IInvokableInstance setUpOutOfSyncNode(Cluster cluster) throws Throwable + private IInvokableInstance setUpOutOfSyncNode(Cluster cluster, Scenario scenario) throws Throwable { IInvokableInstance i1 = cluster.get(1); - IInvokableInstance i2 = cluster.get(2); IInvokableInstance i3 = cluster.get(3); + long afterAlterEpoch = getNextEpoch(i1).getEpoch(); 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); + Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterAlterEpoch)))), 10); - ListenableFuture result = nodetoolAsync(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), "-tp", "accord", KEYSPACE, accordTableName); + long migratingEpoch = getNextEpoch(i1).getEpoch(); + logger.info("Epoch for migrating to Accord is {}", migratingEpoch); + // Node 3 will coordinate the query and not be aware that the migration has begun + Callable pausedBeforeEnacting = pauseBeforeEnacting(i3, migratingEpoch); + ListenableFuture result = nodetoolAsync(coordinator, "consensus_admin", "begin-migration", "-st", midToken.toString(), "-et", maxToken.toString(), KEYSPACE, accordTableName); + // Node 2 coordinates in the batch log case so it has to have caught up + long afterBeginMigrationEpochFinal = migratingEpoch; + Util.spinUntilTrue(() -> cluster.get(2).callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterBeginMigrationEpochFinal))), 10); 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 + long migratingEpochFinal = migratingEpoch; + Util.spinUntilTrue(() -> cluster.stream().allMatch(instance -> instance.callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(migratingEpochFinal)))), 10); + migratingEpoch = getNextEpoch(i1).getEpoch(); + logger.info("Epoch for migration away from Accord is {}", migratingEpoch); + pausedBeforeEnacting = pauseBeforeEnacting(i3, migratingEpoch); + // In the reverse direction doing the alter automatically reverses the migration without a need to call begin migration on any ranges result = alterTableTransactionalModeAsync(TransactionalMode.off); } @@ -740,12 +770,47 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase } throw t; } - i2PausedAfterEnacting.call(); - // Unpause on 1 and 2 where we want them aware of the migration - unpauseEnactment(i1); - unpauseEnactment(i2); + // Make sure 1 and 2 are up to date + for (int i = 1; i < 3; i++) + { + int instanceIndex = i; + long migratingEpochFinal = migratingEpoch; + Util.spinUntilTrue(() -> cluster.get(instanceIndex).callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(migratingEpochFinal))), 10); + } // nodetool should be able to complete now result.get(); + + // Need to complete the migration for its eventual execution in the next epoch to be discovered to be misrouted + // now that we continue to write through Accord during migration away from Accord + // Faking the completed repair is the only way to get it in a state where two coordinators know about the new + // epoch and one doesn't + if (migrateAwayFromAccord && scenario.deliversViaHint && scenario.passesThroughBatchlog) //&& scenario != BATCHLOG_FAILED_ROUTING_THEN_HINT) + { + String keyspace = KEYSPACE; + String table = accordTableName; + long midTokenLong = midToken.getLongValue(); + long maxTokenLong = maxToken.getLongValue(); + long afterReverseMigrationEpoch = SHARED_CLUSTER.get(1).callOnInstance(() -> + { + Epoch startEpoch = ClusterMetadata.current().epoch; + Epoch epochAfterRepair = startEpoch.nextEpoch(); + TableId tableId = Schema.instance.getTableMetadata(keyspace, table).id; + List> ranges = ImmutableList.of(new Range<>(new LongToken(midTokenLong), new LongToken(maxTokenLong))); + RepairJobDesc desc = new RepairJobDesc(null, null, keyspace, table, ranges); + TokenRange range = TokenRange.create(new TokenKey(tableId, new LongToken(midTokenLong)), new TokenKey(tableId, new LongToken(maxTokenLong))); + Ranges accordRanges = Ranges.of(range); + ConsensusMigrationRepairResult repairResult = ConsensusMigrationRepairResult.fromRepair(startEpoch, accordRanges, true, true, true, false); + ConsensusTableMigration.completedRepairJobHandler.onSuccess(new RepairResult(desc, null, repairResult)); + return epochAfterRepair.getEpoch(); + }); + // Make sure 1 and 2 are up to date and know the reverse migration happens + for (int i = 1; i < 3; i++) + { + int instanceIndex = i; + Util.spinUntilTrue(() -> cluster.get(instanceIndex).callOnInstance(() -> ClusterMetadata.current().epoch.equals(Epoch.create(afterReverseMigrationEpoch))), 10); + } + } + return i3; } @@ -767,12 +832,4 @@ public abstract class AccordMigrationRaceTestBase extends AccordTestBase { 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"); - } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordReadInteroperabilityTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordReadInteroperabilityTest.java new file mode 100644 index 0000000000..a66c79ad88 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordReadInteroperabilityTest.java @@ -0,0 +1,180 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; + +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.consensus.TransactionalMode; +import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class AccordReadInteroperabilityTest extends AccordTestBase +{ + private static final Logger logger = LoggerFactory.getLogger(AccordInteroperabilityTest.class); + + @Nonnull + private final TransactionalMode mode; + + private final boolean migrated; + + public AccordReadInteroperabilityTest(@Nonnull TransactionalMode mode, boolean migrated) + { + this.mode = mode; + this.migrated = migrated; + } + + @Parameterized.Parameters(name = "transactionalMode={0}, migrated={1}") + public static Collection data() { + List tests = new ArrayList<>(TransactionalMode.values().length * 2); + for (TransactionalMode mode : TransactionalMode.values()) + { + tests.add(new Object[]{ mode, true }); + tests.add(new Object[]{ mode, false }); + } + return tests; + } + + @Override + protected Logger logger() + { + return logger; + } + + @BeforeClass + public static void setupClass() throws IOException + { + AccordTestBase.setupCluster(builder -> builder.withConfig(config -> config.set("accord.range_migration", "auto") + .set("paxos_variant", "v2")), + 3); + } + + @After + public void tearDown() + { + SHARED_CLUSTER.setMessageSink(null); + } + + + private String testTransactionSelect() + { + return "BEGIN TRANSACTION\n" + + " SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 0;\n" + + "COMMIT TRANSACTION"; + } + + private String testSelect() + { + return "SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 0"; + } + + private String testRangeSelect() + { + return "SELECT * FROM " + qualifiedAccordTableName + " WHERE token(k) > token(0)"; + } + + @Test + public void testTransactionStatementReadIsAtQuorum() throws Throwable + { + testReadIsAtQuorum(testTransactionSelect()); + } + + @Test + public void testNonSerialReadIsAtQuorum() throws Throwable + { + testReadIsAtQuorum(testSelect()); + } + + @Test + public void testSerialReadIsAtQuorum() throws Throwable + { + testReadIsAtQuorum(testSelect(), ConsistencyLevel.SERIAL); + } + + @Test + public void testRangeReadIsAtQuorum() throws Throwable + { + testReadIsAtQuorum(testRangeSelect()); + } + + private void testReadIsAtQuorum(String query) throws Throwable + { + testReadIsAtQuorum(query, ConsistencyLevel.QUORUM); + } + + private void testReadIsAtQuorum(String query, org.apache.cassandra.distributed.api.ConsistencyLevel cl) throws Throwable + { + // Transaction statement doesn't work during migration + if (query.equals(testTransactionSelect()) && !migrated) + return; + test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY(k, c))" + (migrated ? " WITH " + transactionalMode.asCqlParam() : ""), + cluster -> { + SHARED_CLUSTER.setMessageSink(new MessageCountingSink(SHARED_CLUSTER)); + if (!migrated) + { + String alterCQL = "ALTER TABLE " + qualifiedAccordTableName + " WITH " + transactionalMode.asCqlParam(); + if (transactionalMode == TransactionalMode.off) + alterCQL = alterCQL + " AND " + TransactionalMigrationFromMode.full.asCqlParam(); + cluster.coordinator(1).execute(alterCQL, ConsistencyLevel.ALL); + if (transactionalMode == TransactionalMode.off) + { + nodetool(cluster.coordinator(1), "repair", "-skip-paxos", KEYSPACE, accordTableName); + } + else + { + nodetool(cluster.coordinator(1), "repair", "-skip-paxos", "-skip-accord", KEYSPACE, accordTableName); + nodetool(cluster.coordinator(1), "repair", "-skip-accord", KEYSPACE, accordTableName); + } + } + cluster.coordinator(1).execute(query, cl); + // Transactional modes that write through Accord never have a point where they need to run interop reads + // they go straight from not being able to read to being able to read from a single replica + if (!transactionalMode.ignoresSuppliedReadCL() && !transactionalMode.nonSerialWritesThroughAccord) + { + assertEquals(2, messageCount(Verb.ACCORD_INTEROP_STABLE_THEN_READ_REQ)); + assertEquals(2, messageCount(Verb.ACCORD_INTEROP_READ_RSP)); + } + else + { + // Tricky to check for regular commit because a lot of background Accord things create commits + assertEquals(0, messageCount(Verb.ACCORD_INTEROP_STABLE_THEN_READ_REQ)); + assertEquals(0, messageCount(Verb.ACCORD_INTEROP_READ_REQ)); + assertEquals(0, messageCount(Verb.ACCORD_INTEROP_READ_RSP)); + // Durability scheduling creates a lot of background commits that generate read responses + assertTrue(messageCount(Verb.ACCORD_READ_RSP) > 0); + } + }); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordTestBase.java index e61ff43766..a63c79fae6 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordTestBase.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordTestBase.java @@ -19,6 +19,7 @@ package org.apache.cassandra.distributed.test.accord; import java.io.IOException; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; @@ -31,16 +32,11 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; - -import org.apache.cassandra.cql3.CQLStatement; -import org.apache.cassandra.cql3.QueryProcessor; -import org.apache.cassandra.distributed.shared.ClusterUtils; -import org.apache.cassandra.schema.Schema; -import org.apache.cassandra.schema.TableMetadata; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; @@ -62,6 +58,8 @@ 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.CQLStatement; +import org.apache.cassandra.cql3.QueryProcessor; import org.apache.cassandra.cql3.statements.ModificationStatement; import org.apache.cassandra.cql3.statements.TransactionStatement; import org.apache.cassandra.cql3.transactions.ReferenceValue; @@ -73,12 +71,14 @@ import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.Cluster.Builder; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.api.IInstance; 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.ClusterUtils; import org.apache.cassandra.distributed.shared.Metrics; import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.distributed.util.QueryResultUtil; @@ -90,17 +90,21 @@ 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.service.accord.api.AccordRoutingKey; +import org.apache.cassandra.schema.Schema; +import org.apache.cassandra.schema.TableMetadata; import org.apache.cassandra.service.ClientState; +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.service.consensus.migration.TransactionalMigrationFromMode; 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 com.google.common.base.Preconditions.checkState; import static java.lang.String.format; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.apache.cassandra.db.SystemKeyspace.CONSENSUS_MIGRATION_STATE; @@ -250,7 +254,12 @@ 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) + protected static int getReadRetryOnDifferentSystemCount(IInstance instance) + { + return Ints.checkedCast(instance.metrics().getCounter("org.apache.cassandra.metrics.ClientRequest.RetryDifferentSystem.Read")); + } + + protected static int getWriteRetryOnDifferentSystemCount(int coordinatorIndex) { return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.RetryDifferentSystem.Write")); } @@ -263,6 +272,14 @@ public abstract class AccordTestBase extends TestBaseImpl return Ints.checkedCast(sum); } + protected int getReadsRejectedOnWrongSystemCount() + { + long sum = 0; + for (IInvokableInstance instance : SHARED_CLUSTER) + sum += instance.metrics().getCounter("org.apache.cassandra.metrics.Table.ReadsRejectedOnWrongSystem." + 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")); @@ -288,7 +305,12 @@ public abstract class AccordTestBase extends TestBaseImpl return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.Latency.AccordRead")); } - protected static int getAccordMigrationRejects(int coordinatorIndex) + protected static int getAccordReadMigrationRejects(int coordinatorIndex) + { + return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.AccordMigrationRejects.AccordRead")); + } + + protected static int getAccordWriteMigrationRejects(int coordinatorIndex) { return Ints.checkedCast(getMetrics(coordinatorIndex).getCounter("org.apache.cassandra.metrics.ClientRequest.AccordMigrationRejects.AccordWrite")); } @@ -559,6 +581,7 @@ public abstract class AccordTestBase extends TestBaseImpl { public static void install(ClassLoader classLoader, Integer num) { + checkState(Arrays.asList(ModificationStatement.class.getDeclaredMethods()).stream().map(Method::getName).anyMatch(m -> m.equals("readRequiredLists"))); new ByteBuddy().rebase(ModificationStatement.class) .method(named("readRequiredLists")) .intercept(MethodDelegation.to(EnforceUpdateDoesNotPerformRead.class)) @@ -585,7 +608,12 @@ public abstract class AccordTestBase extends TestBaseImpl protected void alterTableTransactionalMode(TransactionalMode mode) { - SHARED_CLUSTER.schemaChange(format("ALTER TABLE %s WITH %s", qualifiedAccordTableName, mode.asCqlParam())); + alterTableTransactionalMode(mode, null); + } + + protected void alterTableTransactionalMode(TransactionalMode mode, @Nullable TransactionalMigrationFromMode from) + { + SHARED_CLUSTER.schemaChange(format("ALTER TABLE %s WITH %s" + (from == null ? "" : " AND %s"), qualifiedAccordTableName, mode.asCqlParam(), from == null ? null : from.asCqlParam())); } protected static void pauseHints() diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/AccordWriteInteroperabilityTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordWriteInteroperabilityTest.java new file mode 100644 index 0000000000..944a02face --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/AccordWriteInteroperabilityTest.java @@ -0,0 +1,225 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; + +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.db.ColumnFamilyStore; +import org.apache.cassandra.db.DataRange; +import org.apache.cassandra.db.filter.ColumnFilter; +import org.apache.cassandra.db.memtable.Memtable; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.db.rows.Row; +import org.apache.cassandra.db.rows.UnfilteredRowIterator; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.shared.InstanceClassLoader; +import org.apache.cassandra.io.sstable.SSTableReadsListener; +import org.apache.cassandra.net.Verb; +import org.apache.cassandra.service.consensus.TransactionalMode; + +import static com.google.common.base.Throwables.getStackTraceAsString; +import static org.apache.cassandra.Util.dk; +import static org.apache.cassandra.Util.spinUntilTrue; +import static org.apache.commons.collections.ListUtils.synchronizedList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(Parameterized.class) +public class AccordWriteInteroperabilityTest extends AccordTestBase +{ + private static final Logger logger = LoggerFactory.getLogger(AccordInteroperabilityTest.class); + + @Nonnull + private final TransactionalMode mode; + + private final boolean migrated; + + public AccordWriteInteroperabilityTest(@Nonnull TransactionalMode mode, boolean migrated) + { + this.mode = mode; + this.migrated = migrated; + } + + @Parameterized.Parameters(name = "transactionalMode={0}, migrated={1}") + public static Collection data() { + List tests = new ArrayList<>(TransactionalMode.values().length * 2); + for (TransactionalMode mode : TransactionalMode.values()) + { + if (mode.accordIsEnabled) + { + tests.add(new Object[]{ mode, true }); + tests.add(new Object[]{ mode, false }); + } + } + return tests; + } + + @Override + protected Logger logger() + { + return logger; + } + + @BeforeClass + public static void setupClass() throws IOException + { + AccordTestBase.setupCluster(builder -> builder.withConfig(config -> config.set("accord.range_migration", "auto") + .set("paxos_variant", "v2")), + 3); + } + + @After + public void tearDown() + { + SHARED_CLUSTER.setMessageSink(null); + } + + + private String testTransactionInsert() + { + return "BEGIN TRANSACTION\n" + + " INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 2, 3);\n" + + "COMMIT TRANSACTION"; + } + + private String testInsert() + { + return "INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 2, 3)"; + } + + private String testBatchInsert() + { + return "BEGIN BATCH\n" + + "INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (1, 2, 3);\n" + + "INSERT INTO " + qualifiedAccordTableName + " (k, c, v) VALUES (42, 43, 44);\n" + + "APPLY BATCH"; + } + + @Test + public void testTransactionStatementApplyIsInteropApply() throws Throwable + { + testApplyIsInteropApply(testTransactionInsert()); + } + + @Test + public void testNonSerialApplyIsInteropApply() throws Throwable + { + testApplyIsInteropApply(testInsert()); + } + + @Test + public void testBatchInsertApplyIsInteropApply() throws Throwable + { + testApplyIsInteropApply(testBatchInsert()); + } + + private void testApplyIsInteropApply(String query) throws Throwable + { + test("CREATE TABLE " + qualifiedAccordTableName + " (k int, c int, v int, PRIMARY KEY(k, c))" + (migrated ? " WITH " + transactionalMode.asCqlParam() : ""), + cluster -> { + MessageCountingSink messageCountingSink = new MessageCountingSink(SHARED_CLUSTER); + List failures = synchronizedList(new ArrayList<>()); + // Verify that the apply response is only sent after the row has been inserted + // TODO (required): Need to delay mutation stage/mutation to ensure this has time to catch it + SHARED_CLUSTER.setMessageSink((to, message) -> { + try + { + if (message.verb() == Verb.ACCORD_APPLY_RSP.id) + { + // It can be async if it's migrated + if (migrated) + return; + int nodeIndex = ((InstanceClassLoader)ClassLoader.getSystemClassLoader()).getInstanceId(); + try + { + String keyspace = KEYSPACE; + String tableName = accordTableName; + SHARED_CLUSTER.get(nodeIndex).runOnInstance(() -> { + ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(keyspace, tableName); + Memtable memtable = cfs.getCurrentMemtable(); + int expectedPartitions = query.startsWith("BEGIN BATCH") ? 2 : 1; + assertEquals(expectedPartitions, memtable.partitionCount()); + UnfilteredPartitionIterator partitions = memtable.partitionIterator(ColumnFilter.all(cfs.metadata()), DataRange.allData(cfs.getPartitioner()), SSTableReadsListener.NOOP_LISTENER); + assertTrue(partitions.hasNext()); + for (int i = 0; i < expectedPartitions; i++) + { + UnfilteredRowIterator rows = partitions.next(); + assertTrue(rows.partitionKey().equals(dk(42)) || rows.partitionKey().equals(dk(1))); + assertTrue(rows.hasNext()); + Row row = (Row)rows.next(); + assertFalse(rows.hasNext()); + } + assertFalse(partitions.hasNext()); + }); + } + catch (Throwable t) + { + failures.add(getStackTraceAsString(t)); + } + } + } + finally + { + messageCountingSink.accept(to, message); + } + }); + + if (!migrated) + { + cluster.coordinator(1).execute("ALTER TABLE " + qualifiedAccordTableName + " WITH " + transactionalMode.asCqlParam(), ConsistencyLevel.ALL); + nodetool(cluster.coordinator(1), "repair", "-skip-paxos", "-skip-accord", KEYSPACE, accordTableName); + } + + String finalQuery = query; + org.apache.cassandra.distributed.api.ConsistencyLevel consistencyLevel = org.apache.cassandra.distributed.api.ConsistencyLevel.QUORUM; + // Need to switch to CAS for it to run through Accord at all + if (!transactionalMode.nonSerialWritesThroughAccord && !query.startsWith("BEGIN TRANSACTION")) + { + finalQuery = query + " IF NOT EXISTS"; + consistencyLevel = org.apache.cassandra.distributed.api.ConsistencyLevel.SERIAL; + } + long startingRegularApplyCount = messageCount(Verb.ACCORD_APPLY_REQ); + cluster.coordinator(1).execute(finalQuery, consistencyLevel); + if (transactionalMode.ignoresSuppliedCommitCL() && migrated) + { + // Apply is async and there can be a lot of sources of regular APPLY + spinUntilTrue(() -> messageCount(Verb.ACCORD_APPLY_REQ) > startingRegularApplyCount); + assertEquals(0, messageCount(Verb.ACCORD_INTEROP_APPLY_REQ)); + } + else + { + assertEquals(3, messageCount(Verb.ACCORD_INTEROP_APPLY_REQ)); + } + assertTrue(failures.toString(), failures.isEmpty()); + }); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java index 539c8b9404..4afe1fa28f 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/InteropTokenRangeTest.java @@ -35,6 +35,9 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; +import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.Token; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ICoordinator; import org.apache.cassandra.distributed.api.SimpleQueryResult; @@ -109,7 +112,7 @@ public class InteropTokenRangeTest extends TestBaseImpl } } - private static NavigableSet tokens(SimpleQueryResult result) + public static NavigableSet tokens(SimpleQueryResult result) { NavigableSet set = new TreeSet<>(); while (result.hasNext()) @@ -117,7 +120,7 @@ public class InteropTokenRangeTest extends TestBaseImpl return set; } - private enum TokenOperator + public enum TokenOperator { eq("token(pk) = token(?)") { @Override @@ -127,6 +130,12 @@ public class InteropTokenRangeTest extends TestBaseImpl return new TreeSet<>(Collections.singleton(token)); return Collections.emptyNavigableSet(); } + + @Override + public boolean intersects(long token, Range range) + { + return range.contains(new LongToken(token)); + } }, lt("token(pk) < token(?)") { @@ -135,6 +144,13 @@ public class InteropTokenRangeTest extends TestBaseImpl { return tokens.headSet(token, false); } + + @Override + public boolean intersects(long token, Range range) + { + // <= is implemented as a min key bound which still intersects the range even though it returns no results + return token >= range.left.getLongValue() + 1; + } }, lte("token(pk) <= token(?)") { @@ -143,6 +159,11 @@ public class InteropTokenRangeTest extends TestBaseImpl { return tokens.headSet(token, true); } + @Override + public boolean intersects(long token, Range range) + { + return token >= range.left.getLongValue() + 1; + } }, gt("token(pk) > token(?)") { @@ -151,6 +172,12 @@ public class InteropTokenRangeTest extends TestBaseImpl { return tokens.tailSet(token, false); } + + @Override + public boolean intersects(long token, Range range) + { + return token < range.right.getLongValue(); + } }, gte("token(pk) >= token(?)") { @@ -159,10 +186,16 @@ public class InteropTokenRangeTest extends TestBaseImpl { return tokens.tailSet(token, true); } + + @Override + public boolean intersects(long token, Range range) + { + return token <= range.right.getLongValue(); + } }; ; - private final String condition; + public final String condition; TokenOperator(String s) { @@ -170,6 +203,9 @@ public class InteropTokenRangeTest extends TestBaseImpl } public abstract NavigableSet expected(long token, NavigableSet tokens); + + // Intersects for the purpose of executing the query not necessarily the results that are returned + public abstract boolean intersects(long token, Range range); } private NavigableSet tokens() diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordReadRaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordReadRaceTest.java new file mode 100644 index 0000000000..db5b026cc8 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordReadRaceTest.java @@ -0,0 +1,28 @@ +/* + * 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 MigrationFromAccordReadRaceTest extends AccordMigrationReadRaceTestBase +{ + @Override + protected boolean migratingAwayFromAccord() + { + return true; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordRaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordWriteRaceTest.java similarity index 91% rename from test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordRaceTest.java rename to test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordWriteRaceTest.java index 985bdc7dd9..1a8f30f765 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordRaceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationFromAccordWriteRaceTest.java @@ -18,7 +18,7 @@ package org.apache.cassandra.distributed.test.accord; -public class MigrationFromAccordRaceTest extends AccordMigrationRaceTestBase +public class MigrationFromAccordWriteRaceTest extends AccordMigrationWriteRaceTestBase { protected boolean migratingAwayFromAccord() { diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordReadRaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordReadRaceTest.java new file mode 100644 index 0000000000..07d65b2c59 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordReadRaceTest.java @@ -0,0 +1,28 @@ +/* + * 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 MigrationToAccordReadRaceTest extends AccordMigrationReadRaceTestBase +{ + @Override + protected boolean migratingAwayFromAccord() + { + return false; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordRaceTest.java b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordWriteRaceTest.java similarity index 91% rename from test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordRaceTest.java rename to test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordWriteRaceTest.java index aa00d9564b..c93b491b97 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordRaceTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/accord/MigrationToAccordWriteRaceTest.java @@ -18,7 +18,7 @@ package org.apache.cassandra.distributed.test.accord; -public class MigrationToAccordRaceTest extends AccordMigrationRaceTestBase +public class MigrationToAccordWriteRaceTest extends AccordMigrationWriteRaceTestBase { protected boolean migratingAwayFromAccord() { diff --git a/test/distributed/org/apache/cassandra/distributed/test/metrics/CoordinatorReadLatencyMetricTest.java b/test/distributed/org/apache/cassandra/distributed/test/metrics/CoordinatorReadLatencyMetricTest.java index ea83ef7536..c82dfb5597 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/metrics/CoordinatorReadLatencyMetricTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/metrics/CoordinatorReadLatencyMetricTest.java @@ -37,6 +37,7 @@ import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.test.TestBaseImpl; import org.apache.cassandra.metrics.ClientRequestsMetricsHolder; import org.apache.cassandra.service.consensus.TransactionalMode; +import org.apache.cassandra.service.consensus.migration.TransactionalMigrationFromMode; import org.apache.cassandra.service.paxos.Paxos; import static org.apache.cassandra.cql3.ast.Conditional.Where.Inequality.LESS_THAN; @@ -68,7 +69,7 @@ public class CoordinatorReadLatencyMetricTest extends TestBaseImpl cluster.get(1).runOnInstance(() -> Paxos.setPaxosVariant(Config.PaxosVariant.v2)); verifyTableLatency(cluster, 1, () -> verifyLatencyMetrics(cluster, select.toCQL(), ConsistencyLevel.SERIAL, select.binds())); - cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl WITH " + TransactionalMode.full.asCqlParam())); + cluster.schemaChange(withKeyspace("ALTER TABLE %s.tbl WITH " + TransactionalMode.full.asCqlParam() + " AND " + TransactionalMigrationFromMode.none.asCqlParam())); var txn = Txn.wrap(select); verifyTableLatency(cluster, 1, () -> verifyLatencyMetrics(cluster, txn.toCQL(), ConsistencyLevel.QUORUM, txn.binds())); diff --git a/test/distributed/org/apache/cassandra/distributed/util/QueryResultUtil.java b/test/distributed/org/apache/cassandra/distributed/util/QueryResultUtil.java index c0fe2515f9..bb6b975ca2 100644 --- a/test/distributed/org/apache/cassandra/distributed/util/QueryResultUtil.java +++ b/test/distributed/org/apache/cassandra/distributed/util/QueryResultUtil.java @@ -26,14 +26,13 @@ import java.util.function.Function; import java.util.function.Predicate; import com.google.monitoring.runtime.instrumentation.common.collect.Iterators; -import org.assertj.core.api.Assertions; -import org.assertj.core.data.Index; - import org.apache.cassandra.distributed.api.QueryResults; import org.apache.cassandra.distributed.api.Row; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.shared.AssertUtils; import org.apache.cassandra.tools.nodetool.formatter.TableBuilder; +import org.assertj.core.api.Assertions; +import org.assertj.core.data.Index; public class QueryResultUtil { @@ -275,6 +274,15 @@ public class QueryResultUtil return this; } + public SimpleQueryResultAssertHelper isDeepEqualTo(Object[][] values) + { + Object[][] results = qr.toObjectArrays(); + Assertions.assertThat(results) + .hasNumberOfRows(values.length) + .isDeepEqualTo(values); + return this; + } + public SimpleQueryResultAssertHelper hasSize(int size) { Assertions.assertThat(qr.toObjectArrays()).hasNumberOfRows(size); diff --git a/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java b/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java index a47f75d3e7..ccff69edb7 100644 --- a/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java +++ b/test/distributed/org/apache/cassandra/fuzz/topology/AccordTopologyMixupTest.java @@ -141,7 +141,7 @@ public class AccordTopologyMixupTest extends TopologyMixupTestBase tables = new ArrayList<>(); tables.add(cfName); - StorageService.instance.migrateConsensusProtocol("accord", keyspaces, tables, range.getKey() + ":" + range.getValue()); + StorageService.instance.migrateConsensusProtocol(keyspaces, tables, range.getKey() + ":" + range.getValue()); }; } } diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java index de3e5f15b7..8592ff23b5 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PairOfSequencesPaxosSimulation.java @@ -36,6 +36,7 @@ import org.apache.cassandra.distributed.api.IInvokableInstance; import org.apache.cassandra.distributed.api.Row; import org.apache.cassandra.distributed.api.SimpleQueryResult; import org.apache.cassandra.distributed.impl.Query; +import org.apache.cassandra.service.consensus.TransactionalMode; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionListener; import org.apache.cassandra.simulator.Debug; @@ -153,10 +154,12 @@ public class PairOfSequencesPaxosSimulation extends AbstractPairOfSequencesPaxos } final List historyCheckers = new ArrayList<>(); + private final TransactionalMode transactionalMode; public PairOfSequencesPaxosSimulation(SimulatedSystems simulated, Cluster cluster, ClusterActions.Options clusterOptions, + TransactionalMode transactionalMode, float readRatio, int concurrency, IntRange simulateKeyForSeconds, IntRange withinKeyConcurrency, ConsistencyLevel serialConsistency, RunnableActionScheduler scheduler, Debug debug, @@ -169,6 +172,7 @@ public class PairOfSequencesPaxosSimulation extends AbstractPairOfSequencesPaxos scheduler, debug, seed, primaryKeys, runForNanos, jitter); + this.transactionalMode = transactionalMode; } @Override @@ -252,7 +256,7 @@ public class PairOfSequencesPaxosSimulation extends AbstractPairOfSequencesPaxos @Override protected String createTableStmt() { - return "CREATE TABLE " + KEYSPACE + ".tbl (pk int, count int, seq1 text, seq2 list, PRIMARY KEY (pk))"; + return "CREATE TABLE " + KEYSPACE + ".tbl (pk int, count int, seq1 text, seq2 list, PRIMARY KEY (pk)) WITH " + transactionalMode.asCqlParam(); } @Override diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java index c54ba1c26d..dc8a538b6c 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosClusterSimulation.java @@ -85,7 +85,7 @@ class PaxosClusterSimulation extends ClusterSimulation implemen int[] primaryKeys = primaryKeys(seed, builder.primaryKeyCount()); KindOfSequence.Period jitter = RandomSource.Choices.uniform(KindOfSequence.values()).choose(random) .period(builder.schedulerJitterNanos(), random); - return new PairOfSequencesPaxosSimulation(simulated, cluster, options.changePaxosVariantTo(builder.finalPaxosVariant), + return new PairOfSequencesPaxosSimulation(simulated, cluster, options.changePaxosVariantTo(builder.finalPaxosVariant), builder.transactionalMode(), builder.readChance().select(random), builder.concurrency(), builder.primaryKeySeconds(), builder.withinKeyConcurrency(), builder.serialConsistency, schedulers, builder.debug(), seed, primaryKeys, builder.secondsToSimulate() >= 0 ? SECONDS.toNanos(builder.secondsToSimulate()) : -1, diff --git a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java index 095c79769a..7c9062d3d5 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java +++ b/test/simulator/main/org/apache/cassandra/simulator/paxos/PaxosSimulationRunner.java @@ -32,8 +32,8 @@ import io.airlift.airline.Option; import org.apache.cassandra.config.Config; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.simulator.SimulationRunner; -import org.apache.cassandra.simulator.utils.IntRange; import org.apache.cassandra.simulator.SimulatorUtils; +import org.apache.cassandra.simulator.utils.IntRange; public class PaxosSimulationRunner extends SimulationRunner { @@ -69,7 +69,7 @@ public class PaxosSimulationRunner extends SimulationRunner @Override protected void run( long seed, PaxosClusterSimulation.Builder builder) throws IOException { - if (Objects.equals(builder.transactionalMode(), "accord")) + if (!Objects.equals(builder.transactionalMode(), "off")) { // Apply handicaps builder.dcs(new IntRange(1, 1)); diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java index 3dfbe1116b..e83645bfae 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/ShortPaxosSimulationTest.java @@ -105,7 +105,7 @@ public class ShortPaxosSimulationTest public void casOnAccordSimulationTest() throws IOException { PaxosSimulationRunner.main(new String[] { "run", - "--lwt-strategy", "mixed_reads", + "--transactional-mode", "full", "-n", "3...6", "-t", "1000", "--cluster-action-limit", "0", diff --git a/test/unit/org/apache/cassandra/Util.java b/test/unit/org/apache/cassandra/Util.java index 70653233d5..9fa97f9202 100644 --- a/test/unit/org/apache/cassandra/Util.java +++ b/test/unit/org/apache/cassandra/Util.java @@ -768,6 +768,11 @@ public class Util .untilAsserted(() -> assertThat(message, call.call(), equalTo(expected))); } + public static void spinUntilTrue(Callable test) + { + spinUntilTrue(test, 10, TimeUnit.SECONDS); + } + public static void spinUntilTrue(Callable test, long timeoutInSeconds) { spinUntilTrue(test, timeoutInSeconds, TimeUnit.SECONDS); diff --git a/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java b/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java index 8513a54400..3bcb75eb9e 100644 --- a/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java +++ b/test/unit/org/apache/cassandra/cql3/ColumnSpecificationTest.java @@ -188,6 +188,7 @@ public class ColumnSpecificationTest extends CQLTester " AND compression = {'chunk_length_in_kb': '16', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}\n" + " AND memtable = 'default'\n" + " AND crc_check_chance = 1.0\n" + + " AND fast_path = 'keyspace'\n" + " AND default_time_to_live = 0\n" + " AND extensions = {}\n" + " AND gc_grace_seconds = 864000\n" + @@ -196,6 +197,8 @@ public class ColumnSpecificationTest extends CQLTester " AND memtable_flush_period_in_ms = 0\n" + " AND min_index_interval = 128\n" + " AND read_repair = 'BLOCKING'\n" + + " AND transactional_mode = 'off'\n" + + " AND transactional_migration_from = 'none'\n" + " AND speculative_retry = '99p';"; } diff --git a/test/unit/org/apache/cassandra/cql3/PreparedStatementsTest.java b/test/unit/org/apache/cassandra/cql3/PreparedStatementsTest.java index c6559ee851..47cae295d8 100644 --- a/test/unit/org/apache/cassandra/cql3/PreparedStatementsTest.java +++ b/test/unit/org/apache/cassandra/cql3/PreparedStatementsTest.java @@ -26,9 +26,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.google.common.util.concurrent.Uninterruptibles; -import org.apache.cassandra.tcm.ClusterMetadata; -import org.apache.cassandra.tcm.ClusterMetadataService; -import org.apache.cassandra.tcm.Epoch; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; @@ -50,11 +47,15 @@ import org.apache.cassandra.serializers.Int32Serializer; import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.ClientWarn; import org.apache.cassandra.service.accord.AccordService; +import org.apache.cassandra.tcm.ClusterMetadata; +import org.apache.cassandra.tcm.ClusterMetadataService; +import org.apache.cassandra.tcm.Epoch; import org.apache.cassandra.transport.ProtocolVersion; import org.apache.cassandra.transport.SimpleClient; import org.apache.cassandra.transport.messages.ResultMessage; import org.assertj.core.api.Assertions; +import static org.apache.cassandra.service.consensus.TransactionalMode.test_unsafe; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -200,7 +201,7 @@ public class PreparedStatementsTest extends CQLTester sessionSchemaUpdate(session, dropKsStatement); sessionSchemaUpdate(session, createKsStatement); - String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='unsafe';"; + String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='" + test_unsafe + "';"; String dropTableStatement = "DROP TABLE IF EXISTS " + KEYSPACE + ".qp_cleanup;"; sessionSchemaUpdate(session, createTableStatement); @@ -209,6 +210,7 @@ public class PreparedStatementsTest extends CQLTester PreparedStatement prepared = session.prepare(insert); PreparedStatement preparedBatch = session.prepare(batch(insert)); PreparedStatement preparedTxn = session.prepare(txn(insert)); + preparedTxn.setConsistencyLevel(com.datastax.driver.core.ConsistencyLevel.QUORUM); sessionSchemaUpdate(session, dropTableStatement); sessionSchemaUpdate(session, createTableStatement); @@ -249,7 +251,7 @@ public class PreparedStatementsTest extends CQLTester private void testInvalidatePreparedStatementOnAlter(ProtocolVersion version, boolean supportsMetadataChange) { Session session = sessionNet(version); - String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (a int PRIMARY KEY, b int, c int) WITH transactional_mode='unsafe';"; + String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (a int PRIMARY KEY, b int, c int) WITH transactional_mode='" + test_unsafe + "';"; String alterTableStatement = "ALTER TABLE " + KEYSPACE + ".qp_cleanup ADD d int;"; String dropTableStatement = "DROP TABLE IF EXISTS " + KEYSPACE + ".qp_cleanup;"; @@ -337,7 +339,7 @@ public class PreparedStatementsTest extends CQLTester private void testInvalidatePreparedStatementOnAlterUnchangedMetadata(ProtocolVersion version) { Session session = sessionNet(version); - String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (a int PRIMARY KEY, b int, c int) WITH transactional_mode='unsafe';"; + String createTableStatement = "CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".qp_cleanup (a int PRIMARY KEY, b int, c int) WITH transactional_mode='" + test_unsafe + "';"; String alterTableStatement = "ALTER TABLE " + KEYSPACE + ".qp_cleanup ADD d int;"; String dropTableStatement = "DROP TABLE IF EXISTS " + KEYSPACE + ".qp_cleanup;"; @@ -399,7 +401,7 @@ public class PreparedStatementsTest extends CQLTester sessionSchemaUpdate(session, dropKsStatement); sessionSchemaUpdate(session, createKsStatement); - runAndAwaitNextEpoch(() -> createTable("CREATE TABLE %s (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='unsafe';")); + runAndAwaitNextEpoch(() -> createTable("CREATE TABLE %s (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='" + test_unsafe + "';")); updateTxnState(); String insertCQL = "INSERT INTO " + currentTable() + " (id, cid, val) VALUES (?, ?, ?)"; @@ -408,6 +410,7 @@ public class PreparedStatementsTest extends CQLTester PreparedStatement preparedInsert = session.prepare(insertCQL); PreparedStatement preparedSelect = session.prepare(selectCQL); PreparedStatement preparedTxn = session.prepare(txn(selectCQL, insertCQL)); + preparedTxn.setConsistencyLevel(com.datastax.driver.core.ConsistencyLevel.QUORUM); session.execute(preparedInsert.bind(1, 1, "value")); assertEquals(1, session.execute(preparedSelect.bind(1)).all().size()); @@ -449,7 +452,7 @@ public class PreparedStatementsTest extends CQLTester String table = "custom_expr_test"; String index = "custom_index"; - sessionSchemaUpdate(session, String.format("CREATE TABLE IF NOT EXISTS %s.%s (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='unsafe';", + sessionSchemaUpdate(session, String.format("CREATE TABLE IF NOT EXISTS %s.%s (id int PRIMARY KEY, cid int, val text) WITH transactional_mode='" + test_unsafe + "';", KEYSPACE, table)); sessionSchemaUpdate(session, String.format("CREATE CUSTOM INDEX %s ON %s.%s(val) USING '%s'", index, KEYSPACE, table, StubIndex.class.getName())); diff --git a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java index 0462c6d61b..180aa45d4f 100644 --- a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java +++ b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerOutOfRangeTest.java @@ -50,8 +50,6 @@ import org.apache.cassandra.tcm.membership.NodeState; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.FBUtilities; -import static org.junit.Assert.assertEquals; - import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.MessageDelivery; import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.broadcastAddress; import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.bytesToken; @@ -59,6 +57,7 @@ import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelpe import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.randomInt; import static org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper.registerOutgoingMessageSink; import static org.apache.cassandra.net.Verb.READ_REQ; +import static org.junit.Assert.assertEquals; public class ReadCommandVerbHandlerOutOfRangeTest { @@ -219,7 +218,7 @@ public class ReadCommandVerbHandlerOutOfRangeTest false, 0, false, - false, + PotentialTxnConflicts.DISALLOW, tmd, FBUtilities.nowInSeconds(), ColumnFilter.all(tmd), @@ -255,7 +254,7 @@ public class ReadCommandVerbHandlerOutOfRangeTest false, 0, false, - false, + PotentialTxnConflicts.DISALLOW, tmd, FBUtilities.nowInSeconds(), ColumnFilter.all(tmd), diff --git a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java index bc2b285d98..c005011051 100644 --- a/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java +++ b/test/unit/org/apache/cassandra/db/ReadCommandVerbHandlerTest.java @@ -173,7 +173,7 @@ public class ReadCommandVerbHandlerTest false, 0, false, - false, + PotentialTxnConflicts.DISALLOW, metadata, FBUtilities.nowInSeconds(), ColumnFilter.all(metadata), diff --git a/test/unit/org/apache/cassandra/db/ReadResponseTest.java b/test/unit/org/apache/cassandra/db/ReadResponseTest.java index e594369037..ebe1cf0322 100644 --- a/test/unit/org/apache/cassandra/db/ReadResponseTest.java +++ b/test/unit/org/apache/cassandra/db/ReadResponseTest.java @@ -255,7 +255,7 @@ public class ReadResponseTest isDigest, 0, false, - false, + PotentialTxnConflicts.DISALLOW, metadata, FBUtilities.nowInSeconds(), ColumnFilter.all(metadata), diff --git a/test/unit/org/apache/cassandra/dht/RangeTest.java b/test/unit/org/apache/cassandra/dht/RangeTest.java index 50863924ba..e8198b9cb4 100644 --- a/test/unit/org/apache/cassandra/dht/RangeTest.java +++ b/test/unit/org/apache/cassandra/dht/RangeTest.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Random; import java.util.Set; @@ -41,18 +42,22 @@ import org.apache.cassandra.db.PartitionPosition; import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken; +import org.apache.cassandra.utils.Pair; +import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.cassandra.Util.range; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_RANGE_EXPENSIVE_CHECKS; -import static org.apache.cassandra.dht.Range.fromString; -import static org.apache.cassandra.dht.Range.normalize; import static org.apache.cassandra.dht.NormalizedRanges.normalizedRanges; +import static org.apache.cassandra.dht.Range.fromString; +import static org.apache.cassandra.dht.Range.intersectionAndRemainder; +import static org.apache.cassandra.dht.Range.normalize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -219,11 +224,11 @@ public class RangeTest extends CassandraTestBase { Set> correct = Range.rangeSet(ranges); Set> result1 = one.intersectionWith(two); - assert result1.equals(correct) : String.format("%s != %s", + assert result1.equals(correct) : format("%s != %s", StringUtils.join(result1, ","), StringUtils.join(correct, ",")); Set> result2 = two.intersectionWith(one); - assert result2.equals(correct) : String.format("%s != %s", + assert result2.equals(correct) : format("%s != %s", StringUtils.join(result2, ","), StringUtils.join(correct, ",")); } @@ -699,7 +704,7 @@ public class RangeTest extends CassandraTestBase for (Token t : tokensToTest) { if (checker.test(t) != Range.isInRanges(t, ranges)) // avoid running Joiner.on(..) every iteration - fail(String.format("This should never flap! If it does, it is a bug (ranges = %s, token = %s)", Joiner.on(",").join(ranges), t)); + fail(format("This should never flap! If it does, it is a bug (ranges = %s, token = %s)", Joiner.on(",").join(ranges), t)); } } } @@ -720,6 +725,12 @@ public class RangeTest extends CassandraTestBase { return new Range<>(t(left), t(right)); } + + private static Range r(Token left, Token right) + { + return new Range<>(left, right); + } + private static Token t(long t) { return new Murmur3Partitioner.LongToken(t); @@ -775,6 +786,11 @@ public class RangeTest extends CassandraTestBase return new Bounds<>(t(left), t(right)); } + private static AbstractBounds bounds(PartitionPosition left, boolean leftInclusive, PartitionPosition right, boolean rightInclusive) + { + return AbstractBounds.bounds(left, leftInclusive, right, rightInclusive); + } + @Test @UseMurmur3Partitioner public void testIsInNormalizedRanges() @@ -884,8 +900,283 @@ public class RangeTest extends CassandraTestBase b.subtract(a); b.subtract(b); - a.invert(); - b.invert(); + if (!a.isEmpty()) + a.invert(); + if (!b.isEmpty()) + b.invert(); + } + } + + @Test + public void testIntersectionAndRemainder() + { + // Intersection will only make max key bounds so use minKeyBound + // so you know where the bound came from + Token oneT = t(1); + PartitionPosition onePP = oneT.minKeyBound(); + Token twoT = t(2); + PartitionPosition twoPP = twoT.minKeyBound(); + Token threeT = t(3); + PartitionPosition threePP = threeT.minKeyBound(); + Token fourT = t(4); + PartitionPosition fourPP = fourT.minKeyBound(); + Token fiveT = t(5); + PartitionPosition fivePP = fiveT.minKeyBound(); + Token sixT = t(6); + PartitionPosition sixPP = sixT.minKeyBound(); + Token sevenT = t(7); + PartitionPosition sevenPP = sevenT.minKeyBound(); + Token eightT = t(8); + PartitionPosition eightPP = eightT.minKeyBound(); + Token minT = Murmur3Partitioner.MINIMUM; + PartitionPosition minPP = minT.minKeyBound(); + + Range r = r(threeT, sixT); + + // Completely before + testInclusivity(onePP, twoPP, r, + Pair.create(null, null), + Pair.create(null, null), + Pair.create(null, null), + Pair.create(null, null)); + + // Completely after + testInclusivity(sevenPP, eightPP, r, + Pair.create(null, bounds(sevenPP, true, eightPP, true)), + Pair.create(null, bounds(sevenPP, true, eightPP, false)), + Pair.create(null, bounds(sevenPP, false, eightPP, true)), + Pair.create(null, bounds(sevenPP, false, eightPP, false))); + + // Overlapping + testInclusivity(threePP, sixPP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, sixPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, sixPP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, sixPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, sixPP, false), null)); + + // Completely contained by range, should echo back the same bound + testInclusivity(fourPP, fivePP, r, + Pair.create(bounds(fourPP, true, fivePP, true), null), + Pair.create(bounds(fourPP, true, fivePP, false), null), + Pair.create(bounds(fourPP, false, fivePP, true), null), + Pair.create(bounds(fourPP, false, fivePP, false), null)); + + // Overlap left only + testInclusivity(threePP, fivePP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null)); + + // Overlap right only + testInclusivity(fourPP, sixPP, r, + Pair.create(bounds(fourPP, true, sixPP, true), null), + Pair.create(bounds(fourPP, true, sixPP, false), null), + Pair.create(bounds(fourPP, false, sixPP, true), null), + Pair.create(bounds(fourPP, false, sixPP, false), null)); + + // Contains range + testInclusivity(twoPP, sevenPP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, true)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, false)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, true)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, false))); + + // Split by range left bound + testInclusivity(twoPP, fivePP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null)); + + // Split by range right bound + testInclusivity(fivePP, sevenPP, r, + Pair.create(bounds(fivePP, true, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, true)), + Pair.create(bounds(fivePP, true, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, false)), + Pair.create(bounds(fivePP, false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, true)), + Pair.create(bounds(fivePP, false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, sevenPP, false))); + + /* + * Test size 1 bound + */ + // Completely before + testInclusivity(onePP, onePP, r, + Pair.create(null, null)); + + // Completely after + testInclusivity(eightPP, eightPP, r, + Pair.create(null, bounds(eightPP, true, eightPP, true))); + + // Completely contained by range, should echo back the same bound + testInclusivity(fivePP, fivePP, r, + Pair.create(bounds(fivePP, true, fivePP, true), null)); + + // Overlap left only + testInclusivity(threePP, threePP, r, + Pair.create(null, null)); + + // Overlap right only + testInclusivity(sixPP, sixPP, r, + Pair.create(bounds(sixPP, true, sixPP, true), null)); + + /* + * Test all cases where the right of Bounds is minimum + */ + // Completely after + testInclusivity(sevenPP, minPP, r, + Pair.create(null, bounds(sevenPP, true, minPP, true)), + Pair.create(null, bounds(sevenPP, true, minPP, false)), + Pair.create(null, bounds(sevenPP, false, minPP, true)), + Pair.create(null, bounds(sevenPP, false, minPP, false))); + + // Contains range + testInclusivity(twoPP, minPP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, true)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, false)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, true)), + Pair.create(bounds(threeT.maxKeyBound(), false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, false))); + + // Split by range right bound + testInclusivity(fivePP, minPP, r, + Pair.create(bounds(fivePP, true, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, true)), + Pair.create(bounds(fivePP, true, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, false)), + Pair.create(bounds(fivePP, false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, true)), + Pair.create(bounds(fivePP, false, sixT.maxKeyBound(), true), bounds(sixT.maxKeyBound(), false, minPP, false))); + + /* + * Test all cases where the right of the range is minimum + */ + r = r(threeT, minT); + // Completely before + testInclusivity(onePP, twoPP, r, + Pair.create(null, null), + Pair.create(null, null), + Pair.create(null, null), + Pair.create(null, null)); + + // Overlapping + testInclusivity(threePP, minPP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, false), null)); + + // Completely contained by range, should echo back the same bound + testInclusivity(fourPP, fivePP, r, + Pair.create(bounds(fourPP, true, fivePP, true), null), + Pair.create(bounds(fourPP, true, fivePP, false), null), + Pair.create(bounds(fourPP, false, fivePP, true), null), + Pair.create(bounds(fourPP, false, fivePP, false), null)); + + // Overlap left only + testInclusivity(threePP, fivePP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null)); + + // Overlap right only + testInclusivity(fourPP, minPP, r, + Pair.create(bounds(fourPP, true, minPP, true), null), + Pair.create(bounds(fourPP, true, minPP, false), null), + Pair.create(bounds(fourPP, false, minPP, true), null), + Pair.create(bounds(fourPP, false, minPP, false), null)); + + // Contains range + testInclusivity(twoPP, minPP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, minPP, false), null)); + + // Split by range left bound + testInclusivity(twoPP, fivePP, r, + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, true), null), + Pair.create(bounds(threeT.maxKeyBound(), false, fivePP, false), null)); + } + + private void testInclusivity(PartitionPosition left, PartitionPosition right, Range range, + Pair, AbstractBounds> expected) + { + testInclusivity(left, right, range, expected, null, null, null); + } + + private void testInclusivity(PartitionPosition left, PartitionPosition right, Range range, + Pair, AbstractBounds> expected1, + Pair, AbstractBounds> expected2, + Pair, AbstractBounds> expected3, + Pair, AbstractBounds> expected4) + { + testInclusivity(left, right, range, new Pair[] { expected1, expected2, expected3, expected4 }); + } + + private void testInclusivity(PartitionPosition left, PartitionPosition right, Range range, + Pair, AbstractBounds>[] expecteds) + { + int i = 0; + for (Boolean leftInclusive : ImmutableList.of(true, false)) + { + for (Boolean rightInclusive : ImmutableList.of(true, false)) + { + Pair, AbstractBounds> expected = expecteds[i++]; + if (expected == null) + continue; + AbstractBounds expectedIntersection = expected.left; + AbstractBounds expectedRemainder = expected.right; + AbstractBounds testBounds = bounds(left, leftInclusive, right, rightInclusive); + Pair, AbstractBounds> interSectionAndRemainder = intersectionAndRemainder(testBounds, range); + AbstractBounds intersection = interSectionAndRemainder.left; + AbstractBounds remainder = interSectionAndRemainder.right; + String message = format("Expected %s intersecting inclusive left %b, inclusive right %b, %s with %s", expected, leftInclusive, rightInclusive, bounds(left, leftInclusive, right, rightInclusive), range); + assertEquals(message, expected, intersectionAndRemainder(bounds(left, leftInclusive, right, rightInclusive), range)); + System.out.println(message.replace("Expected", "Expecting")); + + if (remainder == testBounds) + assertNull(intersection); + if (intersection == testBounds) + assertNull(remainder); + if (Objects.equals(remainder, testBounds) && remainder != testBounds) + fail("Should return existing bounds"); + if (Objects.equals(intersection, testBounds) && intersection != testBounds) + fail("Should return existing bounds"); + + // Need to validate that we roundtrip the actual exact input PartitionPosition and don't lose part of the input bound that might be needed + // Remainder can either be the entire thing because there is no intersection + // Remainder should always preserve the right bound since range is right inclusive, the left bound will always be a `maxKeyBound` from the range + if (remainder != null && remainder != testBounds) + { + assertTrue(remainder.right == expectedRemainder.right); + assertTrue(remainder.right == right); + assertEquals(remainder.inclusiveRight(), expectedRemainder.inclusiveRight()); + assertFalse(remainder.inclusiveLeft()); + assertFalse(remainder.left == left); + // Not strictly necessary, but we do always use the left of the range and create a new key bound + assertEquals(remainder.left, range.right.maxKeyBound()); + } + + // Range is left exclusive so the left should be preserved if it is greater than range left + // otherwise it should be replaced + if (intersection != null && intersection != testBounds) + { + if (intersection.left.getToken().compareTo(range.left) > 0) + { + assertEquals(intersection.inclusiveLeft(), expectedIntersection.inclusiveLeft()); + assertTrue(intersection.inclusiveRight()); + assertTrue(intersection.left == expectedIntersection.left); + assertTrue(intersection.left == left); + } + else + { + // Should be replaced by a KeyBound from the range + assertTrue(intersection.left != expectedIntersection.left); + assertTrue(intersection.left != left); + // Max bound since range is not left inclusive and excluded + assertEquals(intersection.left, range.left.maxKeyBound()); + } + } + } } } } diff --git a/test/unit/org/apache/cassandra/hints/HintsServiceTest.java b/test/unit/org/apache/cassandra/hints/HintsServiceTest.java index eb94473c29..45658705c2 100644 --- a/test/unit/org/apache/cassandra/hints/HintsServiceTest.java +++ b/test/unit/org/apache/cassandra/hints/HintsServiceTest.java @@ -232,7 +232,7 @@ public class HintsServiceTest public AsyncTxnResult mutateWithAccordAsync(ClusterMetadata cm, Mutation mutation, @Nullable ConsistencyLevel consistencyLevel, Dispatcher.RequestTime requestTime) { accordTxnCount.incrementAndGet(); - AsyncTxnResult asyncTxnResult = new AsyncTxnResult(AccordTestUtils.txnId(42, 43, 44)); + AsyncTxnResult asyncTxnResult = new AsyncTxnResult(AccordTestUtils.txnId(42, 43, 44), 42, consistencyLevel, true, requestTime); asyncTxnResult.setSuccess(new TxnData()); return asyncTxnResult; } diff --git a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java index 1e319eb738..ccabbcf265 100644 --- a/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java +++ b/test/unit/org/apache/cassandra/index/accord/RouteIndexTest.java @@ -618,13 +618,13 @@ public class RouteIndexTest extends CQLTester.InMemory @Override public boolean contains(AccordRoutingKey start, AccordRoutingKey end, AccordRoutingKey accordRoutingKey) { - return new TokenRange(start, end).contains(accordRoutingKey); + return TokenRange.create(start, end).contains(accordRoutingKey); } @Override public boolean intersects(TokenRange tokenRange, AccordRoutingKey start, AccordRoutingKey end) { - return tokenRange.compareIntersecting(new TokenRange(start, end)) == 0; + return tokenRange.compareIntersecting(TokenRange.create(start, end)) == 0; } }; @@ -670,8 +670,8 @@ public class RouteIndexTest extends CQLTester.InMemory b = tmp; } TableId tableId = tableIdGen.next(rs); - return new TokenRange(new TokenKey(tableId, new LongToken(a)), - new TokenKey(tableId, new LongToken(b))); + return TokenRange.create(new TokenKey(tableId, new LongToken(a)), + new TokenKey(tableId, new LongToken(b))); }; case 1: // small range Gen.IntGen rangeSizeGen = RANGE_SIZE_DISTRIBUTION.next(rand); @@ -685,16 +685,16 @@ public class RouteIndexTest extends CQLTester.InMemory a = b - rangeSize; } TableId tableId = tableIdGen.next(rs); - return new TokenRange(new TokenKey(tableId, new LongToken(a)), - new TokenKey(tableId, new LongToken(b))); + return TokenRange.create(new TokenKey(tableId, new LongToken(a)), + new TokenKey(tableId, new LongToken(b))); }; case 2: // single element return rs -> { int a = tokenGen.nextInt(rs); int b = a + 1; TableId tableId = tableIdGen.next(rs); - return new TokenRange(new TokenKey(tableId, new LongToken(a)), - new TokenKey(tableId, new LongToken(b))); + return TokenRange.create(new TokenKey(tableId, new LongToken(a)), + new TokenKey(tableId, new LongToken(b))); }; default: throw new AssertionError(); @@ -755,7 +755,7 @@ public class RouteIndexTest extends CQLTester.InMemory a = b; b = tmp; } - range = new TokenRange(a.start(), b.end()); + range = TokenRange.create(a.start(), b.end()); } break; default: diff --git a/test/unit/org/apache/cassandra/service/accord/AccordReadRepairTest.java b/test/unit/org/apache/cassandra/service/accord/AccordReadRepairTest.java index ea6587a17d..e050d43a37 100644 --- a/test/unit/org/apache/cassandra/service/accord/AccordReadRepairTest.java +++ b/test/unit/org/apache/cassandra/service/accord/AccordReadRepairTest.java @@ -69,7 +69,7 @@ public class AccordReadRepairTest extends AccordTestBase { testReadRepair(cluster -> cluster.coordinator(1).execute("SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 1 AND c = 1;", ConsistencyLevel.SERIAL), new Object[][] {{1, 1, 1, 1}}, - TransactionalMode.unsafe_writes, + TransactionalMode.test_unsafe_writes, 0, 2, 1, 0); } @@ -79,7 +79,7 @@ public class AccordReadRepairTest extends AccordTestBase // Even if the condition fails to apply the data checked when applying the condition should be repaired testReadRepair(cluster -> cluster.coordinator(1).execute("INSERT INTO " + qualifiedAccordTableName + " (k, c, v1) VALUES (1, 1, 99) IF NOT EXISTS;", ConsistencyLevel.SERIAL), new Object[][] {{false, 1, 1, 1, 1}}, - TransactionalMode.unsafe_writes, + TransactionalMode.test_unsafe_writes, 2, 0, 1, 0); } @@ -89,7 +89,7 @@ public class AccordReadRepairTest extends AccordTestBase // If the condition applies the read repair should preserve the existing timestamp testReadRepair(cluster -> cluster.coordinator(1).execute("UPDATE " + qualifiedAccordTableName + " SET v2 = 99 WHERE k = 1 and c = 1 IF EXISTS;", ConsistencyLevel.SERIAL), new Object[][] {{Boolean.TRUE}}, - TransactionalMode.unsafe_writes, + TransactionalMode.test_unsafe_writes, 2, 0, 1, 0); } @@ -103,7 +103,7 @@ public class AccordReadRepairTest extends AccordTestBase for (ConsistencyLevel cl : ImmutableList.of(ConsistencyLevel.QUORUM)) testReadRepair(cluster -> cluster.coordinator(1).execute("SELECT * FROM " + qualifiedAccordTableName + " WHERE k = 1 AND c = 1;", cl), new Object[][] {{1, 1, 1, 1}}, - TransactionalMode.unsafe_writes, + TransactionalMode.test_unsafe_writes, 0, 2, 0, 1); } @@ -113,7 +113,7 @@ public class AccordReadRepairTest extends AccordTestBase for (ConsistencyLevel cl : ImmutableList.of(ConsistencyLevel.QUORUM)) testReadRepair(cluster -> cluster.coordinator(1).execute("SELECT * FROM " + qualifiedAccordTableName + " WHERE TOKEN(k) > " + Long.MIN_VALUE + " AND TOKEN(k) < " + Long.MAX_VALUE, cl), new Object[][] {{1, 1, 1, 1}}, - TransactionalMode.unsafe_writes, + TransactionalMode.test_unsafe_writes, 0, 2, 0, 1); } diff --git a/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java b/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java index a516da4d8d..aac793d44d 100644 --- a/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java +++ b/test/unit/org/apache/cassandra/service/accord/AccordTestUtils.java @@ -50,10 +50,10 @@ import accord.local.DurableBefore; import accord.local.Node; import accord.local.Node.Id; import accord.local.NodeCommandStoreService; -import accord.local.TimeService; import accord.local.PreLoadContext; import accord.local.SafeCommandStore; import accord.local.StoreParticipants; +import accord.local.TimeService; import accord.primitives.Ballot; import accord.primitives.FullKeyRoute; import accord.primitives.FullRoute; @@ -98,8 +98,8 @@ import org.apache.cassandra.service.ClientState; import org.apache.cassandra.service.accord.api.AccordAgent; import org.apache.cassandra.service.accord.api.PartitionKey; import org.apache.cassandra.service.accord.txn.TxnData; -import org.apache.cassandra.service.accord.txn.TxnKeyRead; import org.apache.cassandra.service.accord.txn.TxnQuery; +import org.apache.cassandra.service.accord.txn.TxnRead; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.concurrent.Condition; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; @@ -218,7 +218,7 @@ public class AccordTestUtils public static Pair processTxnResultDirect(SafeCommandStore safeStore, TxnId txnId, PartialTxn txn, Timestamp executeAt) { - TxnKeyRead read = (TxnKeyRead) txn.read(); + TxnRead read = (TxnRead) txn.read(); Data readData = read.keys().stream().map(key -> { try { @@ -302,7 +302,7 @@ public class AccordTestUtils public static Txn createTxn(Txn.Kind kind, Seekables seekables) { - return new Txn.InMemory(kind, seekables, TxnKeyRead.EMPTY, TxnQuery.NONE, null); + return new Txn.InMemory(kind, seekables, TxnRead.empty(seekables.domain()), TxnQuery.NONE, null); } public static Ranges fullRange(Txn txn) diff --git a/test/unit/org/apache/cassandra/service/accord/txn/AbstractKeySortedTest.java b/test/unit/org/apache/cassandra/service/accord/txn/AbstractKeySortedTest.java index 001206d858..cdb16793b7 100644 --- a/test/unit/org/apache/cassandra/service/accord/txn/AbstractKeySortedTest.java +++ b/test/unit/org/apache/cassandra/service/accord/txn/AbstractKeySortedTest.java @@ -27,6 +27,8 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import accord.primitives.Routable.Domain; +import accord.primitives.Seekable; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.dht.ByteOrderedPartitioner; @@ -84,12 +86,12 @@ public class AbstractKeySortedTest { public SortedItems(Item... items) { - super(items); + super(items, Domain.Key); } public SortedItems(List items) { - super(items); + super(items, Domain.Key); } @Override @@ -99,11 +101,17 @@ public class AbstractKeySortedTest } @Override - PartitionKey getKey(Item item) + Seekable getKey(Item item) { return item.key; } + @Override + Domain domain() + { + return Domain.Key; + } + @Override Item[] newArray(int size) { diff --git a/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java b/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java index 832fbcbe23..dcb0952461 100644 --- a/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java +++ b/test/unit/org/apache/cassandra/service/reads/ReadExecutorTest.java @@ -47,8 +47,8 @@ import org.apache.cassandra.net.Message; import org.apache.cassandra.net.NoPayload; import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.KeyspaceParams; -import org.apache.cassandra.transport.Dispatcher; import org.apache.cassandra.tcm.Epoch; +import org.apache.cassandra.transport.Dispatcher; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -255,7 +255,7 @@ public class ReadExecutorTest MockSinglePartitionReadCommand(long timeout) { - super(cfs.metadata().epoch, false, 0, false, false, cfs.metadata(), 0, null, null, null, Util.dk("ry@n_luvs_teh_y@nk33z"), null, null, false, null); + super(cfs.metadata().epoch, false, 0, false, PotentialTxnConflicts.DISALLOW, cfs.metadata(), 0, null, null, null, Util.dk("ry@n_luvs_teh_y@nk33z"), null, null, false, null); this.timeout = timeout; } diff --git a/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java b/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java index 7bdd08349a..f4c230e790 100644 --- a/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java +++ b/test/unit/org/apache/cassandra/service/reads/repair/RepairedDataVerifierTest.java @@ -281,7 +281,7 @@ public class RepairedDataVerifierTest isDigest, 0, false, - false, + PotentialTxnConflicts.DISALLOW, metadata, FBUtilities.nowInSeconds(), ColumnFilter.all(metadata),